mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
cleanup multi provider
This commit is contained in:
parent
0e8e7df8d5
commit
d1fb24898c
75
.env.example
75
.env.example
@ -1,35 +1,52 @@
|
||||
### OBP-API Configuration ###
|
||||
VITE_OBP_API_PORTAL_HOST=http://127.0.0.1:8080 # OBP API Portal URL (for "Portal Home" navigation link)
|
||||
VITE_OBP_API_HOST=http://127.0.0.1:8080 # OBP API server base URL (for all backend API requests)
|
||||
# VITE_OBP_API_VERSION is NO LONGER USED - hardcoded to v5.1.0 in shared-constants.ts for stability
|
||||
VITE_OBP_API_MANAGER_HOST=https://apimanagersandbox.openbankproject.com # OBP API Manager URL (optional - for navigation link)
|
||||
VITE_OBP_API_EXPLORER_HOST=http://localhost:5173 # API Explorer application URL (used for OAuth2 redirects and internal routing)
|
||||
VITE_OPB_SERVER_SESSION_PASSWORD=your-secret-session-password-here # Secret key for session encryption (keep this secure!)
|
||||
VITE_SHOW_API_MANAGER_BUTTON=false # Show/hide API Manager button in navigation (true/false)
|
||||
### OBP API Configuration ###
|
||||
VITE_OBP_API_HOST=http://127.0.0.1:8080
|
||||
VITE_OBP_API_VERSION=v5.1.0
|
||||
|
||||
### Redis Configuration ###
|
||||
VITE_OBP_REDIS_URL=redis://127.0.0.1:6379 # Redis connection string for session storage (format: redis://host:port)
|
||||
### API Explorer Host ###
|
||||
VITE_OBP_API_EXPLORER_HOST=http://localhost:5173
|
||||
|
||||
### Opey Configuration ###
|
||||
VITE_CHATBOT_ENABLED=false # Enable/disable Opey chatbot widget (true/false)
|
||||
VITE_CHATBOT_URL=http://localhost:5000 # Opey chatbot service URL (only needed if chatbot is enabled)
|
||||
### Session Configuration ###
|
||||
VITE_OPB_SERVER_SESSION_PASSWORD=change-me-to-a-secure-random-string
|
||||
|
||||
### OAuth2/OIDC Configuration ###
|
||||
VITE_OBP_OAUTH2_CLIENT_ID=48ac28e9-9ee3-47fd-8448-69a62764b779 # OAuth2 client ID (UUID - must match OIDC server registration)
|
||||
VITE_OBP_OAUTH2_CLIENT_SECRET=fOTQF7jfg8C74u7ZhSjVQpoBYvD0KpWfM5UsEZBSFFM # OAuth2 client secret (keep this secure!)
|
||||
VITE_OBP_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback # OAuth2 callback URL (must exactly match OIDC client registration)
|
||||
VITE_OBP_OAUTH2_WELL_KNOWN_URL=http://localhost:9000/obp-oidc/.well-known/openid-configuration # OIDC discovery endpoint URL
|
||||
VITE_OBP_OAUTH2_TOKEN_REFRESH_THRESHOLD=300 # Seconds before token expiry to trigger refresh (default: 300)
|
||||
### OAuth2 Redirect URL (shared by all providers) ###
|
||||
VITE_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
|
||||
### Resource Documentation Version (Optional) ###
|
||||
# VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION=OBPv5.1.0 # Default resource docs version for frontend URLs (format: OBPv5.1.0 - with OBP prefix, auto-constructed if not set)
|
||||
### Redis Configuration (Optional - uses localhost:6379 if not set) ###
|
||||
# VITE_OBP_REDIS_URL=redis://127.0.0.1:6379
|
||||
# VITE_OBP_REDIS_PASSWORD=
|
||||
# VITE_OBP_REDIS_USERNAME=
|
||||
|
||||
### Session Configuration (Optional) ###
|
||||
# VITE_SESSION_MAX_AGE=3600 # Session timeout in seconds (default: 3600 = 1 hour)
|
||||
### Multi-Provider OAuth2/OIDC Configuration ###
|
||||
### The system fetches available providers from: http://localhost:8080/obp/v5.1.0/well-known
|
||||
### Configure credentials below for each provider you want to support
|
||||
|
||||
### Styling Configuration (Optional) ###
|
||||
# VITE_OBP_LOGO_URL=https://example.com/logo.png # Custom logo image URL (uses default OBP logo if not set)
|
||||
# VITE_OBP_LINKS_COLOR=#3c8dbc # Primary link color (CSS color value)
|
||||
# VITE_OBP_HEADER_LINKS_COLOR=#39455f # Header navigation link color
|
||||
# VITE_OBP_HEADER_LINKS_HOVER_COLOR=#39455f # Header navigation link hover color
|
||||
# VITE_OBP_HEADER_LINKS_BACKGROUND_COLOR=#eef0f4 # Header navigation active link background color
|
||||
### OBP-OIDC Provider ###
|
||||
VITE_OBP_OIDC_CLIENT_ID=your-obp-oidc-client-id
|
||||
VITE_OBP_OIDC_CLIENT_SECRET=your-obp-oidc-client-secret
|
||||
|
||||
### OBP Consumer Key (for API calls) ###
|
||||
VITE_OBP_CONSUMER_KEY=your-obp-oidc-client-id
|
||||
|
||||
### Keycloak Provider (Optional) ###
|
||||
# VITE_KEYCLOAK_CLIENT_ID=your-keycloak-client-id
|
||||
# VITE_KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret
|
||||
|
||||
### Google Provider (Optional) ###
|
||||
# VITE_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
|
||||
# VITE_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
### GitHub Provider (Optional) ###
|
||||
# VITE_GITHUB_CLIENT_ID=your-github-client-id
|
||||
# VITE_GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
|
||||
### Custom OIDC Provider (Optional) ###
|
||||
# VITE_CUSTOM_OIDC_PROVIDER_NAME=my-custom-provider
|
||||
# VITE_CUSTOM_OIDC_CLIENT_ID=your-custom-client-id
|
||||
# VITE_CUSTOM_OIDC_CLIENT_SECRET=your-custom-client-secret
|
||||
|
||||
### Chatbot Configuration (Optional) ###
|
||||
VITE_CHATBOT_ENABLED=false
|
||||
# VITE_CHATBOT_URL=http://localhost:5000
|
||||
|
||||
### Resource Docs Version ###
|
||||
VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION=OBPv6.0.0
|
||||
|
||||
67
env_ai
67
env_ai
@ -1,26 +1,63 @@
|
||||
### OBP-API Configuration ###
|
||||
VITE_OBP_API_PORTAL_HOST=http://127.0.0.1:8080
|
||||
### OBP API Configuration ###
|
||||
VITE_OBP_API_HOST=http://127.0.0.1:8080
|
||||
VITE_OBP_API_VERSION=v5.1.0
|
||||
VITE_OBP_API_MANAGER_HOST=https://apimanagersandbox.openbankproject.com
|
||||
VITE_OBP_API_EXPLORER_HOST=http://localhost:5174
|
||||
VITE_OBP_API_VERSION=v6.0.0
|
||||
VITE_OBP_API_EXPLORER_HOST=http://localhost:5173
|
||||
|
||||
### Session Configuration ###
|
||||
VITE_OPB_SERVER_SESSION_PASSWORD=asidudhiuh33875
|
||||
|
||||
### OAuth2 Redirect URL (shared by all providers) ###
|
||||
VITE_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
|
||||
### Redis Configuration ###
|
||||
VITE_OBP_REDIS_URL=redis://127.0.0.1:6379
|
||||
|
||||
### Opey Configuration ###
|
||||
### Chatbot Configuration ###
|
||||
VITE_CHATBOT_ENABLED=false
|
||||
VITE_CHATBOT_URL=http://localhost:5000
|
||||
|
||||
### OAuth2/OIDC Configuration ###
|
||||
# OAuth2 Client Credentials (from OBP-OIDC)
|
||||
VITE_OBP_OAUTH2_CLIENT_ID=48ac28e9-9ee3-47fd-8448-69a62764b779
|
||||
VITE_OBP_OAUTH2_CLIENT_SECRET=fOTQF7jfg8C74u7ZhSjVQpoBYvD0KpWfM5UsEZBSFFM
|
||||
VITE_OBP_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
### Multi-Provider OAuth2/OIDC Configuration ###
|
||||
### The system fetches available providers from: http://localhost:8080/obp/v5.1.0/well-known
|
||||
### Configure credentials below for each provider you want to support
|
||||
|
||||
# OIDC Well-Known Configuration URL
|
||||
VITE_OBP_OAUTH2_WELL_KNOWN_URL=http://127.0.0.1:9000/obp-oidc/.well-known/openid-configuration
|
||||
### OBP-OIDC Provider ###
|
||||
VITE_OBP_OIDC_CLIENT_ID=c2ea173e-8c1a-43c4-ba62-19738f27c43e
|
||||
VITE_OBP_OIDC_CLIENT_SECRET=1E7zsN47Xp4VTb28xEv5ZK4vcX8XMsYIH3IsnjQTYk8
|
||||
|
||||
# Optional: Token refresh threshold (seconds before expiry)
|
||||
VITE_OBP_OAUTH2_TOKEN_REFRESH_THRESHOLD=300
|
||||
### OBP Consumer Key (for API calls) ###
|
||||
VITE_OBP_CONSUMER_KEY=c2ea173e-8c1a-43c4-ba62-19738f27c43e
|
||||
|
||||
### Keycloak Provider (Optional) ###
|
||||
# VITE_KEYCLOAK_CLIENT_ID=obp-api-explorer
|
||||
# VITE_KEYCLOAK_CLIENT_SECRET=your-keycloak-secret-here
|
||||
|
||||
### Google Provider (Optional) ###
|
||||
# VITE_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
|
||||
# VITE_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
### GitHub Provider (Optional) ###
|
||||
# VITE_GITHUB_CLIENT_ID=your-github-client-id
|
||||
# VITE_GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
|
||||
### Custom OIDC Provider (Optional) ###
|
||||
# VITE_CUSTOM_OIDC_PROVIDER_NAME=my-custom-provider
|
||||
# VITE_CUSTOM_OIDC_CLIENT_ID=your-custom-client-id
|
||||
# VITE_CUSTOM_OIDC_CLIENT_SECRET=your-custom-client-secret
|
||||
|
||||
### Opey Configuration ###
|
||||
VITE_OPEY_CONSUMER_ID=74545fb7-9a1f-4ee0-beb4-6e5b7ee50076
|
||||
|
||||
### Resource Docs Version ###
|
||||
VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION=OBPv6.0.0
|
||||
|
||||
### HOW IT WORKS ###
|
||||
# 1. Backend fetches provider list from OBP API: GET /obp/v5.1.0/well-known
|
||||
# 2. OBP API returns available providers with their .well-known URLs
|
||||
# 3. Backend matches providers with credentials configured above
|
||||
# 4. Only providers with both (API registration + credentials) will be available
|
||||
# 5. Users see provider selection if 2+ providers configured (or auto-login if only 1)
|
||||
|
||||
### VERIFY YOUR SETUP ###
|
||||
# curl http://localhost:8080/obp/v5.1.0/well-known
|
||||
# curl http://localhost:8085/api/oauth2/providers
|
||||
# Visit: http://localhost:5173/debug/providers-status
|
||||
|
||||
@ -35,7 +35,6 @@ import type { Application } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import path from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import { OAuth2Service } from './services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from './services/OAuth2ProviderManager.js'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
@ -137,9 +136,7 @@ if (app.get('env') === 'production') {
|
||||
}
|
||||
app.use(session(sessionObject))
|
||||
|
||||
// Initialize OAuth2 Service
|
||||
console.log(`--- OAuth2/OIDC setup -------------------------------------------`)
|
||||
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
|
||||
// OAuth2 Multi-Provider Setup only - no legacy fallback
|
||||
|
||||
// Async IIFE to initialize OAuth2 and start server
|
||||
let instance: any
|
||||
@ -160,61 +157,15 @@ let instance: any
|
||||
providerManager.startHealthCheck(60000) // Check every 60 seconds
|
||||
console.log('OK Provider health monitoring started (every 60s)')
|
||||
} else {
|
||||
console.warn('WARNING No OAuth2 providers initialized from OBP API')
|
||||
console.warn('WARNING Falling back to legacy single-provider mode...')
|
||||
console.error('ERROR: No OAuth2 providers initialized from OBP API')
|
||||
console.error(
|
||||
'ERROR: Check that OBP API is running and returns providers from /obp/v5.1.0/well-known'
|
||||
)
|
||||
console.error('ERROR: Server will start but login will not work')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ERROR Failed to initialize OAuth2 multi-provider:', error)
|
||||
console.warn('WARNING Falling back to legacy single-provider mode...')
|
||||
}
|
||||
console.log(`-----------------------------------------------------------------`)
|
||||
|
||||
// Initialize Legacy OAuth2 Service (for backward compatibility)
|
||||
console.log(`--- OAuth2/OIDC Legacy Setup (Backward Compatibility) -----------`)
|
||||
if (!wellKnownUrl) {
|
||||
console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. Legacy OAuth2 will not function.')
|
||||
console.warn('Server will rely on multi-provider mode from OBP API.')
|
||||
} else {
|
||||
console.log(`OIDC Well-Known URL (legacy): ${wellKnownUrl}`)
|
||||
|
||||
// Get OAuth2Service from container
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
// Initialize OAuth2 service with retry logic
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const maxRetries = Infinity // Retry indefinitely
|
||||
const initialDelay = 1000 // 1 second, then exponential backoff
|
||||
|
||||
console.log(
|
||||
'Attempting legacy OAuth2 initialization (will retry indefinitely with exponential backoff)...'
|
||||
)
|
||||
const success = await oauth2Service.initializeWithRetry(wellKnownUrl, maxRetries, initialDelay)
|
||||
|
||||
if (success) {
|
||||
console.log('OAuth2Service (legacy): Initialization successful')
|
||||
console.log(' Client ID:', process.env.VITE_OBP_OAUTH2_CLIENT_ID || 'NOT SET')
|
||||
console.log(' Redirect URI:', process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'NOT SET')
|
||||
console.log('Legacy OAuth2/OIDC ready for authentication')
|
||||
|
||||
// Start continuous monitoring even when initially connected
|
||||
oauth2Service.startHealthCheck(1000, 240000) // Monitor every 4 minutes
|
||||
console.log('OAuth2Service (legacy): Starting continuous monitoring (every 4 minutes)')
|
||||
} else {
|
||||
console.error('OAuth2Service (legacy): Initialization failed after all retries')
|
||||
|
||||
// Use graceful degradation for both development and production
|
||||
const envMode = isProduction ? 'Production' : 'Development'
|
||||
console.warn(`WARNING: ${envMode} mode: Server will start without legacy OAuth2`)
|
||||
console.warn('WARNING: Legacy login will be unavailable until OIDC server is reachable')
|
||||
console.warn('WARNING: Multi-provider mode will be used if available')
|
||||
console.warn('Please check:')
|
||||
console.warn(' 1. OBP-OIDC server is running')
|
||||
console.warn(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
|
||||
console.warn(' 3. Network connectivity to OIDC provider')
|
||||
|
||||
// Start periodic health check to reconnect when OIDC becomes available
|
||||
oauth2Service.startHealthCheck(1000, 240000) // Start with 1 second, monitor every 4 minutes when connected
|
||||
}
|
||||
console.error('ERROR: Server will start but login will not work')
|
||||
}
|
||||
console.log(`-----------------------------------------------------------------`)
|
||||
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
|
||||
import { PKCEUtils } from '../utils/pkce.js'
|
||||
import type { UserInfo } from '../types/oauth2.js'
|
||||
@ -37,7 +36,6 @@ const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const providerManager = Container.get(OAuth2ProviderManager)
|
||||
const legacyOAuth2Service = Container.get(OAuth2Service)
|
||||
|
||||
/**
|
||||
* GET /oauth2/providers
|
||||
@ -66,7 +64,7 @@ router.get('/oauth2/providers', async (req: Request, res: Response) => {
|
||||
* GET /oauth2/connect
|
||||
* Initiate OAuth2 authentication flow
|
||||
* Query params:
|
||||
* - provider: Provider name (optional, uses legacy if not specified)
|
||||
* - provider: Provider name (required)
|
||||
* - redirect: URL to redirect after auth (optional)
|
||||
*/
|
||||
router.get('/oauth2/connect', async (req: Request, res: Response) => {
|
||||
@ -76,9 +74,18 @@ router.get('/oauth2/connect', async (req: Request, res: Response) => {
|
||||
const session = req.session as any
|
||||
|
||||
console.log('OAuth2 Connect: Starting authentication flow')
|
||||
console.log(` Provider: ${provider || '(legacy mode)'}`)
|
||||
console.log(` Provider: ${provider || 'NOT SPECIFIED'}`)
|
||||
console.log(` Redirect: ${redirect}`)
|
||||
|
||||
// Provider is required
|
||||
if (!provider) {
|
||||
console.error('OAuth2 Connect: No provider specified')
|
||||
return res.status(400).json({
|
||||
error: 'missing_provider',
|
||||
message: 'Provider parameter is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Store redirect URL in session
|
||||
session.oauth2_redirect_page = redirect
|
||||
|
||||
@ -91,11 +98,7 @@ router.get('/oauth2/connect', async (req: Request, res: Response) => {
|
||||
session.oauth2_code_verifier = codeVerifier
|
||||
session.oauth2_state = state
|
||||
|
||||
let authUrl: string
|
||||
|
||||
if (provider) {
|
||||
// Multi-provider mode
|
||||
console.log(`OAuth2 Connect: Using multi-provider mode - ${provider}`)
|
||||
console.log(`OAuth2 Connect: Using provider - ${provider}`)
|
||||
|
||||
const client = providerManager.getProvider(provider)
|
||||
if (!client) {
|
||||
@ -123,23 +126,7 @@ router.get('/oauth2/connect', async (req: Request, res: Response) => {
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
|
||||
authUrl = `${authEndpoint}?${params.toString()}`
|
||||
} else {
|
||||
// Legacy single-provider mode
|
||||
console.log('OAuth2 Connect: Using legacy single-provider mode')
|
||||
|
||||
if (!legacyOAuth2Service.isInitialized()) {
|
||||
console.error('OAuth2 Connect: OAuth2 service not initialized')
|
||||
return res.status(503).json({
|
||||
error: 'oauth2_unavailable',
|
||||
message: 'OAuth2 authentication is not available'
|
||||
})
|
||||
}
|
||||
|
||||
authUrl = legacyOAuth2Service
|
||||
.createAuthorizationURL(state, ['openid', 'profile', 'email'])
|
||||
.toString()
|
||||
}
|
||||
const authUrl = `${authEndpoint}?${params.toString()}`
|
||||
|
||||
// Save session before redirect
|
||||
session.save((err: any) => {
|
||||
@ -211,15 +198,15 @@ router.get('/oauth2/callback', async (req: Request, res: Response) => {
|
||||
return res.redirect('/?oauth2_error=missing_verifier')
|
||||
}
|
||||
|
||||
// Check if multi-provider mode
|
||||
// Get provider from session
|
||||
const provider = session.oauth2_provider
|
||||
|
||||
let tokens: any
|
||||
let userInfo: UserInfo
|
||||
if (!provider) {
|
||||
console.error('OAuth2 Callback: Provider not found in session')
|
||||
return res.redirect('/?oauth2_error=missing_provider')
|
||||
}
|
||||
|
||||
if (provider) {
|
||||
// Multi-provider mode
|
||||
console.log(`OAuth2 Callback: Multi-provider mode - ${provider}`)
|
||||
console.log(`OAuth2 Callback: Processing callback for ${provider}`)
|
||||
|
||||
const client = providerManager.getProvider(provider)
|
||||
if (!client) {
|
||||
@ -229,7 +216,7 @@ router.get('/oauth2/callback', async (req: Request, res: Response) => {
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log('OAuth2 Callback: Exchanging authorization code for tokens')
|
||||
tokens = await client.exchangeAuthorizationCode(code, codeVerifier)
|
||||
const tokens = await client.exchangeAuthorizationCode(code, codeVerifier)
|
||||
|
||||
// Fetch user info
|
||||
console.log('OAuth2 Callback: Fetching user info')
|
||||
@ -245,20 +232,7 @@ router.get('/oauth2/callback', async (req: Request, res: Response) => {
|
||||
throw new Error(`UserInfo request failed: ${userInfoResponse.status}`)
|
||||
}
|
||||
|
||||
userInfo = (await userInfoResponse.json()) as UserInfo
|
||||
|
||||
// Store provider in session
|
||||
session.oauth2_provider = provider
|
||||
} else {
|
||||
// Legacy single-provider mode
|
||||
console.log('OAuth2 Callback: Legacy single-provider mode')
|
||||
|
||||
// Exchange code for tokens
|
||||
tokens = await legacyOAuth2Service.exchangeCodeForTokens(code, codeVerifier)
|
||||
|
||||
// Fetch user info
|
||||
userInfo = await legacyOAuth2Service.getUserInfo(tokens.accessToken)
|
||||
}
|
||||
const userInfo = (await userInfoResponse.json()) as UserInfo
|
||||
|
||||
// Store tokens in session
|
||||
session.oauth2_access_token = tokens.accessToken
|
||||
|
||||
@ -29,62 +29,18 @@ import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
/**
|
||||
* Check if access token is expired and refresh it if needed
|
||||
* This ensures API calls always use a valid token
|
||||
* Check if user is authenticated
|
||||
* TODO: Implement token refresh in multi-provider system
|
||||
*/
|
||||
async function ensureValidToken(session: any): Promise<boolean> {
|
||||
const accessToken = session.oauth2_access_token
|
||||
const refreshToken = session.oauth2_refresh_token
|
||||
|
||||
// If no access token, user is not authenticated
|
||||
if (!accessToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (oauth2Service.isTokenExpired(accessToken)) {
|
||||
console.log('OBP: Access token expired, attempting refresh')
|
||||
|
||||
if (!refreshToken) {
|
||||
console.log('OBP: No refresh token available')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const newTokens = await oauth2Service.refreshAccessToken(refreshToken)
|
||||
|
||||
// Update session with new tokens
|
||||
session.oauth2_access_token = newTokens.accessToken
|
||||
session.oauth2_refresh_token = newTokens.refreshToken || refreshToken
|
||||
session.oauth2_id_token = newTokens.idToken
|
||||
session.oauth2_token_timestamp = Date.now()
|
||||
session.oauth2_expires_in = newTokens.expiresIn
|
||||
|
||||
// Update clientConfig with new access token
|
||||
if (session.clientConfig && session.clientConfig.oauth2) {
|
||||
session.clientConfig.oauth2.accessToken = newTokens.accessToken
|
||||
console.log('OBP: Updated clientConfig with refreshed token')
|
||||
}
|
||||
|
||||
console.log('OBP: Token refresh successful')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('OBP: Token refresh failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Token is still valid
|
||||
return true
|
||||
function isAuthenticated(session: any): boolean {
|
||||
return !!session.oauth2_access_token && !!session.oauth2_user
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,16 +54,6 @@ router.get('/get', async (req: Request, res: Response) => {
|
||||
const path = req.query.path as string
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.get(path, oauthConfig)
|
||||
@ -139,16 +85,6 @@ router.post('/create', async (req: Request, res: Response) => {
|
||||
const data = req.body
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
// Debug logging to diagnose authentication issues
|
||||
@ -184,16 +120,6 @@ router.put('/update', async (req: Request, res: Response) => {
|
||||
const data = req.body
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.update(path, data, oauthConfig)
|
||||
@ -218,16 +144,6 @@ router.delete('/delete', async (req: Request, res: Response) => {
|
||||
const path = req.query.path as string
|
||||
const session = req.session as any
|
||||
|
||||
// Ensure token is valid before making the request
|
||||
const tokenValid = await ensureValidToken(session)
|
||||
if (!tokenValid && session.oauth2_user) {
|
||||
console.log('OBP: Token expired and refresh failed')
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Session expired. Please log in again.'
|
||||
})
|
||||
}
|
||||
|
||||
const oauthConfig = session.clientConfig
|
||||
|
||||
const result = await obpClientService.discard(path, oauthConfig)
|
||||
|
||||
@ -29,7 +29,6 @@ import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { OAuth2ProviderManager } from '../services/OAuth2ProviderManager.js'
|
||||
import { commitId } from '../app.js'
|
||||
import {
|
||||
@ -42,7 +41,6 @@ const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
const providerManager = Container.get(OAuth2ProviderManager)
|
||||
|
||||
const connectors = [
|
||||
@ -158,80 +156,6 @@ router.get('/status', async (req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /status/oauth2
|
||||
* Get OAuth2/OIDC status
|
||||
*/
|
||||
router.get('/status/oauth2', (req: Request, res: Response) => {
|
||||
try {
|
||||
const isInitialized = oauth2Service.isInitialized()
|
||||
const oidcConfig = oauth2Service.getOIDCConfiguration()
|
||||
const healthCheckActive = oauth2Service.isHealthCheckActive()
|
||||
const healthCheckAttempts = oauth2Service.getHealthCheckAttempts()
|
||||
|
||||
res.json({
|
||||
available: isInitialized,
|
||||
message: isInitialized
|
||||
? 'OAuth2/OIDC is ready for authentication'
|
||||
: 'OAuth2/OIDC is not available',
|
||||
issuer: oidcConfig?.issuer || null,
|
||||
authorizationEndpoint: oidcConfig?.authorization_endpoint || null,
|
||||
wellKnownUrl: process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL || null,
|
||||
healthCheck: {
|
||||
active: healthCheckActive,
|
||||
attempts: healthCheckAttempts
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
available: false,
|
||||
message: 'Error checking OAuth2 status',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /status/oauth2/reconnect
|
||||
* Attempt to reconnect OAuth2/OIDC
|
||||
*/
|
||||
router.get('/status/oauth2/reconnect', async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (oauth2Service.isInitialized()) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'OAuth2 is already connected',
|
||||
alreadyConnected: true
|
||||
})
|
||||
}
|
||||
|
||||
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
|
||||
if (!wellKnownUrl) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'VITE_OBP_OAUTH2_WELL_KNOWN_URL not configured'
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Manual OAuth2 reconnection attempt triggered...')
|
||||
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
|
||||
|
||||
console.log('Manual OAuth2 reconnection successful!')
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'OAuth2 reconnection successful',
|
||||
issuer: oauth2Service.getOIDCConfiguration()?.issuer || null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Manual OAuth2 reconnection failed:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'OAuth2 reconnection failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /status/providers
|
||||
* Get configured OAuth2 providers (for debugging)
|
||||
@ -251,30 +175,27 @@ router.get('/status/providers', (req: Request, res: Response) => {
|
||||
const availableProviders = providerManager.getAvailableProviders()
|
||||
const allProviderStatus = providerManager.getAllProviderStatus()
|
||||
|
||||
// Shared redirect URL
|
||||
const sharedRedirectUrl = process.env.VITE_OAUTH2_REDIRECT_URL || 'not configured'
|
||||
|
||||
// Get env configuration (masked)
|
||||
const envConfig = {
|
||||
obpOidc: {
|
||||
consumerId: process.env.VITE_OBP_CONSUMER_KEY || 'not configured',
|
||||
clientId: maskCredential(process.env.VITE_OBP_OAUTH2_CLIENT_ID),
|
||||
wellKnownUrl: process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL || 'not configured',
|
||||
redirectUrl: process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'not configured'
|
||||
clientId: maskCredential(process.env.VITE_OBP_OIDC_CLIENT_ID)
|
||||
},
|
||||
keycloak: {
|
||||
clientId: maskCredential(process.env.VITE_KEYCLOAK_CLIENT_ID),
|
||||
redirectUrl: process.env.VITE_KEYCLOAK_REDIRECT_URL || 'not configured'
|
||||
clientId: maskCredential(process.env.VITE_KEYCLOAK_CLIENT_ID)
|
||||
},
|
||||
google: {
|
||||
clientId: maskCredential(process.env.VITE_GOOGLE_CLIENT_ID),
|
||||
redirectUrl: process.env.VITE_GOOGLE_REDIRECT_URL || 'not configured'
|
||||
clientId: maskCredential(process.env.VITE_GOOGLE_CLIENT_ID)
|
||||
},
|
||||
github: {
|
||||
clientId: maskCredential(process.env.VITE_GITHUB_CLIENT_ID),
|
||||
redirectUrl: process.env.VITE_GITHUB_REDIRECT_URL || 'not configured'
|
||||
clientId: maskCredential(process.env.VITE_GITHUB_CLIENT_ID)
|
||||
},
|
||||
custom: {
|
||||
providerName: process.env.VITE_CUSTOM_OIDC_PROVIDER_NAME || 'not configured',
|
||||
clientId: maskCredential(process.env.VITE_CUSTOM_OIDC_CLIENT_ID),
|
||||
redirectUrl: process.env.VITE_CUSTOM_OIDC_REDIRECT_URL || 'not configured'
|
||||
clientId: maskCredential(process.env.VITE_CUSTOM_OIDC_CLIENT_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,7 +203,8 @@ router.get('/status/providers', (req: Request, res: Response) => {
|
||||
summary: {
|
||||
totalConfigured: availableProviders.length,
|
||||
availableProviders: availableProviders,
|
||||
obpApiHost: process.env.VITE_OBP_API_HOST || 'not configured'
|
||||
obpApiHost: process.env.VITE_OBP_API_HOST || 'not configured',
|
||||
sharedRedirectUrl: sharedRedirectUrl
|
||||
},
|
||||
providerStatus: allProviderStatus,
|
||||
environmentConfig: envConfig,
|
||||
|
||||
@ -29,14 +29,12 @@ import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { Container } from 'typedi'
|
||||
import OBPClientService from '../services/OBPClientService.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import { DEFAULT_OBP_API_VERSION } from '../../src/shared-constants.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Get services from container
|
||||
const obpClientService = Container.get(OBPClientService)
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
const obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
|
||||
|
||||
@ -57,41 +55,9 @@ router.get('/user/current', async (req: Request, res: Response) => {
|
||||
|
||||
console.log('User: Returning OAuth2 user info')
|
||||
const oauth2User = session.oauth2_user
|
||||
const accessToken = session.oauth2_access_token
|
||||
const refreshToken = session.oauth2_refresh_token
|
||||
|
||||
// Check if access token is expired and needs refresh
|
||||
if (accessToken && oauth2Service.isTokenExpired(accessToken)) {
|
||||
console.log('User: Access token expired')
|
||||
|
||||
if (refreshToken) {
|
||||
console.log('User: Attempting token refresh')
|
||||
try {
|
||||
const newTokens = await oauth2Service.refreshAccessToken(refreshToken)
|
||||
|
||||
// Update session with new tokens
|
||||
session.oauth2_access_token = newTokens.accessToken
|
||||
session.oauth2_refresh_token = newTokens.refreshToken || refreshToken
|
||||
session.oauth2_id_token = newTokens.idToken
|
||||
session.oauth2_token_timestamp = Date.now()
|
||||
session.oauth2_expires_in = newTokens.expiresIn
|
||||
|
||||
// Update clientConfig with new access token
|
||||
if (session.clientConfig && session.clientConfig.oauth2) {
|
||||
session.clientConfig.oauth2.accessToken = newTokens.accessToken
|
||||
console.log('User: Updated clientConfig with new access token')
|
||||
}
|
||||
|
||||
console.log('User: Token refresh successful')
|
||||
} catch (error) {
|
||||
console.error('User: Token refresh failed:', error)
|
||||
return res.json({})
|
||||
}
|
||||
} else {
|
||||
console.log('User: No refresh token available, user needs to re-authenticate')
|
||||
return res.json({})
|
||||
}
|
||||
}
|
||||
// TODO: Implement token refresh in multi-provider system
|
||||
// For now, if token expires, user must re-login
|
||||
|
||||
// Get actual user ID from OBP-API
|
||||
let obpUserId = oauth2User.sub // Default to sub if OBP call fails
|
||||
|
||||
@ -62,18 +62,21 @@ export class OAuth2ProviderFactory {
|
||||
* Each provider requires:
|
||||
* - VITE_[PROVIDER]_CLIENT_ID
|
||||
* - VITE_[PROVIDER]_CLIENT_SECRET
|
||||
* - VITE_[PROVIDER]_REDIRECT_URL (optional, defaults to /api/oauth2/callback)
|
||||
* - VITE_OAUTH2_REDIRECT_URL (shared by all providers, defaults to /api/oauth2/callback)
|
||||
*/
|
||||
private loadStrategies(): void {
|
||||
console.log('OAuth2ProviderFactory: Loading provider strategies...')
|
||||
|
||||
// Shared redirect URL for all providers
|
||||
const sharedRedirectUri =
|
||||
process.env.VITE_OAUTH2_REDIRECT_URL || 'http://localhost:5173/api/oauth2/callback'
|
||||
|
||||
// OBP-OIDC Strategy
|
||||
if (process.env.VITE_OBP_OAUTH2_CLIENT_ID) {
|
||||
if (process.env.VITE_OBP_OIDC_CLIENT_ID) {
|
||||
this.strategies.set('obp-oidc', {
|
||||
clientId: process.env.VITE_OBP_OAUTH2_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_OBP_OAUTH2_CLIENT_SECRET || '',
|
||||
redirectUri:
|
||||
process.env.VITE_OBP_OAUTH2_REDIRECT_URL || 'http://localhost:5173/api/oauth2/callback',
|
||||
clientId: process.env.VITE_OBP_OIDC_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_OBP_OIDC_CLIENT_SECRET || '',
|
||||
redirectUri: sharedRedirectUri,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' OK OBP-OIDC strategy loaded')
|
||||
@ -84,8 +87,7 @@ export class OAuth2ProviderFactory {
|
||||
this.strategies.set('keycloak', {
|
||||
clientId: process.env.VITE_KEYCLOAK_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_KEYCLOAK_CLIENT_SECRET || '',
|
||||
redirectUri:
|
||||
process.env.VITE_KEYCLOAK_REDIRECT_URL || 'http://localhost:5173/api/oauth2/callback',
|
||||
redirectUri: sharedRedirectUri,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' OK Keycloak strategy loaded')
|
||||
@ -96,8 +98,7 @@ export class OAuth2ProviderFactory {
|
||||
this.strategies.set('google', {
|
||||
clientId: process.env.VITE_GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_GOOGLE_CLIENT_SECRET || '',
|
||||
redirectUri:
|
||||
process.env.VITE_GOOGLE_REDIRECT_URL || 'http://localhost:5173/api/oauth2/callback',
|
||||
redirectUri: sharedRedirectUri,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' OK Google strategy loaded')
|
||||
@ -108,8 +109,7 @@ export class OAuth2ProviderFactory {
|
||||
this.strategies.set('github', {
|
||||
clientId: process.env.VITE_GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_GITHUB_CLIENT_SECRET || '',
|
||||
redirectUri:
|
||||
process.env.VITE_GITHUB_REDIRECT_URL || 'http://localhost:5173/api/oauth2/callback',
|
||||
redirectUri: sharedRedirectUri,
|
||||
scopes: ['read:user', 'user:email']
|
||||
})
|
||||
console.log(' OK GitHub strategy loaded')
|
||||
@ -121,9 +121,7 @@ export class OAuth2ProviderFactory {
|
||||
this.strategies.set(providerName, {
|
||||
clientId: process.env.VITE_CUSTOM_OIDC_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_CUSTOM_OIDC_CLIENT_SECRET || '',
|
||||
redirectUri:
|
||||
process.env.VITE_CUSTOM_OIDC_REDIRECT_URL ||
|
||||
'http://localhost:5173/api/oauth2/callback',
|
||||
redirectUri: sharedRedirectUri,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(` OK Custom OIDC strategy loaded: ${providerName}`)
|
||||
@ -134,7 +132,9 @@ export class OAuth2ProviderFactory {
|
||||
if (this.strategies.size === 0) {
|
||||
console.warn('OAuth2ProviderFactory: WARNING - No provider strategies configured!')
|
||||
console.warn('OAuth2ProviderFactory: Set environment variables for at least one provider')
|
||||
console.warn('OAuth2ProviderFactory: Example: VITE_OBP_OAUTH2_CLIENT_ID, VITE_OBP_OAUTH2_CLIENT_SECRET')
|
||||
console.warn(
|
||||
'OAuth2ProviderFactory: Example: VITE_OBP_OIDC_CLIENT_ID, VITE_OBP_OIDC_CLIENT_SECRET'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,10 +193,7 @@ export class OAuth2ProviderFactory {
|
||||
console.log(`OAuth2ProviderFactory: Successfully initialized ${wellKnownUri.provider}`)
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`OAuth2ProviderFactory: Failed to initialize ${wellKnownUri.provider}:`,
|
||||
error
|
||||
)
|
||||
console.error(`OAuth2ProviderFactory: Failed to initialize ${wellKnownUri.provider}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,689 +0,0 @@
|
||||
/*
|
||||
* Open Bank Project - API Explorer II
|
||||
* Copyright (C) 2023-2024, TESOBE GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Email: contact@tesobe.com
|
||||
* TESOBE GmbH
|
||||
* Osloerstrasse 16/17
|
||||
* Berlin 13359, Germany
|
||||
*
|
||||
* This product includes software developed at
|
||||
* TESOBE (http://www.tesobe.com/)
|
||||
*
|
||||
*/
|
||||
|
||||
import { OAuth2Client } from 'arctic'
|
||||
import { Service } from 'typedi'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
/**
|
||||
* OpenID Connect Discovery Configuration
|
||||
* As defined in OpenID Connect Discovery 1.0
|
||||
* @see https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
*/
|
||||
export interface OIDCConfiguration {
|
||||
issuer: string
|
||||
authorization_endpoint: string
|
||||
token_endpoint: string
|
||||
userinfo_endpoint: string
|
||||
jwks_uri: string
|
||||
registration_endpoint?: string
|
||||
scopes_supported?: string[]
|
||||
response_types_supported?: string[]
|
||||
response_modes_supported?: string[]
|
||||
grant_types_supported?: string[]
|
||||
subject_types_supported?: string[]
|
||||
id_token_signing_alg_values_supported?: string[]
|
||||
token_endpoint_auth_methods_supported?: string[]
|
||||
claims_supported?: string[]
|
||||
code_challenge_methods_supported?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Token response from OAuth2 token endpoint
|
||||
*/
|
||||
export interface TokenResponse {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
idToken?: string
|
||||
tokenType: string
|
||||
expiresIn?: number
|
||||
scope?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* User information from OIDC UserInfo endpoint
|
||||
*/
|
||||
export interface UserInfo {
|
||||
sub: string
|
||||
name?: string
|
||||
given_name?: string
|
||||
family_name?: string
|
||||
middle_name?: string
|
||||
nickname?: string
|
||||
preferred_username?: string
|
||||
profile?: string
|
||||
picture?: string
|
||||
website?: string
|
||||
email?: string
|
||||
email_verified?: boolean
|
||||
gender?: string
|
||||
birthdate?: string
|
||||
zoneinfo?: string
|
||||
locale?: string
|
||||
phone_number?: string
|
||||
phone_number_verified?: boolean
|
||||
address?: {
|
||||
formatted?: string
|
||||
street_address?: string
|
||||
locality?: string
|
||||
region?: string
|
||||
postal_code?: string
|
||||
country?: string
|
||||
}
|
||||
updated_at?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth2/OIDC Service
|
||||
*
|
||||
* Handles OAuth2 Authorization Code Flow with PKCE and OpenID Connect integration.
|
||||
* This service manages the complete OAuth2/OIDC authentication flow including:
|
||||
* - OIDC Discovery (fetching .well-known/openid-configuration)
|
||||
* - Authorization URL generation with PKCE
|
||||
* - Token exchange (authorization code for tokens)
|
||||
* - Token refresh
|
||||
* - UserInfo endpoint access
|
||||
*
|
||||
* @example
|
||||
* const oauth2Service = Container.get(OAuth2Service)
|
||||
* await oauth2Service.initializeFromWellKnown('http://localhost:9000/obp-oidc/.well-known/openid-configuration')
|
||||
* const authUrl = oauth2Service.createAuthorizationURL(state, ['openid', 'profile', 'email'])
|
||||
*/
|
||||
@Service()
|
||||
export class OAuth2Service {
|
||||
private client: OAuth2Client
|
||||
private oidcConfig: OIDCConfiguration | null = null
|
||||
private readonly clientId: string
|
||||
private readonly clientSecret: string
|
||||
private readonly redirectUri: string
|
||||
private initialized: boolean = false
|
||||
private wellKnownUrl: string = ''
|
||||
private healthCheckInterval: NodeJS.Timeout | null = null
|
||||
private healthCheckAttempts: number = 0
|
||||
private healthCheckActive: boolean = false
|
||||
|
||||
constructor() {
|
||||
// Load OAuth2 configuration from environment
|
||||
this.clientId = process.env.VITE_OBP_OAUTH2_CLIENT_ID || ''
|
||||
this.clientSecret = process.env.VITE_OBP_OAUTH2_CLIENT_SECRET || ''
|
||||
this.redirectUri = process.env.VITE_OBP_OAUTH2_REDIRECT_URL || ''
|
||||
|
||||
// Validate configuration
|
||||
if (!this.clientId) {
|
||||
console.warn('OAuth2Service: VITE_OBP_OAUTH2_CLIENT_ID not set')
|
||||
}
|
||||
if (!this.clientSecret) {
|
||||
console.warn('OAuth2Service: VITE_OBP_OAUTH2_CLIENT_SECRET not set')
|
||||
}
|
||||
if (!this.redirectUri) {
|
||||
console.warn('OAuth2Service: VITE_OBP_OAUTH2_REDIRECT_URL not set')
|
||||
}
|
||||
|
||||
// Initialize OAuth2 client
|
||||
this.client = new OAuth2Client(this.clientId, this.clientSecret, this.redirectUri)
|
||||
|
||||
console.log('OAuth2Service: Initialized with client ID:', this.clientId)
|
||||
console.log('OAuth2Service: Redirect URI:', this.redirectUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize OIDC configuration from well-known discovery endpoint
|
||||
*
|
||||
* @param {string} wellKnownUrl - The .well-known/openid-configuration URL
|
||||
* @throws {Error} If the discovery document cannot be fetched or is invalid
|
||||
*
|
||||
* @example
|
||||
* await oauth2Service.initializeFromWellKnown(
|
||||
* 'http://localhost:9000/obp-oidc/.well-known/openid-configuration'
|
||||
* )
|
||||
*/
|
||||
async initializeFromWellKnown(wellKnownUrl: string): Promise<void> {
|
||||
console.log('OAuth2Service: Fetching OIDC configuration from:', wellKnownUrl)
|
||||
|
||||
// Store the well-known URL for potential retries
|
||||
this.wellKnownUrl = wellKnownUrl
|
||||
|
||||
try {
|
||||
const response = await fetch(wellKnownUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch OIDC configuration: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const config = (await response.json()) as OIDCConfiguration
|
||||
|
||||
// Validate required endpoints
|
||||
if (!config.authorization_endpoint) {
|
||||
throw new Error('OIDC configuration missing authorization_endpoint')
|
||||
}
|
||||
if (!config.token_endpoint) {
|
||||
throw new Error('OIDC configuration missing token_endpoint')
|
||||
}
|
||||
if (!config.userinfo_endpoint) {
|
||||
throw new Error('OIDC configuration missing userinfo_endpoint')
|
||||
}
|
||||
|
||||
this.oidcConfig = config
|
||||
this.initialized = true
|
||||
|
||||
console.log('OAuth2Service: OIDC configuration loaded successfully')
|
||||
console.log(' Issuer:', config.issuer)
|
||||
console.log(' Authorization endpoint:', config.authorization_endpoint)
|
||||
console.log(' Token endpoint:', config.token_endpoint)
|
||||
console.log(' UserInfo endpoint:', config.userinfo_endpoint)
|
||||
console.log(' JWKS URI:', config.jwks_uri)
|
||||
|
||||
// Log supported features
|
||||
if (config.code_challenge_methods_supported) {
|
||||
console.log(' PKCE methods supported:', config.code_challenge_methods_supported.join(', '))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('OAuth2Service: Failed to initialize from well-known URL:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic health check to monitor OIDC availability
|
||||
* Uses exponential backoff when reconnecting: 1s, 2s, 4s, up to 4min
|
||||
* Switches to regular monitoring (every 4 minutes) once connected
|
||||
*
|
||||
* @param {number} initialIntervalMs - Initial interval in milliseconds (default: 1000 = 1 second)
|
||||
* @param {number} monitoringIntervalMs - Interval for continuous monitoring when connected (default: 240000 = 4 minutes)
|
||||
*
|
||||
* @example
|
||||
* oauth2Service.startHealthCheck(1000, 240000) // Start checking at 1 second, monitor every 4 minutes when connected
|
||||
*/
|
||||
startHealthCheck(initialIntervalMs: number = 1000, monitoringIntervalMs: number = 240000): void {
|
||||
if (this.healthCheckInterval) {
|
||||
console.log('OAuth2Service: Health check already running')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.wellKnownUrl) {
|
||||
console.warn('OAuth2Service: Cannot start health check - no well-known URL configured')
|
||||
return
|
||||
}
|
||||
|
||||
this.healthCheckAttempts = 0
|
||||
this.healthCheckActive = true
|
||||
console.log('OAuth2Service: Starting health check with exponential backoff')
|
||||
|
||||
const scheduleNextCheck = () => {
|
||||
if (!this.wellKnownUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
let delay: number
|
||||
|
||||
if (this.initialized) {
|
||||
// When connected, monitor every 4 minutes
|
||||
delay = monitoringIntervalMs
|
||||
} else {
|
||||
// When disconnected, use exponential backoff
|
||||
delay = Math.min(initialIntervalMs * Math.pow(2, this.healthCheckAttempts), 240000)
|
||||
}
|
||||
|
||||
const delayDisplay =
|
||||
delay < 60000
|
||||
? `${(delay / 1000).toFixed(0)} second(s)`
|
||||
: `${(delay / 60000).toFixed(1)} minute(s)`
|
||||
|
||||
if (this.initialized) {
|
||||
console.log(`OAuth2Service: Monitoring scheduled in ${delayDisplay}`)
|
||||
} else {
|
||||
console.log(
|
||||
`OAuth2Service: Health check scheduled in ${delayDisplay} (attempt ${this.healthCheckAttempts + 1})`
|
||||
)
|
||||
}
|
||||
|
||||
this.healthCheckInterval = setTimeout(async () => {
|
||||
if (this.initialized) {
|
||||
// When connected, verify OIDC is still available
|
||||
console.log('OAuth2Service: Verifying OIDC availability...')
|
||||
try {
|
||||
const response = await fetch(this.wellKnownUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`OIDC server returned ${response.status}`)
|
||||
}
|
||||
console.log('OAuth2Service: OIDC is available')
|
||||
// Continue monitoring
|
||||
scheduleNextCheck()
|
||||
} catch (error) {
|
||||
console.error('OAuth2Service: OIDC server is no longer available!')
|
||||
this.initialized = false
|
||||
this.oidcConfig = null
|
||||
this.healthCheckAttempts = 0
|
||||
console.log('OAuth2Service: Attempting to reconnect...')
|
||||
// Schedule reconnection with exponential backoff
|
||||
scheduleNextCheck()
|
||||
}
|
||||
} else {
|
||||
// When disconnected, attempt to reconnect
|
||||
console.log('OAuth2Service: Health check - attempting to reconnect to OIDC server...')
|
||||
try {
|
||||
await this.initializeFromWellKnown(this.wellKnownUrl)
|
||||
console.log('OAuth2Service: Successfully reconnected to OIDC server!')
|
||||
this.healthCheckAttempts = 0
|
||||
// Switch to continuous monitoring
|
||||
scheduleNextCheck()
|
||||
} catch (error) {
|
||||
this.healthCheckAttempts++
|
||||
// Schedule next reconnection attempt
|
||||
scheduleNextCheck()
|
||||
}
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
// Start the first check
|
||||
scheduleNextCheck()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the periodic health check
|
||||
*/
|
||||
stopHealthCheck(): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearTimeout(this.healthCheckInterval)
|
||||
this.healthCheckInterval = null
|
||||
this.healthCheckAttempts = 0
|
||||
this.healthCheckActive = false
|
||||
console.log('OAuth2Service: Health check stopped')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if health check is currently active
|
||||
*
|
||||
* @returns {boolean} True if health check is running
|
||||
*/
|
||||
isHealthCheckActive(): boolean {
|
||||
return this.healthCheckActive
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of health check attempts so far
|
||||
*
|
||||
* @returns {number} Number of health check attempts
|
||||
*/
|
||||
getHealthCheckAttempts(): number {
|
||||
return this.healthCheckAttempts
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to initialize with exponential backoff retry (continues indefinitely)
|
||||
*
|
||||
* @param {number} maxRetries - Maximum number of retry attempts (default: Infinity for continuous retries)
|
||||
* @param {string} wellKnownUrl - The .well-known/openid-configuration URL
|
||||
* @param {number} maxRetries - Maximum number of retry attempts (default: Infinity for continuous retries)
|
||||
* @param {number} initialDelayMs - Initial delay in milliseconds (default: 1000 = 1 second)
|
||||
* @returns {Promise<boolean>} True if initialization succeeded, false if maxRetries reached
|
||||
*
|
||||
* @example
|
||||
* const success = await oauth2Service.initializeWithRetry('http://localhost:9000/.well-known/openid-configuration', Infinity, 1000)
|
||||
*/
|
||||
async initializeWithRetry(
|
||||
wellKnownUrl: string,
|
||||
maxRetries: number = Infinity,
|
||||
initialDelayMs: number = 1000
|
||||
): Promise<boolean> {
|
||||
if (!wellKnownUrl) {
|
||||
console.error('OAuth2Service: Cannot retry - no well-known URL configured')
|
||||
return false
|
||||
}
|
||||
|
||||
// Store the well-known URL for retries and health checks
|
||||
this.wellKnownUrl = wellKnownUrl
|
||||
|
||||
let attempt = 0
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
await this.initializeFromWellKnown(wellKnownUrl)
|
||||
console.log(`OAuth2Service: Initialized successfully on attempt ${attempt + 1}`)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
const delay = Math.min(initialDelayMs * Math.pow(2, attempt), 240000) // Cap at 4 minutes
|
||||
const delayDisplay =
|
||||
delay < 60000
|
||||
? `${(delay / 1000).toFixed(0)} second(s)`
|
||||
: `${(delay / 60000).toFixed(1)} minute(s)`
|
||||
|
||||
if (maxRetries === Infinity || attempt < maxRetries - 1) {
|
||||
console.log(
|
||||
`OAuth2Service: Attempt ${attempt + 1} failed. Retrying in ${delayDisplay}...`
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
attempt++
|
||||
} else {
|
||||
console.error(
|
||||
`OAuth2Service: Failed to initialize after ${maxRetries} attempts:`,
|
||||
error.message
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is initialized and ready to use
|
||||
*
|
||||
* @returns {boolean} True if initialized, false otherwise
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.initialized && this.oidcConfig !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OIDC configuration
|
||||
*
|
||||
* @returns {OIDCConfiguration | null} The OIDC configuration or null if not initialized
|
||||
*/
|
||||
getOIDCConfiguration(): OIDCConfiguration | null {
|
||||
return this.oidcConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authorization URL for the OAuth2 flow
|
||||
*
|
||||
* @param {string} state - CSRF protection state parameter
|
||||
* @param {string[]} scopes - OAuth2 scopes to request (default: ['openid', 'profile', 'email'])
|
||||
* @returns {URL} The authorization URL to redirect the user to
|
||||
* @throws {Error} If the service is not initialized
|
||||
*
|
||||
* @example
|
||||
* const state = PKCEUtils.generateState()
|
||||
* const authUrl = oauth2Service.createAuthorizationURL(state, ['openid', 'profile', 'email'])
|
||||
* // Add PKCE challenge to URL
|
||||
* authUrl.searchParams.set('code_challenge', codeChallenge)
|
||||
* authUrl.searchParams.set('code_challenge_method', 'S256')
|
||||
* response.redirect(authUrl.toString())
|
||||
*/
|
||||
createAuthorizationURL(state: string, scopes: string[] = ['openid', 'profile', 'email']): URL {
|
||||
if (!this.isInitialized() || !this.oidcConfig) {
|
||||
throw new Error(
|
||||
'OAuth2Service: Service not initialized. Call initializeFromWellKnown() first'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('OAuth2Service: Creating authorization URL')
|
||||
console.log(' State:', state)
|
||||
console.log(' Scopes:', scopes.join(' '))
|
||||
|
||||
const authUrl = this.client.createAuthorizationURL(
|
||||
this.oidcConfig.authorization_endpoint,
|
||||
state,
|
||||
scopes
|
||||
)
|
||||
|
||||
return authUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an authorization code for tokens
|
||||
*
|
||||
* @param {string} code - The authorization code from the callback
|
||||
* @param {string} codeVerifier - The PKCE code verifier
|
||||
* @returns {Promise<TokenResponse>} The tokens (access, refresh, ID)
|
||||
* @throws {Error} If the token exchange fails
|
||||
*
|
||||
* @example
|
||||
* const tokens = await oauth2Service.exchangeCodeForTokens(code, codeVerifier)
|
||||
* console.log('Access token:', tokens.accessToken)
|
||||
* console.log('Refresh token:', tokens.refreshToken)
|
||||
* console.log('ID token:', tokens.idToken)
|
||||
*/
|
||||
async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<TokenResponse> {
|
||||
if (!this.isInitialized() || !this.oidcConfig) {
|
||||
throw new Error(
|
||||
'OAuth2Service: Service not initialized. Call initializeFromWellKnown() first'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('OAuth2Service: Exchanging authorization code for tokens')
|
||||
|
||||
try {
|
||||
// Use arctic's validateAuthorizationCode which handles the token request
|
||||
const tokens = await this.client.validateAuthorizationCode(
|
||||
this.oidcConfig.token_endpoint,
|
||||
code,
|
||||
codeVerifier
|
||||
)
|
||||
|
||||
console.log('OAuth2Service: Token exchange successful')
|
||||
|
||||
// Arctic returns an object with accessor functions
|
||||
const tokenResponse: TokenResponse = {
|
||||
accessToken: tokens.accessToken(),
|
||||
refreshToken: tokens.refreshToken ? tokens.refreshToken() : undefined,
|
||||
idToken: tokens.idToken ? tokens.idToken() : undefined,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: tokens.accessTokenExpiresAt
|
||||
? Math.floor((tokens.accessTokenExpiresAt().getTime() - Date.now()) / 1000)
|
||||
: undefined
|
||||
}
|
||||
|
||||
return tokenResponse
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2Service: Token exchange failed:', error)
|
||||
throw new Error(`Token exchange failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh an access token using a refresh token
|
||||
*
|
||||
* @param {string} refreshToken - The refresh token
|
||||
* @returns {Promise<TokenResponse>} The new tokens
|
||||
* @throws {Error} If the token refresh fails
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
||||
if (!this.isInitialized() || !this.oidcConfig) {
|
||||
throw new Error(
|
||||
'OAuth2Service: Service not initialized. Call initializeFromWellKnown() first'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('OAuth2Service: Refreshing access token')
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret
|
||||
})
|
||||
|
||||
const response = await fetch(this.oidcConfig.token_endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: body.toString()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
console.error('OAuth2Service: Token refresh failed:', errorData)
|
||||
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
console.log('OAuth2Service: Token refresh successful')
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token || refreshToken,
|
||||
idToken: data.id_token,
|
||||
tokenType: data.token_type || 'Bearer',
|
||||
expiresIn: data.expires_in,
|
||||
scope: data.scope
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2Service: Token refresh failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user information from the UserInfo endpoint
|
||||
*
|
||||
* @param {string} accessToken - The access token
|
||||
* @returns {Promise<UserInfo>} The user information
|
||||
* @throws {Error} If the UserInfo request fails
|
||||
*
|
||||
* @example
|
||||
* const userInfo = await oauth2Service.getUserInfo(accessToken)
|
||||
* console.log('User ID:', userInfo.sub)
|
||||
* console.log('Email:', userInfo.email)
|
||||
* console.log('Name:', userInfo.name)
|
||||
*/
|
||||
async getUserInfo(accessToken: string): Promise<UserInfo> {
|
||||
if (!this.isInitialized() || !this.oidcConfig) {
|
||||
throw new Error(
|
||||
'OAuth2Service: Service not initialized. Call initializeFromWellKnown() first'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('OAuth2Service: Fetching user info')
|
||||
|
||||
try {
|
||||
const response = await fetch(this.oidcConfig.userinfo_endpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
console.error('OAuth2Service: UserInfo request failed:', errorData)
|
||||
throw new Error(`UserInfo request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const userInfo = (await response.json()) as UserInfo
|
||||
|
||||
console.log('OAuth2Service: User info retrieved successfully')
|
||||
console.log(' User ID (sub):', userInfo.sub)
|
||||
console.log(' Email:', userInfo.email)
|
||||
console.log(' Name:', userInfo.name)
|
||||
|
||||
return userInfo
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2Service: UserInfo request failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and validate an ID token (basic validation only)
|
||||
*
|
||||
* Note: This performs basic JWT decoding. For production use, implement
|
||||
* full signature verification using the JWKS from the jwks_uri endpoint.
|
||||
*
|
||||
* @param {string} idToken - The ID token to decode
|
||||
* @returns {any} The decoded token payload
|
||||
*/
|
||||
decodeIdToken(idToken: string): any {
|
||||
try {
|
||||
const decoded = jwt.decode(idToken, { complete: true })
|
||||
|
||||
if (!decoded) {
|
||||
throw new Error('Failed to decode ID token')
|
||||
}
|
||||
|
||||
console.log('OAuth2Service: ID token decoded')
|
||||
console.log(' Issuer (iss):', decoded.payload['iss'])
|
||||
console.log(' Subject (sub):', decoded.payload['sub'])
|
||||
console.log(' Audience (aud):', decoded.payload['aud'])
|
||||
console.log(' Expiration (exp):', new Date(decoded.payload['exp'] * 1000).toISOString())
|
||||
|
||||
return decoded.payload
|
||||
} catch (error) {
|
||||
console.error('OAuth2Service: Failed to decode ID token:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an access token is expired
|
||||
*
|
||||
* @param {string} accessToken - The access token (JWT)
|
||||
* @returns {boolean} True if expired, false otherwise
|
||||
*/
|
||||
isTokenExpired(accessToken: string): boolean {
|
||||
try {
|
||||
const decoded: any = jwt.decode(accessToken)
|
||||
|
||||
if (!decoded || !decoded.exp) {
|
||||
console.warn('OAuth2Service: Token has no expiration claim')
|
||||
return false
|
||||
}
|
||||
|
||||
const isExpired = Date.now() >= decoded.exp * 1000
|
||||
|
||||
if (isExpired) {
|
||||
console.log('OAuth2Service: Access token is expired')
|
||||
}
|
||||
|
||||
return isExpired
|
||||
} catch (error) {
|
||||
console.error('OAuth2Service: Failed to check token expiration:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token expiration time in seconds
|
||||
*
|
||||
* @param {string} accessToken - The access token (JWT)
|
||||
* @returns {number | null} Seconds until expiration, or null if no expiration
|
||||
*/
|
||||
getTokenExpiresIn(accessToken: string): number | null {
|
||||
try {
|
||||
const decoded: any = jwt.decode(accessToken)
|
||||
|
||||
if (!decoded || !decoded.exp) {
|
||||
return null
|
||||
}
|
||||
|
||||
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000)
|
||||
return expiresIn > 0 ? expiresIn : 0
|
||||
} catch (error) {
|
||||
console.error('OAuth2Service: Failed to get token expiration:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,29 +85,8 @@ const availableProviders = ref<Array<{ name: string; available: boolean; lastChe
|
||||
const showProviderSelector = ref(false)
|
||||
const isLoadingProviders = ref(false)
|
||||
|
||||
// Check OAuth2 availability
|
||||
let oauth2CheckInterval: number | null = null
|
||||
|
||||
async function checkOAuth2Availability() {
|
||||
try {
|
||||
const response = await fetch('/api/status/oauth2')
|
||||
const data = await response.json()
|
||||
const wasAvailable = oauth2Available.value
|
||||
oauth2Available.value = data.available
|
||||
oauth2StatusMessage.value = data.message || ''
|
||||
|
||||
// Log state changes
|
||||
if (!wasAvailable && data.available) {
|
||||
console.log('OAuth2 is now available')
|
||||
} else if (wasAvailable && !data.available) {
|
||||
console.warn('OAuth2 is no longer available!')
|
||||
}
|
||||
} catch (error) {
|
||||
oauth2Available.value = false
|
||||
oauth2StatusMessage.value = 'Failed to check OAuth2 status'
|
||||
console.error('Error checking OAuth2 status:', error)
|
||||
}
|
||||
}
|
||||
// OAuth2 availability is determined by provider availability
|
||||
// No separate status check needed
|
||||
|
||||
// Fetch available OIDC providers
|
||||
async function fetchAvailableProviders() {
|
||||
@ -143,8 +122,9 @@ function handleLoginClick() {
|
||||
// Direct login with single provider
|
||||
loginWithProvider(available[0].name)
|
||||
} else {
|
||||
// Fallback to legacy login (no provider parameter)
|
||||
window.location.href = '/api/oauth2/connect?redirect=' + encodeURIComponent(getCurrentPath())
|
||||
// No providers available
|
||||
console.error('No OAuth2 providers available. Check backend configuration.')
|
||||
alert('Login is not available. Please check that OAuth2 providers are configured.')
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,16 +204,9 @@ const handleMore = (command: string) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Initial OAuth2 availability check
|
||||
await checkOAuth2Availability()
|
||||
|
||||
// Fetch available providers
|
||||
await fetchAvailableProviders()
|
||||
|
||||
// Start continuous polling every 4 minutes to detect OIDC outages
|
||||
console.log('OAuth2: Starting continuous monitoring (every 4 minutes)...')
|
||||
oauth2CheckInterval = window.setInterval(checkOAuth2Availability, 240000) // 4 minutes
|
||||
|
||||
const currentUser = await getCurrentUser()
|
||||
const currentResponseKeys = Object.keys(currentUser)
|
||||
if (currentResponseKeys.includes('username')) {
|
||||
@ -247,11 +220,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up polling interval
|
||||
if (oauth2CheckInterval) {
|
||||
clearInterval(oauth2CheckInterval)
|
||||
oauth2CheckInterval = null
|
||||
}
|
||||
// Cleanup hook
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
|
||||
@ -187,7 +187,7 @@ interface StatusResponse {
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const status = ref<StatusResponse | null>(null)
|
||||
const activeCollapse = ref<string[]>([])
|
||||
const activeCollapse = ref<string[]>(['obpOidc', 'keycloak', 'google', 'github', 'custom'])
|
||||
|
||||
const fetchStatus = async () => {
|
||||
loading.value = true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user