mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
OIDC checking / retrying
This commit is contained in:
parent
ca923f7b5a
commit
cd9ba264ec
@ -147,21 +147,36 @@ let instance: any
|
||||
// Get OAuth2Service from container
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
// Initialize OAuth2 service from OIDC discovery document (await it!)
|
||||
try {
|
||||
await oauth2Service.initializeFromWellKnown(wellKnownUrl)
|
||||
// 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 OAuth2 initialization (will retry indefinitely with exponential backoff)...'
|
||||
)
|
||||
const success = await oauth2Service.initializeWithRetry(wellKnownUrl, maxRetries, initialDelay)
|
||||
|
||||
if (success) {
|
||||
console.log('OAuth2Service: 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('OAuth2/OIDC ready for authentication')
|
||||
} catch (error: any) {
|
||||
console.error('OAuth2Service: Initialization failed:', error.message)
|
||||
console.error('OAuth2/OIDC authentication will not be available')
|
||||
console.error('Please check:')
|
||||
console.error(' 1. OBP-OIDC server is running')
|
||||
console.error(' 2. VITE_OBP_OAUTH2_WELL_KNOWN_URL is correct')
|
||||
console.error(' 3. Network connectivity to OIDC provider')
|
||||
console.warn('Server will start but OAuth2 authentication will fail.')
|
||||
} else {
|
||||
console.error('OAuth2Service: 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 OAuth2`)
|
||||
console.warn('WARNING: Login will be unavailable until OIDC server is reachable')
|
||||
console.warn('WARNING: Starting health check to reconnect automatically...')
|
||||
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) // Start with 1 second, then exponential backoff
|
||||
}
|
||||
}
|
||||
console.log(`-----------------------------------------------------------------`)
|
||||
|
||||
@ -32,6 +32,7 @@ import OBPClientService from '../services/OBPClientService.js'
|
||||
import { Service, Container } from 'typedi'
|
||||
import { OAuthConfig } from 'obp-typescript'
|
||||
import { commitId } from '../app.js'
|
||||
import { OAuth2Service } from '../services/OAuth2Service.js'
|
||||
import {
|
||||
RESOURCE_DOCS_API_VERSION,
|
||||
MESSAGE_DOCS_API_VERSION,
|
||||
@ -139,4 +140,75 @@ export class StatusController {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/oauth2')
|
||||
getOAuth2Status(@Res() response: Response): Response {
|
||||
try {
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
const isInitialized = oauth2Service.isInitialized()
|
||||
const oidcConfig = oauth2Service.getOIDCConfiguration()
|
||||
const healthCheckActive = oauth2Service.isHealthCheckActive()
|
||||
const healthCheckAttempts = oauth2Service.getHealthCheckAttempts()
|
||||
|
||||
return response.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) {
|
||||
return response.status(500).json({
|
||||
available: false,
|
||||
message: 'Error checking OAuth2 status',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/oauth2/reconnect')
|
||||
async reconnectOAuth2(@Res() response: Response): Promise<Response> {
|
||||
try {
|
||||
const oauth2Service = Container.get(OAuth2Service)
|
||||
|
||||
if (oauth2Service.isInitialized()) {
|
||||
return response.json({
|
||||
success: true,
|
||||
message: 'OAuth2 is already connected',
|
||||
alreadyConnected: true
|
||||
})
|
||||
}
|
||||
|
||||
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
|
||||
if (!wellKnownUrl) {
|
||||
return response.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!')
|
||||
return response.json({
|
||||
success: true,
|
||||
message: 'OAuth2 reconnection successful',
|
||||
issuer: oauth2Service.getOIDCConfiguration()?.issuer || null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Manual OAuth2 reconnection failed:', error)
|
||||
return response.status(500).json({
|
||||
success: false,
|
||||
message: 'OAuth2 reconnection failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,6 +122,10 @@ export class OAuth2Service {
|
||||
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
|
||||
@ -161,6 +165,9 @@ export class OAuth2Service {
|
||||
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)
|
||||
|
||||
@ -203,6 +210,151 @@ export class OAuth2Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic health check to reconnect if OIDC server becomes available
|
||||
* Uses exponential backoff: 1min, 2min, 4min, capped at 4min
|
||||
*
|
||||
* @param {number} initialIntervalMs - Initial interval in milliseconds (default: 1000 = 1 second)
|
||||
*
|
||||
* @example
|
||||
* oauth2Service.startHealthCheck(1000) // Start checking at 1 second, then exponential backoff
|
||||
*/
|
||||
startHealthCheck(initialIntervalMs: number = 1000): 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.initialized && this.wellKnownUrl) {
|
||||
// Calculate delay with exponential backoff, capped at 4 minutes
|
||||
const 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)`
|
||||
|
||||
console.log(
|
||||
`OAuth2Service: Health check scheduled in ${delayDisplay} (attempt ${this.healthCheckAttempts + 1})`
|
||||
)
|
||||
|
||||
this.healthCheckInterval = setTimeout(async () => {
|
||||
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!')
|
||||
// Stop health check once reconnected
|
||||
this.stopHealthCheck()
|
||||
} catch (error) {
|
||||
this.healthCheckAttempts++
|
||||
// Schedule next check with longer interval
|
||||
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
|
||||
*
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, watchEffect, onMounted, computed } from 'vue'
|
||||
import { ref, inject, watchEffect, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { OBP_API_DEFAULT_RESOURCE_DOC_VERSION, getCurrentUser } from '../obp'
|
||||
import { getOBPAPIVersions } from '../obp/api-version'
|
||||
@ -74,10 +74,35 @@ const sortedVersions = computed(() => {
|
||||
|
||||
const isShowLoginButton = ref(true)
|
||||
const isShowLogOffButton = ref(false)
|
||||
const oauth2Available = ref(true) // Assume available initially
|
||||
const oauth2StatusMessage = ref('')
|
||||
const logo = ref(logoSource)
|
||||
const headerLinksHoverColor = ref(headerLinksHoverColorSetting)
|
||||
const headerLinksBackgroundColor = ref(headerLinksBackgroundColorSetting)
|
||||
|
||||
// Check OAuth2 availability
|
||||
let oauth2CheckInterval: number | null = null
|
||||
|
||||
async function checkOAuth2Availability() {
|
||||
try {
|
||||
const response = await fetch('/api/status/oauth2')
|
||||
const data = await response.json()
|
||||
oauth2Available.value = data.available
|
||||
oauth2StatusMessage.value = data.message || ''
|
||||
|
||||
if (data.available && oauth2CheckInterval) {
|
||||
// Stop polling once OAuth2 is available
|
||||
clearInterval(oauth2CheckInterval)
|
||||
oauth2CheckInterval = null
|
||||
console.log('OAuth2 is now available, stopped polling')
|
||||
}
|
||||
} catch (error) {
|
||||
oauth2Available.value = false
|
||||
oauth2StatusMessage.value = 'Failed to check OAuth2 status'
|
||||
console.error('Error checking OAuth2 status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const clearActiveTab = () => {
|
||||
const activeLinks = document.querySelectorAll<HTMLElement>('.router-link')
|
||||
for (const active of activeLinks) {
|
||||
@ -125,6 +150,15 @@ const handleMore = (command: string) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Initial OAuth2 availability check
|
||||
await checkOAuth2Availability()
|
||||
|
||||
// If OAuth2 is not available, poll every 30 seconds
|
||||
if (!oauth2Available.value) {
|
||||
console.log('OAuth2 not available, starting periodic check...')
|
||||
oauth2CheckInterval = window.setInterval(checkOAuth2Availability, 30000)
|
||||
}
|
||||
|
||||
const currentUser = await getCurrentUser()
|
||||
const currentResponseKeys = Object.keys(currentUser)
|
||||
if (currentResponseKeys.includes('username')) {
|
||||
@ -137,6 +171,14 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up polling interval
|
||||
if (oauth2CheckInterval) {
|
||||
clearInterval(oauth2CheckInterval)
|
||||
oauth2CheckInterval = null
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const routeName = typeof route.name === 'string' ? route.name : null
|
||||
if (routeName && route.params && !route.params.id) {
|
||||
@ -202,7 +244,12 @@ const getCurrentPath = () => {
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>-->
|
||||
<a v-bind:href="'/api/oauth2/connect?redirect='+ encodeURIComponent(getCurrentPath())" v-show="isShowLoginButton" class="login-button router-link" id="login">
|
||||
<el-tooltip v-if="isShowLoginButton && !oauth2Available" :content="oauth2StatusMessage || 'OAuth2 server not available'" placement="bottom">
|
||||
<button disabled class="login-button-disabled router-link" id="login">
|
||||
{{ $t('header.login') }}
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<a v-else-if="isShowLoginButton && oauth2Available" v-bind:href="'/api/oauth2/connect?redirect='+ encodeURIComponent(getCurrentPath())" class="login-button router-link" id="login">
|
||||
{{ $t('header.login') }}
|
||||
</a>
|
||||
<span v-show="isShowLogOffButton" class="login-user">{{ loginUsername }}</span>
|
||||
@ -280,6 +327,19 @@ a.logoff-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.login-button-disabled {
|
||||
margin: 5px;
|
||||
padding: 9px;
|
||||
color: #999999;
|
||||
background-color: #e0e0e0;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 8px;
|
||||
cursor: not-allowed;
|
||||
font-family: 'Roboto';
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.login-button:hover,
|
||||
.logoff-button:hover {
|
||||
color: #39455f;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user