Sample Code

This document provides complete, working code examples for integrating with Oten IDP across multiple programming languages.

Prerequisites

Before using these examples:

  1. Register your application with Oten IDP

  2. Obtain your client_id and client_secret (if applicable)

  3. Set up your redirect URI

  4. Choose your JAR signing method (HS256 or EdDSA)

Environment Variables

All examples use these environment variables:

# Required for all examples
OTEN_CLIENT_ID=your_client_id
OTEN_REDIRECT_URI=https://yourapp.com/callback

# For HS256 signing
OTEN_CLIENT_SECRET=your_client_secret

# For EdDSA signing
OTEN_PRIVATE_KEY_PATH=./jar-private-key.pem
OTEN_KEY_ID=your-key-id

# Environment
OTEN_ENV=production  # or development

🌐 JavaScript/Node.js

Complete Integration Example

// package.json dependencies:
// {
//   "jsonwebtoken": "^9.0.0",
//   "express": "^4.18.0",
//   "crypto": "^1.0.1"
// }

const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs');

const app = express();

// Configuration
const config = {
  clientId: process.env.OTEN_CLIENT_ID,
  clientSecret: process.env.OTEN_CLIENT_SECRET,
  redirectUri: process.env.OTEN_REDIRECT_URI,
  privateKeyPath: process.env.OTEN_PRIVATE_KEY_PATH,
  keyId: process.env.OTEN_KEY_ID,
  environment: process.env.OTEN_ENV || 'production'
};

const baseUrl = config.environment === 'production'
  ? 'https://account.oten.com'
  : 'https://account.sbx.oten.dev';

// PKCE helper functions
function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url');
}

async function generateCodeChallenge(verifier) {
  const hash = crypto.createHash('sha256').update(verifier).digest();
  return hash.toString('base64url');
}

// JAR creation functions
async function createJARWithHS256(authParams) {
  const now = Math.floor(Date.now() / 1000);

  const jarPayload = {
    // Required JWT claims
    iss: authParams.client_id,
    aud: baseUrl,
    iat: now,
    exp: now + 300, // 5 minutes
    jti: crypto.randomUUID(),

    // OAuth parameters
    ...authParams
  };

  return jwt.sign(jarPayload, config.clientSecret, {
    algorithm: 'HS256',
    header: { alg: 'HS256', typ: 'JWT' }
  });
}

async function createJARWithEdDSA(authParams) {
  const now = Math.floor(Date.now() / 1000);
  const privateKey = fs.readFileSync(config.privateKeyPath, 'utf8');

  const jarPayload = {
    // Required JWT claims
    iss: authParams.client_id,
    aud: baseUrl,
    iat: now,
    exp: now + 300, // 5 minutes
    jti: crypto.randomUUID(),

    // OAuth parameters
    ...authParams
  };

  return jwt.sign(jarPayload, privateKey, {
    algorithm: 'EdDSA',
    header: { alg: 'EdDSA', typ: 'JWT', kid: config.keyId }
  });
}

// Routes
app.get('/login', async (req, res) => {
  try {
    // Generate PKCE parameters
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = await generateCodeChallenge(codeVerifier);
    const state = crypto.randomUUID();

    // Store in session (use proper session storage in production)
    req.session = { codeVerifier, state };

    // Create OAuth parameters
    const authParams = {
      client_id: config.clientId,
      redirect_uri: config.redirectUri,
      response_type: 'code',
      scope: 'openid profile email',
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256'
    };

    // Create JAR
    const requestJWT = config.clientSecret
      ? await createJARWithHS256(authParams)
      : await createJARWithEdDSA(authParams);

    // Redirect to authorization endpoint
    const authURL = `${baseUrl}/v1/oauth/authorize?client_id=${config.clientId}&request=${requestJWT}`;
    res.redirect(authURL);

  } catch (error) {
    console.error('Login error:', error);
    res.status(500).send('Login failed');
  }
});

app.get('/callback', async (req, res) => {
  try {
    const { code, state, error } = req.query;

    if (error) {
      return res.status(400).send(`Authorization error: ${error}`);
    }

    // Verify state
    if (state !== req.session.state) {
      return res.status(400).send('Invalid state parameter');
    }

    // Exchange code for tokens
    const tokenResponse = await fetch(`${baseUrl}/v1/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: config.redirectUri,
        client_id: config.clientId,
        code_verifier: req.session.codeVerifier,
        ...(config.clientSecret && { client_secret: config.clientSecret })
      })
    });

    const tokens = await tokenResponse.json();

    if (tokens.status === 'OK') {
      const tokenData = tokens.data[0];

      // Store tokens securely (use proper storage in production)
      req.session.tokens = tokenData;

      // Decode ID token for user info
      const idTokenPayload = JSON.parse(Buffer.from(tokenData.id_token.split('.')[1], 'base64').toString());

      res.json({
        message: 'Login successful',
        user: {
          id: idTokenPayload.sub,
          email: idTokenPayload.email,
          name: idTokenPayload.name
        },
        tokens: {
          access_token: tokenData.access_token,
          expires_in: tokenData.expires_in
        }
      });
    } else {
      res.status(400).json({ error: 'Token exchange failed', details: tokens });
    }

  } catch (error) {
    console.error('Callback error:', error);
    res.status(500).send('Callback processing failed');
  }
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
  console.log('Visit http://localhost:3000/login to start OAuth flow');
});

🐍 Python

Complete Integration Example

# requirements.txt:
# flask==2.3.0
# pyjwt[crypto]==2.8.0
# requests==2.31.0
# cryptography==41.0.0

from flask import Flask, request, redirect, session, jsonify
import jwt
import time
import uuid
import hashlib
import base64
import os
import requests
from cryptography.hazmat.primitives import serialization

app = Flask(__name__)
app.secret_key = 'your-secret-key-change-in-production'

# Configuration
class Config:
    CLIENT_ID = os.getenv('OTEN_CLIENT_ID')
    CLIENT_SECRET = os.getenv('OTEN_CLIENT_SECRET')
    REDIRECT_URI = os.getenv('OTEN_REDIRECT_URI')
    PRIVATE_KEY_PATH = os.getenv('OTEN_PRIVATE_KEY_PATH')
    KEY_ID = os.getenv('OTEN_KEY_ID')
    ENVIRONMENT = os.getenv('OTEN_ENV', 'production')

    @property
    def base_url(self):
        return ('https://account.oten.com' if self.ENVIRONMENT == 'production'
                else 'https://account.sbx.oten.dev')

config = Config()

# PKCE helper functions
def generate_code_verifier():
    return base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip('=')

def generate_code_challenge(verifier):
    digest = hashlib.sha256(verifier.encode('utf-8')).digest()
    return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')

# JAR creation functions
def create_jar_with_hs256(auth_params):
    now = int(time.time())

    jar_payload = {
        # Required JWT claims
        'iss': auth_params['client_id'],
        'aud': config.base_url,
        'iat': now,
        'exp': now + 300,  # 5 minutes
        'jti': str(uuid.uuid4()),

        # OAuth parameters
        **auth_params
    }

    return jwt.encode(
        jar_payload,
        config.CLIENT_SECRET,
        algorithm='HS256',
        headers={'alg': 'HS256', 'typ': 'JWT'}
    )

def create_jar_with_eddsa(auth_params):
    now = int(time.time())

    # Load private key
    with open(config.PRIVATE_KEY_PATH, 'rb') as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None
        )

    jar_payload = {
        # Required JWT claims
        'iss': auth_params['client_id'],
        'aud': config.base_url,
        'iat': now,
        'exp': now + 300,  # 5 minutes
        'jti': str(uuid.uuid4()),

        # OAuth parameters
        **auth_params
    }

    return jwt.encode(
        jar_payload,
        private_key,
        algorithm='EdDSA',
        headers={'alg': 'EdDSA', 'typ': 'JWT', 'kid': config.KEY_ID}
    )

# Routes
@app.route('/login')
def login():
    try:
        # Generate PKCE parameters
        code_verifier = generate_code_verifier()
        code_challenge = generate_code_challenge(code_verifier)
        state = str(uuid.uuid4())

        # Store in session
        session['code_verifier'] = code_verifier
        session['state'] = state

        # Create OAuth parameters
        auth_params = {
            'client_id': config.CLIENT_ID,
            'redirect_uri': config.REDIRECT_URI,
            'response_type': 'code',
            'scope': 'openid profile email',
            'state': state,
            'code_challenge': code_challenge,
            'code_challenge_method': 'S256'
        }

        # Create JAR
        request_jwt = (create_jar_with_hs256(auth_params) if config.CLIENT_SECRET
                      else create_jar_with_eddsa(auth_params))

        # Redirect to authorization endpoint
        auth_url = f"{config.base_url}/v1/oauth/authorize?client_id={config.CLIENT_ID}&request={request_jwt}"
        return redirect(auth_url)

    except Exception as e:
        return f"Login failed: {str(e)}", 500

@app.route('/callback')
def callback():
    try:
        code = request.args.get('code')
        state = request.args.get('state')
        error = request.args.get('error')

        if error:
            return f"Authorization error: {error}", 400

        # Verify state
        if state != session.get('state'):
            return "Invalid state parameter", 400

        # Exchange code for tokens
        token_data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': config.REDIRECT_URI,
            'client_id': config.CLIENT_ID,
            'code_verifier': session.get('code_verifier')
        }

        if config.CLIENT_SECRET:
            token_data['client_secret'] = config.CLIENT_SECRET

        token_response = requests.post(
            f"{config.base_url}/v1/oauth/token",
            data=token_data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )

        tokens = token_response.json()

        if tokens.get('status') == 'OK':
            token_data = tokens['data'][0]

            # Store tokens in session
            session['tokens'] = token_data

            # Decode ID token for user info
            id_token_payload = jwt.decode(
                token_data['id_token'],
                options={"verify_signature": False}
            )

            return jsonify({
                'message': 'Login successful',
                'user': {
                    'id': id_token_payload.get('sub'),
                    'email': id_token_payload.get('email'),
                    'name': id_token_payload.get('name')
                },
                'tokens': {
                    'access_token': token_data['access_token'],
                    'expires_in': token_data['expires_in']
                }
            })
        else:
            return jsonify({'error': 'Token exchange failed', 'details': tokens}), 400

    except Exception as e:
        return f"Callback processing failed: {str(e)}", 500

if __name__ == '__main__':
    app.run(debug=True, port=3000)

🐹 Go

Complete Integration Example

// go.mod:
// module oten-example
//
// go 1.21
//
// require (
//     github.com/golang-jwt/jwt/v5 v5.0.0
//     github.com/gorilla/mux v1.8.0
//     github.com/gorilla/sessions v1.2.1
// )

package main

import (
    "crypto/ed25519"
    "crypto/rand"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/gorilla/mux"
    "github.com/gorilla/sessions"
)

// Configuration
type Config struct {
    ClientID        string
    ClientSecret    string
    RedirectURI     string
    PrivateKeyPath  string
    KeyID          string
    Environment    string
}

func (c *Config) BaseURL() string {
    if c.Environment == "production" {
        return "https://account.oten.com"
    }
    return "https://account.sbx.oten.dev"
}

var (
    config = &Config{
        ClientID:       os.Getenv("OTEN_CLIENT_ID"),
        ClientSecret:   os.Getenv("OTEN_CLIENT_SECRET"),
        RedirectURI:    os.Getenv("OTEN_REDIRECT_URI"),
        PrivateKeyPath: os.Getenv("OTEN_PRIVATE_KEY_PATH"),
        KeyID:         os.Getenv("OTEN_KEY_ID"),
        Environment:   getEnv("OTEN_ENV", "production"),
    }
    store = sessions.NewCookieStore([]byte("your-secret-key-change-in-production"))
)

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

// PKCE helper functions
func generateCodeVerifier() string {
    bytes := make([]byte, 32)
    rand.Read(bytes)
    return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes)
}

func generateCodeChallenge(verifier string) string {
    hash := sha256.Sum256([]byte(verifier))
    return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
}

Last updated