OAuth2 and OIDC Integration
This guide demonstrates how to integrate your application with UAAA using standard OAuth 2.0 and OpenID Connect protocols.
Overview
UAAA implements:
- OAuth 2.0: Authorization framework (RFC 6749)
- PKCE: Proof Key for Code Exchange (RFC 7636)
- OpenID Connect: Identity layer on OAuth 2.0
- Discovery: Automatic configuration discovery
Prerequisites
1. Register Your Application
bash
curl -X POST https://auth.example.com/api/app \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"_id": "myapp.example.com",
"name": "My Application",
"description": "My awesome app",
"redirectUris": [
"https://myapp.example.com/auth/callback",
"https://myapp.example.com/auth/silent-refresh"
],
"defaultPermissions": ["myapp.example.com/**"]
}'2. Discover OpenID Configuration
bash
curl https://auth.example.com/.well-known/openid-configurationResponse:
json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/oauth/authorize",
"token_endpoint": "https://auth.example.com/oauth/token",
"userinfo_endpoint": "https://auth.example.com/oauth/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"scopes_supported": [
"openid",
"profile",
"email",
"phone",
"uperm://**"
],
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"subject_types_supported": ["public"]
}Integration Flow
Authorization Code Flow with PKCE
mermaid
sequenceDiagram
participant Client
participant UAAA
Note over Client: 1. Generate code_verifier<br/>& code_challenge
Client->>UAAA: 2. Redirect to /oauth/authorize<br/>with code_challenge
Note over UAAA: 3. User authenticates<br/>and authorizes
UAAA->>Client: 4. Redirect to callback<br/>with authorization code
Client->>UAAA: 5. POST /oauth/token<br/>with code_verifier
UAAA->>Client: 6. Return access_token,<br/>id_token, refresh_token
Note over Client: 7. Access protected resources<br/>with access_tokenImplementation Examples
Node.js / Express
javascript
const express = require('express')
const crypto = require('crypto')
const session = require('express-session')
const axios = require('axios')
const app = express()
app.use(session({
secret: 'session-secret',
resave: false,
saveUninitialized: false
}))
const config = {
clientId: 'myapp.example.com',
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo',
redirectUri: 'https://myapp.example.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'uperm://myapp.example.com/**']
}
// Generate PKCE challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url')
return { verifier, challenge }
}
// Login route
app.get('/auth/login', (req, res) => {
const pkce = generatePKCE()
const state = crypto.randomBytes(16).toString('hex')
// Store PKCE verifier and state in session
req.session.pkceVerifier = pkce.verifier
req.session.oauthState = state
const params = new URLSearchParams({
client_id: config.clientId,
response_type: 'code',
redirect_uri: config.redirectUri,
scope: config.scopes.join(' '),
state: state,
code_challenge: pkce.challenge,
code_challenge_method: 'S256'
})
res.redirect(`${config.authorizationEndpoint}?${params}`)
})
// Callback route
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state')
}
try {
// Exchange code for tokens
const tokenResponse = await axios.post(
config.tokenEndpoint,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: req.session.pkceVerifier
}),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
)
const { access_token, id_token, refresh_token } = tokenResponse.data
// Store tokens in session
req.session.accessToken = access_token
req.session.idToken = id_token
req.session.refreshToken = refresh_token
// Clean up PKCE data
delete req.session.pkceVerifier
delete req.session.oauthState
res.redirect('/dashboard')
} catch (error) {
console.error('Token exchange error:', error.response?.data || error.message)
res.status(500).send('Authentication failed')
}
})
// Protected route
app.get('/dashboard', async (req, res) => {
if (!req.session.accessToken) {
return res.redirect('/auth/login')
}
try {
// Fetch user info
const userInfo = await axios.get(config.userInfoEndpoint, {
headers: { Authorization: `Bearer ${req.session.accessToken}` }
})
res.json({
message: 'Welcome!',
user: userInfo.data
})
} catch (error) {
// Token expired or invalid
if (error.response?.status === 401) {
return res.redirect('/auth/login')
}
res.status(500).send('Error fetching user info')
}
})
// API endpoint
app.get('/api/data', async (req, res) => {
if (!req.session.accessToken) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Use access token to call protected API
res.json({ data: 'Protected data', user: req.session.userId })
})
// Logout
app.get('/auth/logout', (req, res) => {
const idToken = req.session.idToken
req.session.destroy(() => {
// Redirect to UAAA logout
res.redirect(
`https://auth.example.com/oauth/logout?` +
`id_token_hint=${idToken}&` +
`post_logout_redirect_uri=https://myapp.example.com`
)
})
})
app.listen(3000, () => {
console.log('App listening on http://localhost:3000')
})Python / Flask
python
from flask import Flask, redirect, request, session, url_for
import requests
import secrets
import hashlib
import base64
app = Flask(__name__)
app.secret_key = 'your-secret-key'
CONFIG = {
'client_id': 'myapp.example.com',
'authorization_endpoint': 'https://auth.example.com/oauth/authorize',
'token_endpoint': 'https://auth.example.com/oauth/token',
'userinfo_endpoint': 'https://auth.example.com/oauth/userinfo',
'redirect_uri': 'https://myapp.example.com/auth/callback',
'scopes': ['openid', 'profile', 'email', 'uperm://myapp.example.com/**']
}
def generate_pkce():
"""Generate PKCE code verifier and challenge"""
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).decode('utf-8').rstrip('=')
return verifier, challenge
@app.route('/auth/login')
def login():
verifier, challenge = generate_pkce()
state = secrets.token_hex(16)
session['pkce_verifier'] = verifier
session['oauth_state'] = state
params = {
'client_id': CONFIG['client_id'],
'response_type': 'code',
'redirect_uri': CONFIG['redirect_uri'],
'scope': ' '.join(CONFIG['scopes']),
'state': state,
'code_challenge': challenge,
'code_challenge_method': 'S256'
}
auth_url = f"{CONFIG['authorization_endpoint']}?{requests.compat.urlencode(params)}"
return redirect(auth_url)
@app.route('/auth/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
if state != session.get('oauth_state'):
return 'Invalid state', 400
# Exchange code for tokens
token_response = requests.post(
CONFIG['token_endpoint'],
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': CONFIG['redirect_uri'],
'client_id': CONFIG['client_id'],
'code_verifier': session.get('pkce_verifier')
},
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
if token_response.status_code != 200:
return 'Token exchange failed', 500
tokens = token_response.json()
session['access_token'] = tokens['access_token']
session['id_token'] = tokens['id_token']
session['refresh_token'] = tokens.get('refresh_token')
# Clean up
session.pop('pkce_verifier', None)
session.pop('oauth_state', None)
return redirect('/dashboard')
@app.route('/dashboard')
def dashboard():
if 'access_token' not in session:
return redirect(url_for('login'))
# Fetch user info
user_response = requests.get(
CONFIG['userinfo_endpoint'],
headers={'Authorization': f"Bearer {session['access_token']}"}
)
if user_response.status_code != 200:
return redirect(url_for('login'))
user = user_response.json()
return f"<h1>Welcome {user.get('name', 'User')}</h1><p>Email: {user.get('email')}</p>"
@app.route('/auth/logout')
def logout():
id_token = session.get('id_token')
session.clear()
logout_url = (
f"https://auth.example.com/oauth/logout?"
f"id_token_hint={id_token}&"
f"post_logout_redirect_uri=https://myapp.example.com"
)
return redirect(logout_url)
if __name__ == '__main__':
app.run(debug=True, port=3000)React / SPA
typescript
// src/auth/AuthProvider.tsx
import React, { createContext, useContext, useState, useEffect } from 'react'
import axios from 'axios'
const CONFIG = {
clientId: 'myapp.example.com',
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo',
redirectUri: 'https://myapp.example.com/auth/callback',
scopes: ['openid', 'profile', 'email', 'uperm://myapp.example.com/**']
}
interface AuthContextType {
isAuthenticated: boolean
user: any
login: () => void
logout: () => void
getAccessToken: () => string | null
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [user, setUser] = useState(null)
useEffect(() => {
const token = localStorage.getItem('access_token')
if (token) {
fetchUserInfo(token)
}
}, [])
async function fetchUserInfo(token: string) {
try {
const response = await axios.get(CONFIG.userInfoEndpoint, {
headers: { Authorization: `Bearer ${token}` }
})
setUser(response.data)
setIsAuthenticated(true)
} catch (error) {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
setIsAuthenticated(false)
}
}
function generatePKCE() {
const randomBytes = new Uint8Array(32)
crypto.getRandomValues(randomBytes)
const verifier = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
const encoder = new TextEncoder()
const data = encoder.encode(verifier)
return crypto.subtle.digest('SHA-256', data).then((hash) => {
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
return { verifier, challenge }
})
}
async function login() {
const { verifier, challenge } = await generatePKCE()
const state = Array.from(crypto.getRandomValues(new Uint8Array(16)))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
localStorage.setItem('pkce_verifier', verifier)
localStorage.setItem('oauth_state', state)
const params = new URLSearchParams({
client_id: CONFIG.clientId,
response_type: 'code',
redirect_uri: CONFIG.redirectUri,
scope: CONFIG.scopes.join(' '),
state,
code_challenge: challenge,
code_challenge_method: 'S256'
})
window.location.href = `${CONFIG.authorizationEndpoint}?${params}`
}
function logout() {
const idToken = localStorage.getItem('id_token')
localStorage.removeItem('access_token')
localStorage.removeItem('id_token')
localStorage.removeItem('refresh_token')
setIsAuthenticated(false)
setUser(null)
window.location.href = (
`https://auth.example.com/oauth/logout?` +
`id_token_hint=${idToken}&` +
`post_logout_redirect_uri=${window.location.origin}`
)
}
function getAccessToken() {
return localStorage.getItem('access_token')
}
return (
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, getAccessToken }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
// src/auth/Callback.tsx
export function AuthCallback() {
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const state = params.get('state')
if (state !== localStorage.getItem('oauth_state')) {
console.error('Invalid state')
return
}
exchangeCodeForTokens(code!)
}, [])
async function exchangeCodeForTokens(code: string) {
try {
const response = await axios.post(
CONFIG.tokenEndpoint,
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: CONFIG.redirectUri,
client_id: CONFIG.clientId,
code_verifier: localStorage.getItem('pkce_verifier')!
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
)
const { access_token, id_token, refresh_token } = response.data
localStorage.setItem('access_token', access_token)
localStorage.setItem('id_token', id_token)
if (refresh_token) localStorage.setItem('refresh_token', refresh_token)
localStorage.removeItem('pkce_verifier')
localStorage.removeItem('oauth_state')
window.location.href = '/'
} catch (error) {
console.error('Token exchange failed', error)
}
}
return <div>Completing authentication...</div>
}Token Refresh
javascript
async function refreshAccessToken(refreshToken) {
const response = await axios.post(
'https://auth.example.com/oauth/token',
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'myapp.example.com'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
)
return response.data
}Scopes
OpenID Connect Scopes
openid: Required for OIDC, includes ID tokenprofile: Basic profile (name, username)email: Email addressphone: Phone number
Permission Scopes
uperm://myapp.example.com/**: All app permissionsuperm://myapp.example.com/api/read: Specific permissionuperm+optional://myapp.example.com/admin: Optional permission
Upgradability
upgradable: Session token (can upgrade security level)- Default: App token (fixed security level)
Next Steps
- PreAuth Integration: Silent authentication for external identity
- PreferType Integration: Specify preferred credential type