Phase 2: Integrate OAuth2 into application

Application Integration:
- Update server/app.ts to initialize OAuth2Service on startup
  * Conditional initialization based on VITE_USE_OAUTH2 flag
  * OIDC discovery document fetching
  * Comprehensive error handling and logging
  * Graceful fallback if OIDC provider unavailable

UserController Updates:
- Support dual authentication (OAuth 1.0a and OAuth2)
- OAuth2 user session detection and retrieval
- Automatic token refresh when access token expires
- Unified user data format for both auth methods
- Enhanced logout to clear both OAuth 1.0a and OAuth2 sessions
- Comprehensive logging for debugging

Features:
- Seamless switching between auth methods via feature flag
- Backward compatibility maintained
- Automatic token refresh before expiry
- Session cleanup on logout
- Error handling with fallback to empty response

Next phase: Update frontend components for OAuth2 login flow
This commit is contained in:
simonredfern 2025-11-29 19:54:58 +01:00
parent 86295f827a
commit b2df3a9791
2 changed files with 159 additions and 27 deletions

View File

@ -34,7 +34,8 @@ import express, { Application } from 'express'
import { useExpressServer, useContainer } from 'routing-controllers'
import { Container } from 'typedi'
import path from 'path'
import { execSync } from 'child_process';
import { execSync } from 'child_process'
import { OAuth2Service } from './services/OAuth2Service'
const port = 8085
const app: Application = express()
@ -106,6 +107,46 @@ if (app.get('env') === 'production') {
app.use(session(sessionObject))
useContainer(Container)
// Initialize OAuth2 Service
console.log(`--- OAuth2/OIDC setup -------------------------------------------`)
const useOAuth2 = process.env.VITE_USE_OAUTH2 === 'true'
console.log(`OAuth2/OIDC enabled: ${useOAuth2}`)
if (useOAuth2) {
const wellKnownUrl = process.env.VITE_OBP_OAUTH2_WELL_KNOWN_URL
if (!wellKnownUrl) {
console.warn('VITE_OBP_OAUTH2_WELL_KNOWN_URL not set. OAuth2 will not function.')
} else {
console.log(`OIDC Well-Known URL: ${wellKnownUrl}`)
// Get OAuth2Service from container
const oauth2Service = Container.get(OAuth2Service)
// Initialize OAuth2 service from OIDC discovery document
oauth2Service
.initializeFromWellKnown(wellKnownUrl)
.then(() => {
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) => {
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')
})
}
} else {
console.log('OAuth2/OIDC is disabled. Using OAuth 1.0a authentication.')
console.log('To enable OAuth2, set VITE_USE_OAUTH2=true in environment')
}
console.log(`-----------------------------------------------------------------`)
const routePrefix = '/api'
const server = useExpressServer(app, {
@ -121,31 +162,31 @@ console.log(
)
// Get commit ID
export let commitId = '';
export let commitId = ''
try {
// Try to get the commit ID
commitId = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
console.log('Current Commit ID:', commitId);
// Try to get the commit ID
commitId = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim()
console.log('Current Commit ID:', commitId)
} catch (error) {
// Log the error but do not terminate the process
console.error('Warning: Failed to retrieve the commit ID. Proceeding without it.');
console.error('Error details:', error.message);
commitId = 'unknown'; // Assign a fallback value
// Log the error but do not terminate the process
console.error('Warning: Failed to retrieve the commit ID. Proceeding without it.')
console.error('Error details:', error.message)
commitId = 'unknown' // Assign a fallback value
}
// Continue execution with or without a valid commit ID
console.log('Execution continues with commitId:', commitId);
console.log('Execution continues with commitId:', commitId)
// Error Handling to Shut Down the App
server.on('error', (err) => {
redisClient.disconnect();
redisClient.disconnect()
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use.`);
process.exit(1);
// Shut down the app
console.error(`Port ${port} is already in use.`)
process.exit(1)
// Shut down the app
} else {
console.error('An error occurred:', err);
console.error('An error occurred:', err)
}
});
})
export default app

View File

@ -31,14 +31,18 @@ import OBPClientService from '../services/OBPClientService'
import OauthInjectedService from '../services/OauthInjectedService'
import { Service } from 'typedi'
import superagent from 'superagent'
import { OAuth2Service } from '../services/OAuth2Service'
@Service()
@Controller('/user')
export class UserController {
private obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
private useOAuth2 = process.env.VITE_USE_OAUTH2 === 'true'
constructor(
private obpClientService: OBPClientService,
private oauthInjectedService: OauthInjectedService
private oauthInjectedService: OauthInjectedService,
private oauth2Service: OAuth2Service
) {}
@Get('/logoff')
async logout(
@ -46,20 +50,41 @@ export class UserController {
@Req() request: Request,
@Res() response: Response
): Response {
console.log('UserController: Logging out user')
// Clear OAuth 1.0a session data
this.oauthInjectedService.requestTokenKey = undefined
this.oauthInjectedService.requestTokenSecret = undefined
session['clientConfig'] = undefined
if (request.query.redirect) {
response.redirect(request.query.redirect as string)
} else {
if(!this.obpExplorerHome) {
console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`)
// Clear OAuth2 session data
delete session['oauth2_access_token']
delete session['oauth2_refresh_token']
delete session['oauth2_id_token']
delete session['oauth2_token_type']
delete session['oauth2_expires_in']
delete session['oauth2_token_timestamp']
delete session['oauth2_user_info']
delete session['oauth2_user']
// Destroy the session completely
session.destroy((err: any) => {
if (err) {
console.error('UserController: Error destroying session:', err)
} else {
console.log('UserController: Session destroyed successfully')
}
response.redirect(this.obpExplorerHome)
})
const redirectPage = (request.query.redirect as string) || this.obpExplorerHome || '/'
if (!this.obpExplorerHome) {
console.error(`VITE_OBP_API_EXPLORER_HOST: ${this.obpExplorerHome}`)
}
console.log('UserController: Redirecting to:', redirectPage)
response.redirect(redirectPage)
return response
}
@ -69,10 +94,76 @@ export class UserController {
@Req() request: Request,
@Res() response: Response
): Response {
console.log('UserController: Getting current user')
console.log(' OAuth2 enabled:', this.useOAuth2)
// Check OAuth2 session first (if OAuth2 is enabled)
if (this.useOAuth2 && session['oauth2_user']) {
console.log('UserController: Returning OAuth2 user info')
const oauth2User = session['oauth2_user']
// Check if access token is expired and needs refresh
const accessToken = session['oauth2_access_token']
const refreshToken = session['oauth2_refresh_token']
if (accessToken && this.oauth2Service.isTokenExpired(accessToken)) {
console.log('UserController: Access token expired')
if (refreshToken) {
console.log('UserController: Attempting token refresh')
try {
const newTokens = await this.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
console.log('UserController: Token refresh successful')
} catch (error) {
console.error('UserController: Token refresh failed:', error)
// Return empty object to indicate user needs to re-authenticate
return response.json({})
}
} else {
console.log('UserController: No refresh token available, user needs to re-authenticate')
return response.json({})
}
}
// Return user info in format compatible with frontend
return response.json({
user_id: oauth2User.sub,
username: oauth2User.username,
email: oauth2User.email,
email_verified: oauth2User.email_verified,
name: oauth2User.name,
given_name: oauth2User.given_name,
family_name: oauth2User.family_name,
provider: oauth2User.provider || 'oauth2'
})
}
// Fall back to OAuth 1.0a
console.log('UserController: Checking OAuth 1.0a session')
const oauthConfig = session['clientConfig']
if (!oauthConfig) {
console.log('UserController: No authentication session found')
return response.json({})
}
console.log('UserController: Returning OAuth 1.0a user info')
const version = this.obpClientService.getOBPVersion()
return response.json(
await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig)
)
try {
const userData = await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig)
return response.json(userData)
} catch (error) {
console.error('UserController: Failed to get user from OBP API:', error)
return response.json({})
}
}
}