@owlmeans/server-oidc-rp
v0.1.1
Published
The **@owlmeans/server-oidc-rp** package provides server-side OpenID Connect Relying Party (RP) functionality for OwlMeans Common Libraries, enabling secure authentication and authorization through OIDC protocols in fullstack applications.
Downloads
143
Readme
@owlmeans/server-oidc-rp
The @owlmeans/server-oidc-rp package provides server-side OpenID Connect Relying Party (RP) functionality for OwlMeans Common Libraries, enabling secure authentication and authorization through OIDC protocols in fullstack applications.
Purpose
This package serves as a comprehensive OIDC client implementation for server-side applications, specifically designed for:
- OIDC Client Management: Create and manage OpenID Connect clients with automatic discovery
- Token Operations: Handle authorization code grants, client credentials, token refresh, and introspection
- Provider Integration: Connect to multiple OIDC providers with flexible configuration
- Account Linking: Link external OIDC accounts with internal user profiles
- Security Integration: Integrate with OwlMeans authentication and authorization system
- Module-based Authentication: Provide OIDC guards and gates for route protection
Key Concepts
OpenID Connect Relying Party
A Relying Party (RP) is an OAuth 2.0 client application that uses OpenID Connect to verify the identity of end-users. This package implements server-side RP functionality.
Provider Configuration
The package supports multiple OIDC providers with configuration including discovery URLs, client credentials, and service mappings.
Account Linking
Enables linking external OIDC provider accounts with internal user accounts, supporting both automatic linking and manual credential mapping.
Token Management
Comprehensive token lifecycle management including acquisition, refresh, introspection, and validation.
Installation
npm install @owlmeans/server-oidc-rpAPI Reference
Types
OidcClientService
Main service interface for OIDC client operations.
interface OidcClientService extends InitializedService {
getConfiguration(clientId: string | Partial<OidcProviderConfig>): Promise<OidcClientDescriptor>
getClient(clientId: string | OidcClientDescriptor | Partial<OidcProviderConfig>): Promise<OidcClientAdapter>
getConfig(clientId: string | Partial<OidcProviderConfig>): Promise<OidcProviderConfig | undefined>
getDefault(): string | undefined
registerTemporaryProvider(config: OidcProviderConfig): OidcProviderConfig
unregisterTemporaryProvider(params: Partial<OidcProviderConfig>): void
hasProvider(params: Partial<OidcProviderConfig>): boolean
entityToClientId(params: Partial<OidcProviderConfig>): string
providerApi(): ProviderApiService | null
accountLinking(): AccountLinkingService | null
}OidcClientAdapter
Interface for individual OIDC client instances.
interface OidcClientAdapter {
getMetadata(): ServerMetadata
getClientId(): string
getConfig(): OidcProviderConfig
makeAuthUrl(params: Record<string, string>): string
grantWithCredentials(): Promise<TokenSet>
grantWithCode(currentUrl: string, checks: AuthorizationCodeGrantChecks, params: Record<string, string>): Promise<TokenSet>
refresh(tokenSet: TokenSetParameters | string): Promise<TokenSetParameters>
introspect(tokenSet: TokenSetParameters, type?: string): Promise<IntrospectionResponse>
}AccountLinkingService
Service interface for linking OIDC accounts with internal profiles.
interface AccountLinkingService extends InitializedService {
getLinkedProfile(details: ProviderProfileDetails): Promise<AuthPayload | null>
linkProfile(details: ProviderProfileDetails, meta: AccountMeta): Promise<AuthPayload>
linkCredentials(details: ProviderProfileDetails): Promise<AuthPayload>
getOwnerProfiles(entityId: string): Promise<Profile[]>
getOwnerCredentials(userId: string, entityId?: string, type?: string): Promise<AuthCredentials | undefined>
}ProviderApiService
Service interface for interacting with OIDC provider APIs.
interface ProviderApiService extends InitializedService {
getUserDetails(token: string, userId: string): Promise<OidcUserDetails>
getSettings(token: string, realm: string): Promise<OidcProviderSettings>
}Config
Configuration interface extending ServerConfig.
interface Config extends ServerConfig, WithSharedConfig {
oidc: OidcRpConfig
}
interface OidcRpConfig extends OidcSharedConfig {
accountLinkingService?: string
providerApiService?: string
}Factory Functions
makeOidcClientService(alias?: string): OidcClientService
Creates an OIDC client service instance.
Parameters:
alias: Service alias (defaults to 'oidc-client')
Returns: OidcClientService instance
import { makeOidcClientService } from '@owlmeans/server-oidc-rp'
const oidcService = makeOidcClientService('my-oidc-client')Guard Functions
appendOidcGuard<C extends Config, T extends Context<C>>(context: T, opts?: OidcGuardOptions): T
Appends OIDC guard functionality to a context.
Parameters:
context: Server context instanceopts: Optional OIDC guard configuration
Returns: Enhanced context with OIDC guard capabilities
import { appendOidcGuard } from '@owlmeans/server-oidc-rp'
const enhancedContext = appendOidcGuard(context, {
defaultProvider: 'keycloak'
})setupOidcGuard(modules: CommonModule[], coguards?: string | string[]): void
Sets up OIDC guard functionality for modules.
Parameters:
modules: Array of modules to enhancecoguards: Optional co-guard configurations
import { setupOidcGuard } from '@owlmeans/server-oidc-rp'
setupOidcGuard(modules, ['authenticated', 'oidc-verified'])Module Setup
setupAuthServiceModules(modules: CommonModule[], serviceAlias: string, prefix?: string): void
Sets up authentication service modules with OIDC routes.
Parameters:
modules: Array of modules to add OIDC routes toserviceAlias: Service alias for the routesprefix: URL prefix for OIDC routes (defaults to 'oidc-api')
import { setupAuthServiceModules } from '@owlmeans/server-oidc-rp'
setupAuthServiceModules(modules, 'auth-service', 'oidc-api')Constants
Service Aliases
const DEFAULT_ALIAS = 'oidc-client'
const DEF_OIDC_ACCOUNT_LINKING = 'oidc-consumer-account-linking'
const DEF_OIDC_PROVIDER_API = 'oidc-consumer-provider-api'Route Definitions
const authService = {
provider: {
list: 'external-auth:provider:list'
},
auth: {
update: 'external-auth:auth:update'
}
}Cache and Timing Constants
const OIDC_TOKEN_STORE = 'oidc-token-store'
const PROVIDER_CACHE_TTL = (5 * 60 - 1) * 1000 // ~5 minutes
const OIDC_AUTH_LIFTETIME = 24 * 3600 * 1000 // 24 hours
const OIDC_WRAP_FRESHNESS = (5 * 60 - 1) * 1000 // ~5 minutesUsage Examples
Basic OIDC Client Setup
import { makeOidcClientService, setupOidcGuard } from '@owlmeans/server-oidc-rp'
import { makeServerContext, makeServerConfig } from '@owlmeans/server-context'
// Create server context with OIDC configuration
const config = makeServerConfig('auth-server', {
oidc: {
providers: [{
clientId: 'my-app',
secret: 'client-secret',
service: 'keycloak',
basePath: '/realms/my-realm',
def: true
}]
}
})
const context = makeServerContext(config)
// Create and register OIDC client service
const oidcService = makeOidcClientService()
context.registerService(oidcService)
// Initialize context
await context.configure().init()Using OIDC Client
// Get the OIDC service
const oidcService = context.service<OidcClientService>('oidc-client')
// Get OIDC client for default provider
const defaultClientId = oidcService.getDefault()
const client = await oidcService.getClient(defaultClientId!)
// Create authorization URL
const authUrl = client.makeAuthUrl({
redirect_uri: 'https://myapp.com/callback',
scope: 'openid profile email',
state: 'random-state'
})
console.log('Redirect user to:', authUrl)Authorization Code Flow
// Handle callback from OIDC provider
app.get('/callback', async (req, res) => {
const client = await oidcService.getClient('my-app')
try {
const tokenSet = await client.grantWithCode(
req.url,
{
state: req.query.state,
code_verifier: 'stored-code-verifier' // if using PKCE
},
{ flow: 'authorization_code' }
)
console.log('Access token:', tokenSet.access_token)
console.log('ID token:', tokenSet.id_token)
// Store tokens securely
res.redirect('/dashboard')
} catch (error) {
console.error('OIDC authentication failed:', error)
res.redirect('/login?error=auth_failed')
}
})Client Credentials Flow
// Get access token using client credentials
const client = await oidcService.getClient('service-account')
const tokenSet = await client.grantWithCredentials()
console.log('Service access token:', tokenSet.access_token)
// Use token for API calls
const response = await fetch('https://api.service.com/data', {
headers: {
'Authorization': `Bearer ${tokenSet.access_token}`
}
})Token Refresh
// Refresh an expired token
try {
const newTokenSet = await client.refresh(oldTokenSet)
console.log('New access token:', newTokenSet.access_token)
} catch (error) {
console.error('Token refresh failed:', error)
// Redirect to login
}Token Introspection
// Validate and introspect a token
const introspection = await client.introspect(tokenSet, 'access_token')
if (introspection.active) {
console.log('Token is valid')
console.log('Token expires at:', new Date(introspection.exp! * 1000))
console.log('Token subject:', introspection.sub)
} else {
console.log('Token is invalid or expired')
}Multiple Provider Configuration
const config = makeServerConfig('multi-provider-app', {
oidc: {
providers: [
{
clientId: 'keycloak-client',
secret: 'secret1',
service: 'keycloak',
basePath: '/realms/main',
def: true
},
{
clientId: 'google-client',
secret: 'secret2',
discoveryUrl: 'https://accounts.google.com/.well-known/openid_configuration'
}
]
}
})
// Get specific provider clients
const keycloakClient = await oidcService.getClient('keycloak-client')
const googleClient = await oidcService.getClient('google-client')Account Linking
// Setup account linking service
const accountLinkingService: AccountLinkingService = createService('account-linking', {
getLinkedProfile: async (details) => {
// Check if external profile is already linked
const existing = await database.findLinkedAccount(details.sub, details.iss)
return existing ? existing.authPayload : null
},
linkProfile: async (details, meta) => {
// Link external account to existing user
const user = await database.findUserByUsername(meta.username)
await database.linkExternalAccount(user.id, details)
return createAuthPayload(user)
},
linkCredentials: async (details) => {
// Auto-link based on email or other criteria
const user = await database.findUserByEmail(details.email)
if (user) {
await database.linkExternalAccount(user.id, details)
return createAuthPayload(user)
}
throw new Error('No matching user found')
}
})
context.registerService(accountLinkingService)Module Protection with OIDC Guards
import { module, guard } from '@owlmeans/module'
import { route } from '@owlmeans/route'
// Create protected modules
const protectedModules = [
module(route('profile', '/profile'), guard('oidc-authenticated')),
module(route('admin', '/admin'), guard(['oidc-authenticated', 'admin-role']))
]
// Setup OIDC guards
setupOidcGuard(protectedModules, 'oidc-authenticated')Error Handling
The package throws AuthManagerError for various OIDC-related issues:
import { AuthManagerError } from '@owlmeans/auth'
try {
const client = await oidcService.getClient('invalid-client')
} catch (error) {
if (error instanceof AuthManagerError) {
switch (error.message) {
case 'oidc.client.basepath':
console.error('OIDC provider base path not configured')
break
case 'oidc.client.service':
console.error('OIDC provider service not specified')
break
case 'oidc.client.client-id':
console.error('OIDC client ID missing')
break
case 'oidc.client.secert':
console.error('OIDC client secret missing')
break
default:
console.error('OIDC error:', error.message)
}
}
}Integration Patterns
Express.js Integration
import express from 'express'
import { makeOidcClientService } from '@owlmeans/server-oidc-rp'
const app = express()
// OIDC login route
app.get('/login/:provider?', async (req, res) => {
const providerId = req.params.provider || oidcService.getDefault()
const client = await oidcService.getClient(providerId!)
const authUrl = client.makeAuthUrl({
redirect_uri: `${req.protocol}://${req.get('host')}/callback`,
scope: 'openid profile email',
state: generateState()
})
res.redirect(authUrl)
})
// OIDC callback route
app.get('/callback', async (req, res) => {
// Handle authorization code flow as shown above
})API Gateway Integration
// Create API gateway with OIDC token validation
const validateToken = async (req: Request) => {
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
throw new Error('Missing or invalid authorization header')
}
const token = authHeader.substring(7)
const client = await oidcService.getClient(oidcService.getDefault()!)
const introspection = await client.introspect({ access_token: token })
if (!introspection.active) {
throw new Error('Invalid or expired token')
}
return introspection
}Best Practices
- Secure Configuration: Store client secrets securely and never expose them in client-side code
- Token Storage: Store tokens securely with appropriate encryption and expiration handling
- Error Handling: Implement comprehensive error handling for all OIDC operations
- Provider Discovery: Use OIDC discovery when possible for automatic configuration
- Token Validation: Always validate tokens before using them for authorization decisions
- Account Linking: Implement secure account linking with proper user consent flows
- Monitoring: Monitor OIDC operations for security and performance issues
Dependencies
This package depends on:
@owlmeans/auth- Authentication framework@owlmeans/auth-common- Common authentication utilities@owlmeans/context- Context management@owlmeans/oidc- Core OIDC functionality@owlmeans/server-context- Server context management@owlmeans/server-module- Server module systemopenid-client- OpenID Connect client libraryjose- JSON Web Token utilities
Related Packages
@owlmeans/web-oidc-rp- Web client OIDC Relying Party@owlmeans/oidc- Core OIDC functionality@owlmeans/server-auth- Server authentication framework@owlmeans/auth- Authentication foundation
