mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 18:56:58 +00:00
Add multi-OIDC provider backend services
- Add TypeScript interfaces for multi-provider OAuth2 support - Create OAuth2ClientWithConfig extending arctic OAuth2Client with OIDC discovery - Create OAuth2ProviderFactory with strategy pattern for different providers - Create OAuth2ProviderManager for managing multiple providers with health checks - Support for OBP-OIDC, Keycloak, Google, GitHub, and custom providers
This commit is contained in:
parent
100a79ce70
commit
3dadca8234
577
MULTI-OIDC-FLOW-DIAGRAM.md
Normal file
577
MULTI-OIDC-FLOW-DIAGRAM.md
Normal file
@ -0,0 +1,577 @@
|
||||
# Multi-OIDC Provider Flow Diagrams
|
||||
|
||||
## 1. System Initialization Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SERVER STARTUP │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Load Environment Variables │
|
||||
│ - VITE_OBP_OAUTH2_CLIENT_ID │
|
||||
│ - VITE_KEYCLOAK_CLIENT_ID │
|
||||
│ - VITE_GOOGLE_CLIENT_ID (optional) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Initialize OAuth2ProviderFactory │
|
||||
│ - Load provider strategies │
|
||||
│ - Configure client credentials │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Initialize OAuth2ProviderManager │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Fetch Well-Known URIs from OBP API │
|
||||
│ GET /obp/v5.1.0/well-known │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ OBP-OIDC │ │ Keycloak │
|
||||
│ Well-Known URL │ │ Well-Known URL │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
│ │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ For Each Provider: │
|
||||
│ 1. Get strategy from factory │
|
||||
│ 2. Create OAuth2ClientWithConfig │
|
||||
│ 3. Fetch .well-known/openid-config │
|
||||
│ 4. Store in providers Map │
|
||||
│ 5. Track status (available/unavailable)│
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Start Health Check (60s intervals) │
|
||||
│ - Monitor all providers │
|
||||
│ - Update availability status │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Server Ready │
|
||||
│ - Multiple providers initialized │
|
||||
│ - Health monitoring active │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. User Login Flow (Multi-Provider)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ USER │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Opens API Explorer II
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ HeaderNav.vue │
|
||||
│ - Fetch available providers │
|
||||
│ GET /api/oauth2/providers │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Display Login Button │
|
||||
│ (with dropdown if multiple providers) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│ User clicks "Login"
|
||||
▼
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
Single │ │ Multiple
|
||||
Provider │ │ Providers
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Direct Login │ │ Show Provider │
|
||||
│ │ │ Selection Dialog │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
│ │
|
||||
│ │ User selects provider
|
||||
│ │ (e.g., "obp-oidc")
|
||||
│ │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Redirect to: │
|
||||
│ /api/oauth2/connect? │
|
||||
│ provider=obp-oidc& │
|
||||
│ redirect=/resource-docs │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OAuth2ConnectController │
|
||||
│ 1. Get provider from query param │
|
||||
│ 2. Retrieve OAuth2Client from Manager │
|
||||
│ 3. Generate PKCE code_verifier │
|
||||
│ 4. Generate code_challenge (SHA256) │
|
||||
│ 5. Generate state (CSRF token) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Store in Session: │
|
||||
│ - oauth2_provider: "obp-oidc" │
|
||||
│ - oauth2_code_verifier: "..." │
|
||||
│ - oauth2_state: "..." │
|
||||
│ - oauth2_redirect: "/resource-docs" │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Build Authorization URL: │
|
||||
│ {provider_auth_endpoint}? │
|
||||
│ client_id=...& │
|
||||
│ redirect_uri=...& │
|
||||
│ response_type=code& │
|
||||
│ scope=openid+profile+email& │
|
||||
│ state=...& │
|
||||
│ code_challenge=...& │
|
||||
│ code_challenge_method=S256 │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 302 Redirect to OIDC Provider │
|
||||
│ (e.g., OBP-OIDC or Keycloak) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OIDC PROVIDER (OBP-OIDC / Keycloak) │
|
||||
│ - User enters credentials │
|
||||
│ - User authenticates │
|
||||
│ - Provider validates credentials │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Authentication successful
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 302 Redirect back to: │
|
||||
│ /api/oauth2/callback? │
|
||||
│ code=AUTHORIZATION_CODE& │
|
||||
│ state=CSRF_TOKEN │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OAuth2CallbackController │
|
||||
│ 1. Retrieve provider from session │
|
||||
│ 2. Validate state (CSRF protection) │
|
||||
│ 3. Get OAuth2Client for provider │
|
||||
│ 4. Retrieve code_verifier from session │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Exchange Authorization Code for Tokens │
|
||||
│ POST {provider_token_endpoint} │
|
||||
│ Body: │
|
||||
│ grant_type=authorization_code │
|
||||
│ code=... │
|
||||
│ redirect_uri=... │
|
||||
│ client_id=... │
|
||||
│ client_secret=... │
|
||||
│ code_verifier=... (PKCE) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Receive Tokens: │
|
||||
│ - access_token │
|
||||
│ - refresh_token │
|
||||
│ - id_token (JWT) │
|
||||
│ - expires_in │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Fetch User Info │
|
||||
│ GET {provider_userinfo_endpoint} │
|
||||
│ Authorization: Bearer {access_token} │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Store in Session: │
|
||||
│ - oauth2_access_token │
|
||||
│ - oauth2_refresh_token │
|
||||
│ - oauth2_id_token │
|
||||
│ - oauth2_provider: "obp-oidc" │
|
||||
│ - user: { username, email, name, ... } │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 302 Redirect to Original Page │
|
||||
│ /resource-docs │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ User Logged In │
|
||||
│ - Username displayed in header │
|
||||
│ - Access token available for API calls │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API Request Flow (Authenticated)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ USER │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Makes API request
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Frontend │
|
||||
│ GET /obp/v5.1.0/banks │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ RequestController │
|
||||
│ - Retrieve access_token from session │
|
||||
│ - Check if token is expired │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
Token │ │ Token
|
||||
Valid │ │ Expired
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Use Access Token │ │ Refresh Token │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌───────────────────────────┐
|
||||
│ │ Get provider from session│
|
||||
│ │ Get refresh_token │
|
||||
│ └───────────────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌───────────────────────────┐
|
||||
│ │ POST {token_endpoint} │
|
||||
│ │ grant_type=refresh_token │
|
||||
│ │ refresh_token=... │
|
||||
│ └───────────────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌───────────────────────────┐
|
||||
│ │ Receive new tokens │
|
||||
│ │ - new access_token │
|
||||
│ │ - new refresh_token │
|
||||
│ │ Update session │
|
||||
│ └───────────────────────────┘
|
||||
│ │
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Forward to OBP API │
|
||||
│ Authorization: Bearer {access_token} │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OBP API validates token with provider │
|
||||
│ - Validates signature │
|
||||
│ - Checks expiration │
|
||||
│ - Verifies scopes │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Return API Response │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Display data to user │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Provider Health Check Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Health Check Timer (60s intervals) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ OAuth2ProviderManager │
|
||||
│ performHealthCheck() │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Check OBP-OIDC │ │ Check Keycloak │
|
||||
│ HEAD {issuer} │ │ HEAD {issuer} │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
│ │
|
||||
┌───────┴───────┐ ┌───────┴───────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
│ OK │ │ FAIL │ │ OK │ │ FAIL │
|
||||
│ 200 │ │ 5xx │ │ 200 │ │ 5xx │
|
||||
└──────┘ └──────┘ └──────┘ └──────┘
|
||||
│ │ │ │
|
||||
└───────┬───────┘ └───────┬───────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Update Status │ │ Update Status │
|
||||
│ available: true │ │ available: false │
|
||||
│ lastChecked: now │ │ lastChecked: now │
|
||||
│ │ │ error: "..." │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
│ │
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Log Health Status │
|
||||
│ - obp-oidc: ✓ healthy │
|
||||
│ - keycloak: ✗ unhealthy (timeout) │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Frontend can query: │
|
||||
│ GET /api/oauth2/providers │
|
||||
│ (Returns updated status) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Component Interaction Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FRONTEND (Vue 3) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ HeaderNav.vue │ │
|
||||
│ │ - fetchAvailableProviders() │ │
|
||||
│ │ - handleLoginClick() │ │
|
||||
│ │ - loginWithProvider(provider) │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└─────────────────────────┼────────────────────────────────────────┘
|
||||
│ HTTP
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ BACKEND (Express) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ OAuth2ProvidersController │ │
|
||||
│ │ GET /api/oauth2/providers │ │
|
||||
│ └────────┬───────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ OAuth2ProviderManager │ │
|
||||
│ │ - providers: Map<string, OAuth2ClientWithConfig> │ │
|
||||
│ │ - providerStatus: Map<string, ProviderStatus> │ │
|
||||
│ │ - fetchWellKnownUris() │ │
|
||||
│ │ - initializeProviders() │ │
|
||||
│ │ - getProvider(name) │ │
|
||||
│ │ - getAvailableProviders() │ │
|
||||
│ │ - startHealthCheck() │ │
|
||||
│ └────────┬───────────────────────────────┬────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ uses │ creates │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ OBPClientService │ │ OAuth2ProviderFactory │ │
|
||||
│ │ - Fetch well-known │ │ - strategies: Map │ │
|
||||
│ │ from OBP API │ │ - initializeProvider() │ │
|
||||
│ └─────────────────────┘ └──────────┬──────────────┘ │
|
||||
│ │ │
|
||||
│ │ creates │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ OAuth2ClientWithConfig │ │
|
||||
│ │ - OIDCConfig │ │
|
||||
│ │ - provider: string │ │
|
||||
│ │ - initOIDCConfig() │ │
|
||||
│ │ - getAuthEndpoint() │ │
|
||||
│ │ - getTokenEndpoint() │ │
|
||||
│ │ - getUserInfoEndpoint() │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ OAuth2ConnectController │ │
|
||||
│ │ GET /api/oauth2/connect?provider=obp-oidc │ │
|
||||
│ │ 1. Get provider from ProviderManager │ │
|
||||
│ │ 2. Generate PKCE │ │
|
||||
│ │ 3. Store in session │ │
|
||||
│ │ 4. Redirect to provider │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ OAuth2CallbackController │ │
|
||||
│ │ GET /api/oauth2/callback?code=xxx&state=yyy │ │
|
||||
│ │ 1. Get provider from session │ │
|
||||
│ │ 2. Get OAuth2Client from ProviderManager │ │
|
||||
│ │ 3. Exchange code for tokens │ │
|
||||
│ │ 4. Fetch user info │ │
|
||||
│ │ 5. Store in session │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────┬───────────────────────────────────────┘
|
||||
│ HTTP
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OBP API │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ GET /obp/v5.1.0/well-known │
|
||||
│ → Returns list of OIDC provider configurations │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OIDC PROVIDERS │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ OBP-OIDC │ │ Keycloak │ │
|
||||
│ │ localhost:9000 │ │ localhost:8180 │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Data Flow: Session Storage
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SESSION DATA LIFECYCLE │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Step 1: Login Initiation
|
||||
┌──────────────────────────────────────┐
|
||||
│ Session │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ oauth2_provider: "obp-oidc" │ │ ← Store provider choice
|
||||
│ │ oauth2_code_verifier: "..." │ │ ← Store for PKCE
|
||||
│ │ oauth2_state: "..." │ │ ← Store for CSRF protection
|
||||
│ │ oauth2_redirect: "/resource-docs"│ │ ← Store redirect URL
|
||||
│ └────────────────────────────────┘ │
|
||||
└──────────────────────────────────────┘
|
||||
|
||||
Step 2: After Token Exchange
|
||||
┌──────────────────────────────────────┐
|
||||
│ Session │
|
||||
│ ┌────────────────────────────────┐ │
|
||||
│ │ oauth2_provider: "obp-oidc" │ │ ← Provider used
|
||||
│ │ oauth2_access_token: "..." │ │ ← For API calls
|
||||
│ │ oauth2_refresh_token: "..." │ │ ← For token refresh
|
||||
│ │ oauth2_id_token: "..." │ │ ← User identity (JWT)
|
||||
│ │ user: { │ │ ← User profile
|
||||
│ │ username: "john.doe" │ │
|
||||
│ │ email: "john@example.com" │ │
|
||||
│ │ name: "John Doe" │ │
|
||||
│ │ provider: "obp-oidc" │ │
|
||||
│ │ sub: "uuid-1234" │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└──────────────────────────────────────┘
|
||||
(oauth2_code_verifier deleted)
|
||||
(oauth2_state deleted)
|
||||
(oauth2_redirect deleted)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Error Handling Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ERROR SCENARIOS │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Scenario 1: Provider Not Available
|
||||
User clicks login
|
||||
│
|
||||
▼
|
||||
Fetch providers → No providers available
|
||||
│
|
||||
▼
|
||||
Show error: "Authentication not available"
|
||||
|
||||
Scenario 2: Invalid Provider
|
||||
User selects provider → GET /api/oauth2/connect?provider=invalid
|
||||
│
|
||||
▼
|
||||
ProviderManager.getProvider("invalid") → undefined
|
||||
│
|
||||
▼
|
||||
Return 400: "Provider not available"
|
||||
|
||||
Scenario 3: State Mismatch (CSRF Attack)
|
||||
Callback received → state parameter doesn't match session
|
||||
│
|
||||
▼
|
||||
Reject request → Redirect with error
|
||||
│
|
||||
▼
|
||||
Display: "Invalid state (CSRF protection)"
|
||||
|
||||
Scenario 4: Token Exchange Failure
|
||||
Exchange code for tokens → 401 Unauthorized
|
||||
│
|
||||
▼
|
||||
Log error → Redirect with error
|
||||
│
|
||||
▼
|
||||
Display: "Authentication failed"
|
||||
|
||||
Scenario 5: Provider Health Check Failure
|
||||
Health check → Provider unreachable
|
||||
│
|
||||
▼
|
||||
Mark as unavailable → Update status
|
||||
│
|
||||
▼
|
||||
Frontend queries providers → Shows as unavailable
|
||||
│
|
||||
▼
|
||||
User cannot select unavailable provider
|
||||
```
|
||||
1917
MULTI-OIDC-PROVIDER-IMPLEMENTATION.md
Normal file
1917
MULTI-OIDC-PROVIDER-IMPLEMENTATION.md
Normal file
File diff suppressed because it is too large
Load Diff
372
MULTI-OIDC-PROVIDER-SUMMARY.md
Normal file
372
MULTI-OIDC-PROVIDER-SUMMARY.md
Normal file
@ -0,0 +1,372 @@
|
||||
# Multi-OIDC Provider Implementation - Executive Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a high-level summary of implementing multiple OIDC provider support in API Explorer II, based on the proven architecture from OBP-Portal.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**API Explorer II** currently supports OAuth2/OIDC authentication with a **single provider** configured via environment variables:
|
||||
|
||||
```bash
|
||||
VITE_OBP_OAUTH2_WELL_KNOWN_URL=http://localhost:9000/obp-oidc/.well-known/openid-configuration
|
||||
VITE_OBP_OAUTH2_CLIENT_ID=<client-id>
|
||||
VITE_OBP_OAUTH2_CLIENT_SECRET=<client-secret>
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Only one OIDC provider supported at a time
|
||||
- No user choice of authentication method
|
||||
- Requires redeployment to switch providers
|
||||
- No fallback if provider is unavailable
|
||||
|
||||
---
|
||||
|
||||
## Target State
|
||||
|
||||
**Multi-Provider Support** allows users to choose from multiple identity providers at login:
|
||||
|
||||
- **OBP-OIDC** - Open Bank Project's identity provider
|
||||
- **Keycloak** - Enterprise identity management
|
||||
- **Google** - Consumer identity (optional)
|
||||
- **GitHub** - Developer identity (optional)
|
||||
- **Custom** - Any OpenID Connect provider
|
||||
|
||||
---
|
||||
|
||||
## How OBP-Portal Does It
|
||||
|
||||
### 1. Dynamic Provider Discovery
|
||||
|
||||
OBP-Portal fetches available OIDC providers from the **OBP API**:
|
||||
|
||||
```
|
||||
GET /obp/v5.1.0/well-known
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"well_known_uris": [
|
||||
{
|
||||
"provider": "obp-oidc",
|
||||
"url": "http://localhost:9000/obp-oidc/.well-known/openid-configuration"
|
||||
},
|
||||
{
|
||||
"provider": "keycloak",
|
||||
"url": "http://localhost:8180/realms/obp/.well-known/openid-configuration"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Provider Manager
|
||||
|
||||
**Key Component:** `OAuth2ProviderManager`
|
||||
|
||||
**Responsibilities:**
|
||||
- Fetch well-known URIs from OBP API
|
||||
- Initialize OAuth2 client for each provider
|
||||
- Track provider health (available/unavailable)
|
||||
- Perform periodic health checks (60s intervals)
|
||||
- Provide access to specific providers
|
||||
|
||||
### 3. Provider Factory
|
||||
|
||||
**Key Component:** `OAuth2ProviderFactory`
|
||||
|
||||
**Responsibilities:**
|
||||
- Strategy pattern for provider-specific configuration
|
||||
- Load credentials from environment variables
|
||||
- Create OAuth2 clients with OIDC discovery
|
||||
- Support multiple provider types
|
||||
|
||||
**Strategy Pattern:**
|
||||
```typescript
|
||||
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
|
||||
})
|
||||
|
||||
strategies.set('keycloak', {
|
||||
clientId: process.env.VITE_KEYCLOAK_CLIENT_ID,
|
||||
clientSecret: process.env.VITE_KEYCLOAK_CLIENT_SECRET,
|
||||
redirectUri: process.env.VITE_KEYCLOAK_REDIRECT_URL
|
||||
})
|
||||
```
|
||||
|
||||
### 4. User Flow
|
||||
|
||||
```
|
||||
1. User clicks "Login"
|
||||
→ Shows provider selection dialog
|
||||
|
||||
2. User selects provider (e.g., "OBP-OIDC")
|
||||
→ GET /api/oauth2/connect?provider=obp-oidc
|
||||
|
||||
3. Server:
|
||||
- Retrieves OAuth2 client for "obp-oidc"
|
||||
- Generates PKCE parameters
|
||||
- Stores provider name in session
|
||||
- Redirects to provider's authorization endpoint
|
||||
|
||||
4. User authenticates on selected OIDC provider
|
||||
|
||||
5. Provider redirects back:
|
||||
→ GET /api/oauth2/callback?code=xxx&state=yyy
|
||||
|
||||
6. Server:
|
||||
- Retrieves provider from session ("obp-oidc")
|
||||
- Gets corresponding OAuth2 client
|
||||
- Exchanges code for tokens
|
||||
- Stores tokens with provider name
|
||||
|
||||
7. User authenticated with selected provider
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Architecture for API Explorer II
|
||||
|
||||
### New Services
|
||||
|
||||
#### 1. **OAuth2ClientWithConfig** (extends `OAuth2Client` from arctic)
|
||||
```typescript
|
||||
class OAuth2ClientWithConfig extends OAuth2Client {
|
||||
public OIDCConfig?: OIDCConfiguration
|
||||
public provider: string
|
||||
|
||||
async initOIDCConfig(oidcConfigUrl: string): Promise<void>
|
||||
getAuthorizationEndpoint(): string
|
||||
getTokenEndpoint(): string
|
||||
getUserInfoEndpoint(): string
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **OAuth2ProviderFactory**
|
||||
```typescript
|
||||
class OAuth2ProviderFactory {
|
||||
private strategies: Map<string, ProviderStrategy>
|
||||
|
||||
async initializeProvider(wellKnownUri: WellKnownUri): Promise<OAuth2ClientWithConfig>
|
||||
getConfiguredProviders(): string[]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **OAuth2ProviderManager**
|
||||
```typescript
|
||||
class OAuth2ProviderManager {
|
||||
private providers: Map<string, OAuth2ClientWithConfig>
|
||||
|
||||
async fetchWellKnownUris(): Promise<WellKnownUri[]>
|
||||
async initializeProviders(): Promise<boolean>
|
||||
getProvider(providerName: string): OAuth2ClientWithConfig
|
||||
getAvailableProviders(): string[]
|
||||
startHealthCheck(intervalMs: number): void
|
||||
}
|
||||
```
|
||||
|
||||
### Updated Controllers
|
||||
|
||||
#### 1. **OAuth2ProvidersController** (NEW)
|
||||
```typescript
|
||||
GET /api/oauth2/providers
|
||||
→ Returns: { providers: [...], count: 2, availableCount: 1 }
|
||||
```
|
||||
|
||||
#### 2. **OAuth2ConnectController** (UPDATED)
|
||||
```typescript
|
||||
GET /api/oauth2/connect?provider=obp-oidc&redirect=/resource-docs
|
||||
→ Redirects to selected provider's authorization endpoint
|
||||
```
|
||||
|
||||
#### 3. **OAuth2CallbackController** (UPDATED)
|
||||
```typescript
|
||||
GET /api/oauth2/callback?code=xxx&state=yyy
|
||||
→ Uses provider from session to exchange code for tokens
|
||||
```
|
||||
|
||||
### Frontend Updates
|
||||
|
||||
#### **HeaderNav.vue** (UPDATED)
|
||||
|
||||
**Before:**
|
||||
```vue
|
||||
<a href="/api/oauth2/connect">Login</a>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```vue
|
||||
<button @click="handleLoginClick">
|
||||
Login
|
||||
<span v-if="availableProviders.length > 1">▼</span>
|
||||
</button>
|
||||
|
||||
<!-- Provider Selection Dialog -->
|
||||
<el-dialog v-model="showProviderSelector">
|
||||
<div v-for="provider in availableProviders">
|
||||
<div @click="loginWithProvider(provider.name)">
|
||||
{{ provider.name }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# OBP-OIDC Provider
|
||||
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
|
||||
|
||||
# Keycloak Provider
|
||||
VITE_KEYCLOAK_CLIENT_ID=obp-api-explorer
|
||||
VITE_KEYCLOAK_CLIENT_SECRET=your-keycloak-secret
|
||||
VITE_KEYCLOAK_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
|
||||
# Google Provider (Optional)
|
||||
VITE_GOOGLE_CLIENT_ID=your-google-client-id
|
||||
VITE_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
VITE_GOOGLE_REDIRECT_URL=http://localhost:5173/api/oauth2/callback
|
||||
```
|
||||
|
||||
**Note:** No need to specify well-known URLs - they are fetched from OBP API!
|
||||
|
||||
---
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### 1. **Dynamic Discovery**
|
||||
- Providers are discovered from OBP API at runtime
|
||||
- No hardcoded provider list
|
||||
- Easy to add new providers without code changes
|
||||
|
||||
### 2. **User Choice**
|
||||
- Users select their preferred authentication method
|
||||
- Better user experience
|
||||
- Support for organizational identity preferences
|
||||
|
||||
### 3. **Resilience**
|
||||
- Health monitoring detects provider outages
|
||||
- Can fallback to alternative providers
|
||||
- Automatic retry for failed initializations
|
||||
|
||||
### 4. **Extensibility**
|
||||
- Strategy pattern makes adding providers trivial
|
||||
- Just add environment variables
|
||||
- No code changes needed
|
||||
|
||||
### 5. **Backward Compatibility**
|
||||
- Existing single-provider mode still works
|
||||
- Gradual migration path
|
||||
- No breaking changes
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### **Phase 1: Backend Services** (Week 1)
|
||||
- [ ] Create `OAuth2ClientWithConfig`
|
||||
- [ ] Create `OAuth2ProviderFactory`
|
||||
- [ ] Create `OAuth2ProviderManager`
|
||||
- [ ] Create TypeScript interfaces
|
||||
|
||||
### **Phase 2: Backend Controllers** (Week 1-2)
|
||||
- [ ] Create `OAuth2ProvidersController`
|
||||
- [ ] Update `OAuth2ConnectController` with provider parameter
|
||||
- [ ] Update `OAuth2CallbackController` to use provider from session
|
||||
|
||||
### **Phase 3: Frontend** (Week 2)
|
||||
- [ ] Update `HeaderNav.vue` to fetch providers
|
||||
- [ ] Add provider selection UI (dialog/dropdown)
|
||||
- [ ] Update login flow to include provider selection
|
||||
|
||||
### **Phase 4: Configuration & Testing** (Week 2-3)
|
||||
- [ ] Configure environment variables for multiple providers
|
||||
- [ ] Write unit tests
|
||||
- [ ] Write integration tests
|
||||
- [ ] Manual testing with OBP-OIDC and Keycloak
|
||||
- [ ] Update documentation
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
### **Step 1: Deploy with Backward Compatibility**
|
||||
- Implement new services
|
||||
- Keep existing single-provider mode working
|
||||
- Test thoroughly
|
||||
|
||||
### **Step 2: Enable Multi-Provider**
|
||||
- Add provider environment variables
|
||||
- Enable provider selection UI
|
||||
- Monitor for issues
|
||||
|
||||
### **Step 3: Deprecate Single-Provider**
|
||||
- Update documentation
|
||||
- Remove `VITE_OBP_OAUTH2_WELL_KNOWN_URL` env variable
|
||||
- Use OBP API well-known endpoint by default
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- `OAuth2ProviderFactory.test.ts` - Strategy creation
|
||||
- `OAuth2ProviderManager.test.ts` - Provider initialization
|
||||
- `OAuth2ClientWithConfig.test.ts` - OIDC config loading
|
||||
|
||||
### Integration Tests
|
||||
- Multi-provider login flow
|
||||
- Provider selection
|
||||
- Token exchange with different providers
|
||||
- Callback handling
|
||||
|
||||
### Manual Testing
|
||||
- Login with OBP-OIDC
|
||||
- Login with Keycloak
|
||||
- Provider unavailable scenarios
|
||||
- Network error handling
|
||||
- User cancellation
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ Users can choose from multiple OIDC providers
|
||||
- ✅ Providers are discovered from OBP API automatically
|
||||
- ✅ Health monitoring detects provider outages
|
||||
- ✅ Backward compatible with single-provider mode
|
||||
- ✅ No code changes needed to add new providers (only env vars)
|
||||
- ✅ Comprehensive test coverage (>80%)
|
||||
- ✅ Documentation updated
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Full Implementation Guide:** `MULTI-OIDC-PROVIDER-IMPLEMENTATION.md`
|
||||
- **OBP-Portal Reference:** `~/Documents/workspace_2024/OBP-Portal`
|
||||
- **OBP API Well-Known Endpoint:** `/obp/v5.1.0/well-known`
|
||||
- **Current OAuth2 Docs:** `OAUTH2-README.md`, `OAUTH2-OIDC-INTEGRATION-PREP.md`
|
||||
- **Arctic OAuth2 Library:** https://github.com/pilcrowOnPaper/arctic
|
||||
- **OpenID Connect Discovery:** https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
For detailed implementation instructions, see **MULTI-OIDC-PROVIDER-IMPLEMENTATION.md**
|
||||
|
||||
For OBP-Portal reference implementation, see:
|
||||
- `OBP-Portal/src/lib/oauth/providerManager.ts`
|
||||
- `OBP-Portal/src/lib/oauth/providerFactory.ts`
|
||||
- `OBP-Portal/src/lib/oauth/client.ts`
|
||||
282
server/services/OAuth2ClientWithConfig.ts
Normal file
282
server/services/OAuth2ClientWithConfig.ts
Normal file
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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 type { OIDCConfiguration, TokenResponse } from '../types/oauth2.js'
|
||||
|
||||
/**
|
||||
* Extended OAuth2 Client with OIDC configuration support
|
||||
*
|
||||
* This class extends the arctic OAuth2Client to add:
|
||||
* - OIDC discovery document (.well-known/openid-configuration)
|
||||
* - Provider name tracking
|
||||
* - Provider-specific token exchange logic
|
||||
*
|
||||
* @example
|
||||
* const client = new OAuth2ClientWithConfig(
|
||||
* 'client-id',
|
||||
* 'client-secret',
|
||||
* 'http://localhost:5173/api/oauth2/callback',
|
||||
* 'obp-oidc'
|
||||
* )
|
||||
* await client.initOIDCConfig('http://localhost:9000/obp-oidc/.well-known/openid-configuration')
|
||||
*/
|
||||
export class OAuth2ClientWithConfig extends OAuth2Client {
|
||||
public OIDCConfig?: OIDCConfiguration
|
||||
public provider: string
|
||||
|
||||
constructor(clientId: string, clientSecret: string, redirectUri: string, provider: string) {
|
||||
super(clientId, clientSecret, redirectUri)
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize OIDC configuration from well-known discovery endpoint
|
||||
*
|
||||
* @param oidcConfigUrl - Full URL to .well-known/openid-configuration
|
||||
* @throws {Error} If the discovery document cannot be fetched or is invalid
|
||||
*
|
||||
* @example
|
||||
* await client.initOIDCConfig('http://localhost:9000/obp-oidc/.well-known/openid-configuration')
|
||||
*/
|
||||
async initOIDCConfig(oidcConfigUrl: string): Promise<void> {
|
||||
console.log(
|
||||
`OAuth2ClientWithConfig: Fetching OIDC config for ${this.provider} from:`,
|
||||
oidcConfigUrl
|
||||
)
|
||||
|
||||
try {
|
||||
const response = await fetch(oidcConfigUrl)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch OIDC configuration for ${this.provider}: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const config = (await response.json()) as OIDCConfiguration
|
||||
|
||||
// Validate required endpoints
|
||||
if (!config.authorization_endpoint) {
|
||||
throw new Error(`OIDC configuration for ${this.provider} missing authorization_endpoint`)
|
||||
}
|
||||
if (!config.token_endpoint) {
|
||||
throw new Error(`OIDC configuration for ${this.provider} missing token_endpoint`)
|
||||
}
|
||||
if (!config.userinfo_endpoint) {
|
||||
throw new Error(`OIDC configuration for ${this.provider} missing userinfo_endpoint`)
|
||||
}
|
||||
|
||||
this.OIDCConfig = config
|
||||
|
||||
console.log(`OAuth2ClientWithConfig: OIDC config loaded for ${this.provider}`)
|
||||
console.log(` Issuer: ${config.issuer}`)
|
||||
console.log(` Authorization: ${config.authorization_endpoint}`)
|
||||
console.log(` Token: ${config.token_endpoint}`)
|
||||
console.log(` UserInfo: ${config.userinfo_endpoint}`)
|
||||
|
||||
// Log supported PKCE methods if available
|
||||
if (config.code_challenge_methods_supported) {
|
||||
console.log(` PKCE methods: ${config.code_challenge_methods_supported.join(', ')}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`OAuth2ClientWithConfig: Failed to initialize ${this.provider}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization endpoint from OIDC config
|
||||
*
|
||||
* @returns Authorization endpoint URL
|
||||
* @throws {Error} If OIDC configuration not initialized
|
||||
*/
|
||||
getAuthorizationEndpoint(): string {
|
||||
if (!this.OIDCConfig?.authorization_endpoint) {
|
||||
throw new Error(`OIDC configuration not initialized for ${this.provider}`)
|
||||
}
|
||||
return this.OIDCConfig.authorization_endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token endpoint from OIDC config
|
||||
*
|
||||
* @returns Token endpoint URL
|
||||
* @throws {Error} If OIDC configuration not initialized
|
||||
*/
|
||||
getTokenEndpoint(): string {
|
||||
if (!this.OIDCConfig?.token_endpoint) {
|
||||
throw new Error(`OIDC configuration not initialized for ${this.provider}`)
|
||||
}
|
||||
return this.OIDCConfig.token_endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userinfo endpoint from OIDC config
|
||||
*
|
||||
* @returns UserInfo endpoint URL
|
||||
* @throws {Error} If OIDC configuration not initialized
|
||||
*/
|
||||
getUserInfoEndpoint(): string {
|
||||
if (!this.OIDCConfig?.userinfo_endpoint) {
|
||||
throw new Error(`OIDC configuration not initialized for ${this.provider}`)
|
||||
}
|
||||
return this.OIDCConfig.userinfo_endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OIDC configuration is initialized
|
||||
*
|
||||
* @returns True if OIDC config has been loaded
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.OIDCConfig !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate authorization code and exchange for tokens
|
||||
*
|
||||
* This method extends the base OAuth2Client functionality to support
|
||||
* provider-specific token exchange requirements (e.g., Basic Auth vs form-based credentials)
|
||||
*
|
||||
* @param code - Authorization code from OIDC provider
|
||||
* @param codeVerifier - PKCE code verifier
|
||||
* @returns Token response with access token, refresh token, and ID token
|
||||
*/
|
||||
async validateAuthorizationCode(code: string, codeVerifier: string): Promise<TokenResponse> {
|
||||
const tokenEndpoint = this.getTokenEndpoint()
|
||||
|
||||
console.log(`OAuth2ClientWithConfig: Exchanging authorization code for ${this.provider}`)
|
||||
|
||||
// Prepare token request body
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: this.redirectURI,
|
||||
code_verifier: codeVerifier,
|
||||
client_id: this.clientId
|
||||
})
|
||||
|
||||
// Add client_secret to body (some providers prefer this over Basic Auth)
|
||||
if (this.clientSecret) {
|
||||
body.append('client_secret', this.clientSecret)
|
||||
}
|
||||
|
||||
try {
|
||||
// Try with Basic Authentication first (RFC 6749 standard)
|
||||
const authHeader = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${authHeader}`
|
||||
},
|
||||
body: body.toString()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
throw new Error(
|
||||
`Token exchange failed for ${this.provider}: ${response.status} ${response.statusText} - ${errorData}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
idToken: data.id_token,
|
||||
tokenType: data.token_type || 'Bearer',
|
||||
expiresIn: data.expires_in,
|
||||
scope: data.scope
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`OAuth2ClientWithConfig: Token exchange error for ${this.provider}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*
|
||||
* @param refreshToken - Refresh token from previous authentication
|
||||
* @returns New token response
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
||||
const tokenEndpoint = this.getTokenEndpoint()
|
||||
|
||||
console.log(`OAuth2ClientWithConfig: Refreshing access token for ${this.provider}`)
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: this.clientId
|
||||
})
|
||||
|
||||
if (this.clientSecret) {
|
||||
body.append('client_secret', this.clientSecret)
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${authHeader}`
|
||||
},
|
||||
body: body.toString()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
throw new Error(
|
||||
`Token refresh failed for ${this.provider}: ${response.status} ${response.statusText} - ${errorData}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token || refreshToken, // Some providers don't return new refresh token
|
||||
idToken: data.id_token,
|
||||
tokenType: data.token_type || 'Bearer',
|
||||
expiresIn: data.expires_in,
|
||||
scope: data.scope
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`OAuth2ClientWithConfig: Token refresh error for ${this.provider}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
241
server/services/OAuth2ProviderFactory.ts
Normal file
241
server/services/OAuth2ProviderFactory.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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 { Service } from 'typedi'
|
||||
import { OAuth2ClientWithConfig } from './OAuth2ClientWithConfig.js'
|
||||
import type { WellKnownUri, ProviderStrategy } from '../types/oauth2.js'
|
||||
|
||||
/**
|
||||
* Factory for creating OAuth2 clients for different OIDC providers
|
||||
*
|
||||
* Uses the Strategy pattern to handle provider-specific configurations:
|
||||
* - OBP-OIDC
|
||||
* - Keycloak
|
||||
* - Google
|
||||
* - GitHub
|
||||
* - Custom providers
|
||||
*
|
||||
* Configuration is loaded from environment variables.
|
||||
*
|
||||
* @example
|
||||
* const factory = Container.get(OAuth2ProviderFactory)
|
||||
* const client = await factory.initializeProvider({
|
||||
* provider: 'obp-oidc',
|
||||
* url: 'http://localhost:9000/obp-oidc/.well-known/openid-configuration'
|
||||
* })
|
||||
*/
|
||||
@Service()
|
||||
export class OAuth2ProviderFactory {
|
||||
private strategies: Map<string, ProviderStrategy> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.loadStrategies()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load provider strategies from environment variables
|
||||
*
|
||||
* Each provider requires:
|
||||
* - VITE_[PROVIDER]_CLIENT_ID
|
||||
* - VITE_[PROVIDER]_CLIENT_SECRET
|
||||
* - VITE_[PROVIDER]_REDIRECT_URL (optional, defaults to /api/oauth2/callback)
|
||||
*/
|
||||
private loadStrategies(): void {
|
||||
console.log('OAuth2ProviderFactory: Loading provider strategies...')
|
||||
|
||||
// OBP-OIDC Strategy
|
||||
if (process.env.VITE_OBP_OAUTH2_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',
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' ✓ OBP-OIDC strategy loaded')
|
||||
}
|
||||
|
||||
// Keycloak Strategy
|
||||
if (process.env.VITE_KEYCLOAK_CLIENT_ID) {
|
||||
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',
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' ✓ Keycloak strategy loaded')
|
||||
}
|
||||
|
||||
// Google Strategy
|
||||
if (process.env.VITE_GOOGLE_CLIENT_ID) {
|
||||
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',
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(' ✓ Google strategy loaded')
|
||||
}
|
||||
|
||||
// GitHub Strategy
|
||||
if (process.env.VITE_GITHUB_CLIENT_ID) {
|
||||
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',
|
||||
scopes: ['read:user', 'user:email']
|
||||
})
|
||||
console.log(' ✓ GitHub strategy loaded')
|
||||
}
|
||||
|
||||
// Generic OIDC Strategy (for custom providers)
|
||||
if (process.env.VITE_CUSTOM_OIDC_CLIENT_ID) {
|
||||
const providerName = process.env.VITE_CUSTOM_OIDC_PROVIDER_NAME || 'custom-oidc'
|
||||
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',
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
})
|
||||
console.log(` ✓ Custom OIDC strategy loaded: ${providerName}`)
|
||||
}
|
||||
|
||||
console.log(`OAuth2ProviderFactory: Loaded ${this.strategies.size} provider strategies`)
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an OAuth2 client for a specific provider
|
||||
*
|
||||
* @param wellKnownUri - Provider information from OBP API
|
||||
* @returns Initialized OAuth2 client or null if no strategy exists or initialization fails
|
||||
*
|
||||
* @example
|
||||
* const client = await factory.initializeProvider({
|
||||
* provider: 'obp-oidc',
|
||||
* url: 'http://localhost:9000/obp-oidc/.well-known/openid-configuration'
|
||||
* })
|
||||
*/
|
||||
async initializeProvider(wellKnownUri: WellKnownUri): Promise<OAuth2ClientWithConfig | null> {
|
||||
console.log(`OAuth2ProviderFactory: Initializing provider: ${wellKnownUri.provider}`)
|
||||
|
||||
const strategy = this.strategies.get(wellKnownUri.provider)
|
||||
if (!strategy) {
|
||||
console.warn(
|
||||
`OAuth2ProviderFactory: No strategy found for provider: ${wellKnownUri.provider}`
|
||||
)
|
||||
console.warn(
|
||||
`OAuth2ProviderFactory: Available strategies: ${Array.from(this.strategies.keys()).join(', ')}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate strategy configuration
|
||||
if (!strategy.clientId) {
|
||||
console.error(
|
||||
`OAuth2ProviderFactory: Missing clientId for provider: ${wellKnownUri.provider}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!strategy.clientSecret) {
|
||||
console.warn(
|
||||
`OAuth2ProviderFactory: Missing clientSecret for provider: ${wellKnownUri.provider}`
|
||||
)
|
||||
console.warn(`OAuth2ProviderFactory: Some providers require a client secret`)
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new OAuth2ClientWithConfig(
|
||||
strategy.clientId,
|
||||
strategy.clientSecret,
|
||||
strategy.redirectUri,
|
||||
wellKnownUri.provider
|
||||
)
|
||||
|
||||
// Initialize OIDC configuration from discovery endpoint
|
||||
await client.initOIDCConfig(wellKnownUri.url)
|
||||
|
||||
console.log(`OAuth2ProviderFactory: Successfully initialized ${wellKnownUri.provider}`)
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`OAuth2ProviderFactory: Failed to initialize ${wellKnownUri.provider}:`,
|
||||
error
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of configured provider names
|
||||
*
|
||||
* @returns Array of provider names that have strategies configured
|
||||
*/
|
||||
getConfiguredProviders(): string[] {
|
||||
return Array.from(this.strategies.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider strategy exists
|
||||
*
|
||||
* @param providerName - Name of the provider to check
|
||||
* @returns True if strategy exists for this provider
|
||||
*/
|
||||
hasStrategy(providerName: string): boolean {
|
||||
return this.strategies.has(providerName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get strategy for a specific provider (for debugging/testing)
|
||||
*
|
||||
* @param providerName - Name of the provider
|
||||
* @returns Provider strategy or undefined if not found
|
||||
*/
|
||||
getStrategy(providerName: string): ProviderStrategy | undefined {
|
||||
return this.strategies.get(providerName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of configured strategies
|
||||
*
|
||||
* @returns Number of provider strategies loaded
|
||||
*/
|
||||
getStrategyCount(): number {
|
||||
return this.strategies.size
|
||||
}
|
||||
}
|
||||
385
server/services/OAuth2ProviderManager.ts
Normal file
385
server/services/OAuth2ProviderManager.ts
Normal file
@ -0,0 +1,385 @@
|
||||
/*
|
||||
* 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 { Service, Container } from 'typedi'
|
||||
import { OAuth2ProviderFactory } from './OAuth2ProviderFactory.js'
|
||||
import { OAuth2ClientWithConfig } from './OAuth2ClientWithConfig.js'
|
||||
import OBPClientService from './OBPClientService.js'
|
||||
import type { WellKnownUri, WellKnownResponse, ProviderStatus } from '../types/oauth2.js'
|
||||
|
||||
/**
|
||||
* Manager for multiple OAuth2/OIDC providers
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Fetch available OIDC providers from OBP API
|
||||
* - Initialize OAuth2 clients for each provider
|
||||
* - Track provider health status
|
||||
* - Perform periodic health checks
|
||||
* - Provide access to provider clients
|
||||
*
|
||||
* The manager automatically:
|
||||
* - Retries failed provider initializations
|
||||
* - Monitors provider availability (60s intervals by default)
|
||||
* - Updates provider status in real-time
|
||||
*
|
||||
* @example
|
||||
* const manager = Container.get(OAuth2ProviderManager)
|
||||
* await manager.initializeProviders()
|
||||
* const client = manager.getProvider('obp-oidc')
|
||||
*/
|
||||
@Service()
|
||||
export class OAuth2ProviderManager {
|
||||
private providers: Map<string, OAuth2ClientWithConfig> = new Map()
|
||||
private providerStatus: Map<string, ProviderStatus> = new Map()
|
||||
private healthCheckInterval: NodeJS.Timeout | null = null
|
||||
private factory: OAuth2ProviderFactory
|
||||
private obpClientService: OBPClientService
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor() {
|
||||
this.factory = Container.get(OAuth2ProviderFactory)
|
||||
this.obpClientService = Container.get(OBPClientService)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch well-known URIs from OBP API
|
||||
*
|
||||
* Calls: GET /obp/v5.1.0/well-known
|
||||
*
|
||||
* @returns Array of well-known URIs with provider names
|
||||
*/
|
||||
async fetchWellKnownUris(): Promise<WellKnownUri[]> {
|
||||
console.log('OAuth2ProviderManager: Fetching well-known URIs from OBP API...')
|
||||
|
||||
try {
|
||||
// Use OBPClientService to call the API
|
||||
const response = await this.obpClientService.call<WellKnownResponse>(
|
||||
'GET',
|
||||
'/obp/v5.1.0/well-known',
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
if (!response.well_known_uris || response.well_known_uris.length === 0) {
|
||||
console.warn('OAuth2ProviderManager: No well-known URIs found in OBP API response')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log(`OAuth2ProviderManager: Found ${response.well_known_uris.length} providers:`)
|
||||
response.well_known_uris.forEach((uri) => {
|
||||
console.log(` - ${uri.provider}: ${uri.url}`)
|
||||
})
|
||||
|
||||
return response.well_known_uris
|
||||
} catch (error) {
|
||||
console.error('OAuth2ProviderManager: Failed to fetch well-known URIs:', error)
|
||||
console.warn('OAuth2ProviderManager: Falling back to no providers')
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all OAuth2 providers from OBP API
|
||||
*
|
||||
* This method:
|
||||
* 1. Fetches well-known URIs from OBP API
|
||||
* 2. Initializes OAuth2 client for each provider
|
||||
* 3. Tracks successful and failed initializations
|
||||
* 4. Returns success status
|
||||
*
|
||||
* @returns True if at least one provider was initialized successfully
|
||||
*/
|
||||
async initializeProviders(): Promise<boolean> {
|
||||
console.log('OAuth2ProviderManager: Initializing providers...')
|
||||
|
||||
const wellKnownUris = await this.fetchWellKnownUris()
|
||||
|
||||
if (wellKnownUris.length === 0) {
|
||||
console.warn('OAuth2ProviderManager: No providers to initialize')
|
||||
console.warn(
|
||||
'OAuth2ProviderManager: Check that OBP API is running and /obp/v5.1.0/well-known endpoint is available'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
let successCount = 0
|
||||
|
||||
for (const providerUri of wellKnownUris) {
|
||||
try {
|
||||
const client = await this.factory.initializeProvider(providerUri)
|
||||
|
||||
if (client && client.isInitialized()) {
|
||||
this.providers.set(providerUri.provider, client)
|
||||
this.providerStatus.set(providerUri.provider, {
|
||||
name: providerUri.provider,
|
||||
available: true,
|
||||
lastChecked: new Date()
|
||||
})
|
||||
successCount++
|
||||
console.log(`OAuth2ProviderManager: ✓ ${providerUri.provider} initialized`)
|
||||
} else {
|
||||
this.providerStatus.set(providerUri.provider, {
|
||||
name: providerUri.provider,
|
||||
available: false,
|
||||
lastChecked: new Date(),
|
||||
error: 'Failed to initialize client'
|
||||
})
|
||||
console.warn(`OAuth2ProviderManager: ✗ ${providerUri.provider} failed to initialize`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
this.providerStatus.set(providerUri.provider, {
|
||||
name: providerUri.provider,
|
||||
available: false,
|
||||
lastChecked: new Date(),
|
||||
error: errorMessage
|
||||
})
|
||||
console.error(`OAuth2ProviderManager: ✗ ${providerUri.provider} error:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.initialized = successCount > 0
|
||||
|
||||
console.log(
|
||||
`OAuth2ProviderManager: Initialized ${successCount}/${wellKnownUris.length} providers`
|
||||
)
|
||||
|
||||
if (successCount === 0) {
|
||||
console.error('OAuth2ProviderManager: ERROR - No providers were successfully initialized')
|
||||
console.error(
|
||||
'OAuth2ProviderManager: Users will not be able to log in until at least one provider is available'
|
||||
)
|
||||
}
|
||||
|
||||
return this.initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic health checks for all providers
|
||||
*
|
||||
* @param intervalMs - Health check interval in milliseconds (default: 60000 = 1 minute)
|
||||
*/
|
||||
startHealthCheck(intervalMs: number = 60000): void {
|
||||
if (this.healthCheckInterval) {
|
||||
console.log('OAuth2ProviderManager: Health check already running')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`OAuth2ProviderManager: Starting health check (every ${intervalMs / 1000}s = ${intervalMs / 60000} minute(s))`
|
||||
)
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck()
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic health checks
|
||||
*/
|
||||
stopHealthCheck(): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval)
|
||||
this.healthCheckInterval = null
|
||||
console.log('OAuth2ProviderManager: Health check stopped')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform health check on all providers
|
||||
*
|
||||
* This checks if each provider's issuer endpoint is reachable
|
||||
*/
|
||||
private async performHealthCheck(): Promise<void> {
|
||||
console.log('OAuth2ProviderManager: Performing health check...')
|
||||
|
||||
const checkPromises: Promise<void>[] = []
|
||||
|
||||
for (const [providerName, client] of this.providers.entries()) {
|
||||
checkPromises.push(this.checkProviderHealth(providerName, client))
|
||||
}
|
||||
|
||||
await Promise.allSettled(checkPromises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check health of a single provider
|
||||
*
|
||||
* @param providerName - Name of the provider
|
||||
* @param client - OAuth2 client for the provider
|
||||
*/
|
||||
private async checkProviderHealth(
|
||||
providerName: string,
|
||||
client: OAuth2ClientWithConfig
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Try to fetch OIDC issuer endpoint to verify provider is reachable
|
||||
const endpoint = client.OIDCConfig?.issuer
|
||||
if (!endpoint) {
|
||||
throw new Error('No issuer endpoint configured')
|
||||
}
|
||||
|
||||
// Use HEAD request for efficiency
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'HEAD',
|
||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||
})
|
||||
|
||||
const isAvailable = response.ok
|
||||
this.providerStatus.set(providerName, {
|
||||
name: providerName,
|
||||
available: isAvailable,
|
||||
lastChecked: new Date(),
|
||||
error: isAvailable ? undefined : `HTTP ${response.status}`
|
||||
})
|
||||
|
||||
console.log(` ${providerName}: ${isAvailable ? '✓ healthy' : '✗ unhealthy'}`)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
this.providerStatus.set(providerName, {
|
||||
name: providerName,
|
||||
available: false,
|
||||
lastChecked: new Date(),
|
||||
error: errorMessage
|
||||
})
|
||||
console.log(` ${providerName}: ✗ unhealthy (${errorMessage})`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth2 client for a specific provider
|
||||
*
|
||||
* @param providerName - Provider name (e.g., "obp-oidc", "keycloak")
|
||||
* @returns OAuth2 client or undefined if not found
|
||||
*/
|
||||
getProvider(providerName: string): OAuth2ClientWithConfig | undefined {
|
||||
return this.providers.get(providerName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all available (initialized and healthy) provider names
|
||||
*
|
||||
* @returns Array of available provider names
|
||||
*/
|
||||
getAvailableProviders(): string[] {
|
||||
const available: string[] = []
|
||||
|
||||
for (const [name, status] of this.providerStatus.entries()) {
|
||||
if (status.available && this.providers.has(name)) {
|
||||
available.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status for all providers
|
||||
*
|
||||
* @returns Array of provider status objects
|
||||
*/
|
||||
getAllProviderStatus(): ProviderStatus[] {
|
||||
return Array.from(this.providerStatus.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status for a specific provider
|
||||
*
|
||||
* @param providerName - Provider name
|
||||
* @returns Provider status or undefined if not found
|
||||
*/
|
||||
getProviderStatus(providerName: string): ProviderStatus | undefined {
|
||||
return this.providerStatus.get(providerName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the manager has been initialized
|
||||
*
|
||||
* @returns True if at least one provider was successfully initialized
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of initialized providers
|
||||
*
|
||||
* @returns Number of providers in the map
|
||||
*/
|
||||
getProviderCount(): number {
|
||||
return this.providers.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of available (healthy) providers
|
||||
*
|
||||
* @returns Number of providers that are currently available
|
||||
*/
|
||||
getAvailableProviderCount(): number {
|
||||
return this.getAvailableProviders().length
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually retry initialization for a failed provider
|
||||
*
|
||||
* @param providerName - Provider name to retry
|
||||
* @returns True if initialization succeeded
|
||||
*/
|
||||
async retryProvider(providerName: string): Promise<boolean> {
|
||||
console.log(`OAuth2ProviderManager: Retrying initialization for ${providerName}`)
|
||||
|
||||
try {
|
||||
// Fetch well-known URIs again to get latest configuration
|
||||
const wellKnownUris = await this.fetchWellKnownUris()
|
||||
const providerUri = wellKnownUris.find((uri) => uri.provider === providerName)
|
||||
|
||||
if (!providerUri) {
|
||||
console.error(`OAuth2ProviderManager: Provider ${providerName} not found in OBP API`)
|
||||
return false
|
||||
}
|
||||
|
||||
const client = await this.factory.initializeProvider(providerUri)
|
||||
|
||||
if (client && client.isInitialized()) {
|
||||
this.providers.set(providerName, client)
|
||||
this.providerStatus.set(providerName, {
|
||||
name: providerName,
|
||||
available: true,
|
||||
lastChecked: new Date()
|
||||
})
|
||||
console.log(`OAuth2ProviderManager: ✓ ${providerName} retry successful`)
|
||||
return true
|
||||
} else {
|
||||
console.error(`OAuth2ProviderManager: ✗ ${providerName} retry failed`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`OAuth2ProviderManager: Error retrying ${providerName}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
130
server/types/oauth2.ts
Normal file
130
server/types/oauth2.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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/)
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Well-known URI from OBP API /obp/v[version]/well-known endpoint
|
||||
*/
|
||||
export interface WellKnownUri {
|
||||
provider: string // e.g., "obp-oidc", "keycloak"
|
||||
url: string // e.g., "http://localhost:9000/obp-oidc/.well-known/openid-configuration"
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from OBP API well-known endpoint
|
||||
*/
|
||||
export interface WellKnownResponse {
|
||||
well_known_uris: WellKnownUri[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider configuration strategy
|
||||
*/
|
||||
export interface ProviderStrategy {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectUri: string
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider status information
|
||||
*/
|
||||
export interface ProviderStatus {
|
||||
name: string
|
||||
available: boolean
|
||||
lastChecked: Date
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user