🔍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 configurationQuick 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