🔍Discovery Configuration

This guide shows you how to automatically configure your application using Oten IDP's OpenID Connect Discovery endpoint instead of hardcoding configuration values.

Need context? Check the Configuration Reference for complete endpoint details.

What You'll Learn

In this guide, you will:

  • Use OpenID Connect Discovery to automatically fetch configuration

  • Implement dynamic configuration loading in different programming languages

  • Handle configuration caching and error scenarios

  • Validate your configuration setup

Why Use Discovery?

Benefits of Discovery Configuration

Automatic Updates: Configuration changes are picked up automatically ✅ Environment Agnostic: Same code works across dev/staging/production ✅ Reduced Errors: No hardcoded URLs that can become outdated ✅ Standards Compliant: Uses OpenID Connect Discovery specification ✅ Future Proof: New features are automatically available

Discovery vs Manual Configuration

// ❌ Manual configuration (not recommended)
const config = {
  authorizationEndpoint: 'https://account.sbx.oten.dev/v1/oauth/authorize',
  tokenEndpoint: 'https://account.sbx.oten.dev/v1/oauth/token',
  // ... hardcoded values that may change
};

// ✅ Discovery configuration (recommended)
const response = await fetch('https://account.sbx.oten.dev/.well-known/openid-configuration');
const config = await response.json();
// Automatically gets latest configuration

Quick Start

Basic Discovery Implementation

class OtenDiscovery {
  constructor(environment = 'development') {
    this.environment = environment;
    this.config = null;
    this.lastFetch = null;
    this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
  }

  getDiscoveryUrl() {
    const baseUrl = this.environment === 'production'
      ? 'https://account.oten.com'
      : 'https://account.sbx.oten.dev';

    return `${baseUrl}/.well-known/openid-configuration`;
  }

  async fetchConfiguration() {
    const discoveryUrl = this.getDiscoveryUrl();
    
    try {
      console.log(`Fetching configuration from: ${discoveryUrl}`);
      const response = await fetch(discoveryUrl);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      const config = await response.json();
      this.config = config;
      this.lastFetch = Date.now();
      
      console.log('✅ Configuration loaded successfully');
      return config;
    } catch (error) {
      console.error('❌ Failed to fetch configuration:', error.message);
      throw error;
    }
  }

  async getConfiguration() {
    // Check if we need to fetch or refresh configuration
    if (!this.config || this.shouldRefresh()) {
      await this.fetchConfiguration();
    }
    
    return this.config;
  }

  shouldRefresh() {
    if (!this.lastFetch) return true;
    return (Date.now() - this.lastFetch) > this.cacheTimeout;
  }

  async getEndpoint(type) {
    const config = await this.getConfiguration();
    
    const endpoints = {
      authorization: config.authorization_endpoint,
      token: config.token_endpoint,
      userinfo: config.userinfo_endpoint,
      jwks: config.jwks_uri,
      issuer: config.issuer
    };

    if (!endpoints[type]) {
      throw new Error(`Unknown endpoint type: ${type}`);
    }

    return endpoints[type];
  }

  async getSupportedFeatures() {
    const config = await this.getConfiguration();
    
    return {
      scopes: config.scopes_supported || [],
      responseTypes: config.response_types_supported || [],
      grantTypes: config.grant_types_supported || [],
      codeChallengeMethod: config.code_challenge_methods_supported || [],
      tokenAuthMethods: config.token_endpoint_auth_methods_supported || [],
      claims: config.claims_supported || [],
      idTokenSigningAlgs: config.id_token_signing_alg_values_supported || []
    };
  }
}

// Usage example
const discovery = new OtenDiscovery('development');

// Get specific endpoints
const authEndpoint = await discovery.getEndpoint('authorization');
const tokenEndpoint = await discovery.getEndpoint('token');

// Get supported features
const features = await discovery.getSupportedFeatures();
console.log('Supported scopes:', features.scopes);
console.log('Supported grant types:', features.grantTypes);

Framework-Specific Examples

Express.js with Passport

const passport = require('passport');
const { Strategy: OIDCStrategy } = require('passport-openidconnect');

async function configurePassport() {
  const discovery = new OtenDiscovery(process.env.NODE_ENV);
  const config = await discovery.getConfiguration();

  passport.use('oten', new OIDCStrategy({
    issuer: config.issuer,
    authorizationURL: config.authorization_endpoint,
    tokenURL: config.token_endpoint,
    userInfoURL: config.userinfo_endpoint,
    clientID: process.env.OTEN_CLIENT_ID,
    clientSecret: process.env.OTEN_CLIENT_SECRET,
    callbackURL: process.env.OTEN_REDIRECT_URI,
    scope: ['openid', 'profile', 'email']
  }, (issuer, profile, done) => {
    return done(null, profile);
  }));
}

// Initialize passport with discovery
configurePassport().catch(console.error);

React SPA with OIDC Client

import { UserManager } from 'oidc-client-ts';

class OtenAuth {
  constructor() {
    this.userManager = null;
    this.discovery = new OtenDiscovery('development');
  }

  async initialize() {
    const config = await this.discovery.getConfiguration();
    
    this.userManager = new UserManager({
      authority: config.issuer,
      client_id: process.env.REACT_APP_CLIENT_ID,
      redirect_uri: `${window.location.origin}/callback`,
      response_type: 'code',
      scope: 'openid profile email',
      post_logout_redirect_uri: window.location.origin,
      automaticSilentRenew: true,
      silent_redirect_uri: `${window.location.origin}/silent-callback`,
      
      // Use discovered endpoints
      metadata: {
        issuer: config.issuer,
        authorization_endpoint: config.authorization_endpoint,
        token_endpoint: config.token_endpoint,
        userinfo_endpoint: config.userinfo_endpoint,
        jwks_uri: config.jwks_uri,
        end_session_endpoint: config.end_session_endpoint
      }
    });

    return this.userManager;
  }

  async login() {
    if (!this.userManager) {
      await this.initialize();
    }
    return this.userManager.signinRedirect();
  }
}

// Usage
const auth = new OtenAuth();
await auth.initialize();

Python Flask with Authlib

import os
import requests
from authlib.integrations.flask_client import OAuth
from flask import Flask

class OtenDiscovery:
    def __init__(self, environment='development'):
        self.environment = environment
        self.config = None

    def get_discovery_url(self):
        base_url = ('https://account.oten.com'
                   if self.environment == 'production'
                   else 'https://account.sbx.oten.dev')
        return f"{base_url}/.well-known/openid-configuration"
    
    def fetch_configuration(self):
        discovery_url = self.get_discovery_url()
        response = requests.get(discovery_url)
        response.raise_for_status()
        self.config = response.json()
        return self.config
    
    def get_configuration(self):
        if not self.config:
            self.fetch_configuration()
        return self.config

def create_app():
    app = Flask(__name__)
    oauth = OAuth(app)
    
    # Initialize discovery
    discovery = OtenDiscovery(os.environ.get('FLASK_ENV', 'development'))
    config = discovery.get_configuration()

    # Register OAuth client with discovered configuration
    oten = oauth.register(
        name='oten',
        client_id=os.environ.get('OTEN_CLIENT_ID'),
        client_secret=os.environ.get('OTEN_CLIENT_SECRET'),
        server_metadata_url=discovery.get_discovery_url(),
        client_kwargs={
            'scope': 'openid profile email',
            'code_challenge_method': 'S256'
        }
    )

    return app, oten

app, oten = create_app()

Configuration Caching and Refresh

Smart Caching Strategy

class ConfigurationCache {
  constructor(ttl = 5 * 60 * 1000) { // 5 minutes default TTL
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value) {
    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear() {
    this.cache.clear();
  }
}

class CachedOtenDiscovery extends OtenDiscovery {
  constructor(environment = 'development', cacheTTL = 5 * 60 * 1000) {
    super(environment);
    this.cache = new ConfigurationCache(cacheTTL);
  }

  async getConfiguration() {
    const cacheKey = `config_${this.environment}`;
    let config = this.cache.get(cacheKey);

    if (!config) {
      config = await this.fetchConfiguration();
      this.cache.set(cacheKey, config);
    }

    return config;
  }

  clearCache() {
    this.cache.clear();
  }
}

Testing Discovery Configuration

Configuration Validation

async function validateDiscoveryConfiguration(environment = 'development') {
  const discovery = new OtenDiscovery(environment);
  
  console.log(`Validating discovery configuration for ${environment}...`);

  try {
    // Fetch configuration
    const config = await discovery.getConfiguration();
    
    // Validate required fields
    const requiredFields = [
      'issuer',
      'authorization_endpoint', 
      'token_endpoint',
      'jwks_uri',
      'scopes_supported',
      'response_types_supported'
    ];

    const missingFields = requiredFields.filter(field => !config[field]);
    
    if (missingFields.length > 0) {
      throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
    }

    // Validate endpoints are accessible
    const endpoints = [
      { name: 'JWKS', url: config.jwks_uri },
      { name: 'Authorization', url: config.authorization_endpoint, method: 'HEAD' }
    ];

    for (const endpoint of endpoints) {
      try {
        const response = await fetch(endpoint.url, { 
          method: endpoint.method || 'GET',
          timeout: 5000 
        });
        
        if (response.ok) {
          console.log(`✅ ${endpoint.name} endpoint accessible`);
        } else {
          console.log(`⚠️ ${endpoint.name} endpoint returned ${response.status}`);
        }
      } catch (error) {
        console.log(`❌ ${endpoint.name} endpoint error: ${error.message}`);
      }
    }

    // Validate supported features
    const features = await discovery.getSupportedFeatures();
    
    console.log('\nDiscovered Configuration:');
    console.log(`Issuer: ${config.issuer}`);
    console.log(`Scopes: ${features.scopes.join(', ')}`);
    console.log(`Response Types: ${features.responseTypes.join(', ')}`);
    console.log(`Grant Types: ${features.grantTypes.join(', ')}`);
    console.log(`PKCE Methods: ${features.codeChallengeMethod.join(', ')}`);

    return true;

  } catch (error) {
    console.error('❌ Discovery validation failed:', error.message);
    return false;
  }
}

// Run validation
validateDiscoveryConfiguration('development')
  .then(success => {
    if (success) {
      console.log('\nDiscovery configuration validation passed!');
    } else {
      console.log('\nDiscovery configuration validation failed!');
    }
  });

Next: Step 1: Choose Library

Last updated