added /debug/oidc

This commit is contained in:
simonredfern 2026-01-07 23:49:47 +01:00
parent 99c4d4d22c
commit 10e14a2738
6 changed files with 748 additions and 1 deletions

2
components.d.ts vendored
View File

@ -38,6 +38,8 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
GlossarySearchNav: typeof import('./src/components/GlossarySearchNav.vue')['default']

View File

@ -218,4 +218,265 @@ router.get('/status/providers', (req: Request, res: Response) => {
}
})
/**
* GET /status/oidc-debug
* Get detailed OIDC discovery information for debugging
* Shows the full discovery process and configuration for all providers
*/
router.get('/status/oidc-debug', async (req: Request, res: Response) => {
try {
console.log('OIDC Debug: Starting detailed discovery process...')
// Step 1: Get OBP API well-known endpoint info
const obpApiHost = obpClientService.getOBPClientConfig().baseUri
const wellKnownEndpoint = `${obpApiHost}/obp/v5.1.0/well-known`
const step1 = {
description: 'Discovery of OIDC providers from OBP API',
endpoint: wellKnownEndpoint,
success: false,
response: null as any,
error: null as string | null,
providers: [] as any[]
}
try {
console.log(`OIDC Debug: Fetching from ${wellKnownEndpoint}`)
const wellKnownResponse = await obpClientService.get('/obp/v5.1.0/well-known', null)
step1.response = wellKnownResponse
step1.success = !!(wellKnownResponse && wellKnownResponse.well_known_uris)
step1.providers = wellKnownResponse.well_known_uris || []
console.log(`OIDC Debug: Found ${step1.providers.length} providers`)
} catch (error) {
step1.error = error instanceof Error ? error.message : 'Unknown error'
console.error('OIDC Debug: Error fetching OBP well-known:', error)
}
// Step 2: For each provider, fetch their OIDC configuration
const providerDetails = []
for (const provider of step1.providers) {
console.log(`OIDC Debug: Fetching OIDC config for ${provider.provider}`)
const detail = {
providerName: provider.provider,
wellKnownUrl: provider.url,
success: false,
oidcConfiguration: null as any,
error: null as string | null,
endpoints: {
authorization: null as string | null,
token: null as string | null,
userinfo: null as string | null,
jwks: null as string | null
},
issuer: null as string | null,
supportedFeatures: {
pkce: false,
scopes: [] as string[],
responseTypes: [] as string[],
grantTypes: [] as string[]
}
}
try {
const response = await fetch(provider.url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const config = await response.json()
detail.oidcConfiguration = config
detail.success = true
detail.issuer = config.issuer
// Extract endpoints
detail.endpoints.authorization = config.authorization_endpoint
detail.endpoints.token = config.token_endpoint
detail.endpoints.userinfo = config.userinfo_endpoint
detail.endpoints.jwks = config.jwks_uri
// Extract supported features
detail.supportedFeatures.pkce =
config.code_challenge_methods_supported?.includes('S256') || false
detail.supportedFeatures.scopes = config.scopes_supported || []
detail.supportedFeatures.responseTypes = config.response_types_supported || []
detail.supportedFeatures.grantTypes = config.grant_types_supported || []
console.log(`OIDC Debug: Successfully fetched config for ${provider.provider}`)
} catch (error) {
detail.error = error instanceof Error ? error.message : 'Unknown error'
console.error(`OIDC Debug: Error fetching config for ${provider.provider}:`, error)
}
providerDetails.push(detail)
}
// Step 3: Get current provider status from manager
const currentStatus = providerManager.getAllProviderStatus()
const availableProviders = providerManager.getAvailableProviders()
// Step 4: Get environment configuration
const maskCredential = (value: string | undefined): string => {
if (!value || value.length < 6) {
return value ? '***masked***' : 'not configured'
}
return `${value.substring(0, 2)}...${value.substring(value.length - 2)}`
}
const envConfig = {
obpOidc: {
clientId: maskCredential(process.env.VITE_OBP_OIDC_CLIENT_ID),
clientSecret: process.env.VITE_OBP_OIDC_CLIENT_SECRET ? 'configured' : 'not configured',
configured: !!(
process.env.VITE_OBP_OIDC_CLIENT_ID && process.env.VITE_OBP_OIDC_CLIENT_SECRET
)
},
keycloak: {
clientId: maskCredential(process.env.VITE_KEYCLOAK_CLIENT_ID),
clientSecret: process.env.VITE_KEYCLOAK_CLIENT_SECRET ? 'configured' : 'not configured',
configured: !!(
process.env.VITE_KEYCLOAK_CLIENT_ID && process.env.VITE_KEYCLOAK_CLIENT_SECRET
)
},
google: {
clientId: maskCredential(process.env.VITE_GOOGLE_CLIENT_ID),
clientSecret: process.env.VITE_GOOGLE_CLIENT_SECRET ? 'configured' : 'not configured',
configured: !!(process.env.VITE_GOOGLE_CLIENT_ID && process.env.VITE_GOOGLE_CLIENT_SECRET)
},
github: {
clientId: maskCredential(process.env.VITE_GITHUB_CLIENT_ID),
clientSecret: process.env.VITE_GITHUB_CLIENT_SECRET ? 'configured' : 'not configured',
configured: !!(process.env.VITE_GITHUB_CLIENT_ID && process.env.VITE_GITHUB_CLIENT_SECRET)
},
custom: {
providerName: process.env.VITE_CUSTOM_OIDC_PROVIDER_NAME || 'not configured',
clientId: maskCredential(process.env.VITE_CUSTOM_OIDC_CLIENT_ID),
clientSecret: process.env.VITE_CUSTOM_OIDC_CLIENT_SECRET ? 'configured' : 'not configured',
configured: !!(
process.env.VITE_CUSTOM_OIDC_CLIENT_ID && process.env.VITE_CUSTOM_OIDC_CLIENT_SECRET
)
},
shared: {
redirectUrl: process.env.VITE_OAUTH2_REDIRECT_URL || 'not configured',
obpApiHost: process.env.VITE_OBP_API_HOST || 'not configured'
}
}
// Compile summary
const summary = {
timestamp: new Date().toISOString(),
obpApiReachable: step1.success,
totalProvidersDiscovered: step1.providers.length,
successfulConfigurations: providerDetails.filter((p) => p.success).length,
failedConfigurations: providerDetails.filter((p) => !p.success).length,
currentlyAvailable: availableProviders.length,
configuredInEnvironment: Object.values(envConfig).filter(
(c) => typeof c === 'object' && 'configured' in c && c.configured
).length
}
res.json({
summary,
discoveryProcess: {
step1_obpApiDiscovery: step1,
step2_providerConfigurations: providerDetails,
step3_currentStatus: currentStatus
},
environment: envConfig,
recommendations: generateRecommendations(step1, providerDetails, envConfig, currentStatus),
note: 'This debug information shows the complete OIDC discovery process for troubleshooting'
})
console.log('OIDC Debug: Response sent successfully')
} catch (error) {
console.error('OIDC Debug: Error generating debug info:', error)
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined
})
}
})
/**
* Generate troubleshooting recommendations based on the discovery results
*/
function generateRecommendations(
step1: any,
providerDetails: any[],
envConfig: any,
currentStatus: any[]
): string[] {
const recommendations: string[] = []
// Check if OBP API is reachable
if (!step1.success) {
recommendations.push(
'❌ OBP API well-known endpoint is not reachable. Check that VITE_OBP_API_HOST is correct and the API server is running.'
)
recommendations.push(` Current endpoint: ${step1.endpoint}`)
if (step1.error) {
recommendations.push(` Error: ${step1.error}`)
}
} else {
recommendations.push('✅ OBP API well-known endpoint is reachable')
}
// Check if any providers were discovered
if (step1.providers.length === 0) {
recommendations.push(
'⚠️ No OIDC providers found in OBP API response. The OBP API may not have any providers configured.'
)
} else {
recommendations.push(`✅ Found ${step1.providers.length} provider(s) from OBP API`)
}
// Check each provider's configuration
providerDetails.forEach((provider) => {
if (!provider.success) {
recommendations.push(
`❌ Provider '${provider.providerName}' OIDC configuration failed to load`
)
recommendations.push(` Well-known URL: ${provider.wellKnownUrl}`)
if (provider.error) {
recommendations.push(` Error: ${provider.error}`)
}
recommendations.push(
` Check that the provider's well-known endpoint is accessible and returning valid JSON`
)
} else {
recommendations.push(
`✅ Provider '${provider.providerName}' OIDC configuration loaded successfully`
)
}
// Check if provider has environment credentials
const envKey = provider.providerName.replace('-', '')
const providerEnv = envConfig[envKey] || envConfig[provider.providerName]
if (providerEnv && !providerEnv.configured) {
recommendations.push(
`⚠️ Provider '${provider.providerName}' is missing environment credentials`
)
const upperName = provider.providerName.toUpperCase().replace('-', '_')
recommendations.push(` Set VITE_${upperName}_CLIENT_ID and VITE_${upperName}_CLIENT_SECRET`)
}
})
// Check current provider status
currentStatus.forEach((status) => {
if (!status.available) {
recommendations.push(`⚠️ Provider '${status.name}' is currently unavailable`)
if (status.error) {
recommendations.push(` Error: ${status.error}`)
}
}
})
if (recommendations.length === 0) {
recommendations.push('✅ All checks passed! OIDC configuration looks good.')
}
return recommendations
}
export default router

View File

@ -52,6 +52,9 @@ const logoffurl = ref('')
const obpApiVersions = ref(inject(obpApiActiveVersionsKey) || [])
const obpMessageDocs = ref(Object.keys(inject(obpGroupedMessageDocsKey) || {}))
// Debug menu items
const debugMenuItems = ref(['/debug/providers-status', '/debug/oidc'])
// Split versions into main and other
const mainVersions = ['BGv1.3', 'OBPv5.1.0', 'OBPv6.0.0', 'UKv3.1', 'dynamic-endpoints', 'dynamic-entities', 'OBPdynamic-endpoint', 'OBPdynamic-entity']
const sortedVersions = computed(() => {
@ -205,6 +208,9 @@ const handleMore = (command: string) => {
} else if (command.includes('_')) {
console.log('Navigating to message docs:', command)
router.push({ name: 'message-docs', params: { id: command } })
} else if (command.startsWith('/debug/')) {
console.log('Navigating to debug page:', command)
router.push(command)
} else {
console.log('Navigating to resource docs:', `/resource-docs/${command}`)
console.log('Current route:', route.path)
@ -291,6 +297,15 @@ const getCurrentPath = () => {
:background-color="headerLinksBackgroundColor"
@select="handleMore"
/>
<SvelteDropdown
class="menu-right"
id="header-nav-debug"
label="Debug"
:items="debugMenuItems"
:hover-color="headerLinksHoverColor"
:background-color="headerLinksBackgroundColor"
@select="handleMore"
/>
<!--<span class="el-dropdown-link">
<RouterLink class="router-link" id="header-nav-spaces" to="/spaces">{{
$t('header.spaces')
@ -494,7 +509,8 @@ button.login-button-disabled {
/* Custom dropdown containers */
#header-nav-versions,
#header-nav-message-docs {
#header-nav-message-docs,
#header-nav-debug {
display: inline-block;
vertical-align: middle;
}

View File

@ -40,6 +40,7 @@ import APIServerStatusView from '../views/APIServerStatusView.vue'
import { isServerUp, OBP_API_DEFAULT_RESOURCE_DOC_VERSION } from '../obp'
import MessageDocsContent from '@/components/CodeBlock.vue'
import ProvidersStatusView from '../views/ProvidersStatusView.vue'
import OIDCDebugView from '../views/OIDCDebugView.vue'
export default async function router(): Promise<any> {
const isServerActive = await isServerUp()
@ -60,6 +61,11 @@ export default async function router(): Promise<any> {
name: 'providers-status',
component: ProvidersStatusView
},
{
path: '/debug/oidc',
name: 'oidc-debug',
component: OIDCDebugView
},
{
path: '/glossary',
name: 'glossary',

455
src/views/OIDCDebugView.vue Normal file
View File

@ -0,0 +1,455 @@
<!--
Open Bank Project - API Explorer II
Copyright (C) 2023-2025, 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/)
-->
<template>
<div class="oidc-debug-view">
<div class="header">
<h1>OIDC Provider Discovery</h1>
<el-button type="primary" @click="refreshDebugInfo" :loading="loading">
<el-icon><Refresh /></el-icon>
Refresh
</el-button>
</div>
<el-alert type="info" :closable="false" style="margin-bottom: 20px">
<p><strong>Step 1:</strong> API Explorer discovers providers from OBP API: <code>{{ obpWellKnownUrl }}</code></p>
<p><strong>Step 2:</strong> For each provider, fetch their OIDC configuration from their .well-known URL</p>
</el-alert>
<div v-if="loading" v-loading="loading" class="loading-container">
<p>Loading provider information...</p>
</div>
<div v-else-if="error" class="error-container">
<el-alert type="error" :closable="false">
<p><strong>Error:</strong> {{ error }}</p>
</el-alert>
</div>
<div v-else-if="debugInfo">
<!-- OBP API Discovery Status -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span>Step 1: OBP API Provider Discovery</span>
<el-tag :type="debugInfo.discoveryProcess.step1_obpApiDiscovery.success ? 'success' : 'danger'">
{{ debugInfo.discoveryProcess.step1_obpApiDiscovery.success ? 'Success' : 'Failed' }}
</el-tag>
</div>
</template>
<div class="section-content">
<div class="info-row">
<label>OBP API Endpoint:</label>
<code>{{ debugInfo.discoveryProcess.step1_obpApiDiscovery.endpoint }}</code>
</div>
<div class="info-row">
<label>Providers Discovered:</label>
<span>{{ debugInfo.discoveryProcess.step1_obpApiDiscovery.providers.length }}</span>
</div>
<div v-if="debugInfo.discoveryProcess.step1_obpApiDiscovery.error" class="error-message">
<strong>Error:</strong> {{ debugInfo.discoveryProcess.step1_obpApiDiscovery.error }}
</div>
</div>
</el-card>
<!-- Provider List -->
<el-card class="section-card" shadow="hover">
<template #header>
<div class="card-header">
<span>Step 2: Provider OIDC Configurations</span>
<el-tag type="info">{{ debugInfo.discoveryProcess.step2_providerConfigurations.length }} Provider(s)</el-tag>
</div>
</template>
<div class="section-content">
<div v-if="debugInfo.discoveryProcess.step2_providerConfigurations.length === 0" class="no-data">
<el-empty description="No providers found" />
</div>
<div v-else class="provider-list">
<div
v-for="provider in debugInfo.discoveryProcess.step2_providerConfigurations"
:key="provider.providerName"
class="provider-item"
:class="{ 'provider-success': provider.success, 'provider-error': !provider.success }"
>
<div class="provider-header">
<h3>{{ provider.providerName }}</h3>
<el-tag :type="provider.success ? 'success' : 'danger'" size="small">
{{ provider.success ? 'Success' : 'Failed' }}
</el-tag>
</div>
<div class="provider-details">
<div class="detail-row">
<label>Well-Known URL:</label>
<div class="url-display">
<code>{{ provider.wellKnownUrl }}</code>
<el-button size="small" @click="copyToClipboard(provider.wellKnownUrl)" text>
<el-icon><CopyDocument /></el-icon>
Copy
</el-button>
<el-button size="small" @click="testEndpoint(provider.wellKnownUrl)" text>
<el-icon><Link /></el-icon>
Test
</el-button>
</div>
</div>
<div v-if="provider.error" class="error-message">
<strong>Error:</strong> {{ provider.error }}
</div>
<div v-if="provider.success" class="endpoints-section">
<label>OIDC Endpoints:</label>
<div class="endpoints-list">
<div class="endpoint-row">
<span class="endpoint-label">Issuer:</span>
<code>{{ provider.issuer }}</code>
</div>
<div class="endpoint-row">
<span class="endpoint-label">Authorization:</span>
<code>{{ provider.endpoints.authorization }}</code>
</div>
<div class="endpoint-row">
<span class="endpoint-label">Token:</span>
<code>{{ provider.endpoints.token }}</code>
</div>
<div class="endpoint-row">
<span class="endpoint-label">UserInfo:</span>
<code>{{ provider.endpoints.userinfo }}</code>
</div>
<div class="endpoint-row">
<span class="endpoint-label">JWKS:</span>
<code>{{ provider.endpoints.jwks }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, CopyDocument, Link } from '@element-plus/icons-vue'
interface DebugInfo {
discoveryProcess: {
step1_obpApiDiscovery: {
endpoint: string
success: boolean
error: string | null
providers: Array<{ provider: string; url: string }>
}
step2_providerConfigurations: Array<{
providerName: string
wellKnownUrl: string
success: boolean
error: string | null
endpoints: {
authorization: string | null
token: string | null
userinfo: string | null
jwks: string | null
}
issuer: string | null
}>
}
}
const loading = ref(true)
const error = ref<string | null>(null)
const debugInfo = ref<DebugInfo | null>(null)
const obpWellKnownUrl = computed(() => {
return debugInfo.value?.discoveryProcess.step1_obpApiDiscovery.endpoint || 'Loading...'
})
const fetchDebugInfo = async () => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/status/oidc-debug')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
debugInfo.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
ElMessage.error('Failed to load provider information')
} finally {
loading.value = false
}
}
const refreshDebugInfo = async () => {
ElMessage.info('Refreshing provider information...')
await fetchDebugInfo()
ElMessage.success('Provider information refreshed')
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
ElMessage.success('Copied to clipboard')
} catch (err) {
ElMessage.error('Failed to copy to clipboard')
}
}
const testEndpoint = (url: string) => {
window.open(url, '_blank')
}
onMounted(() => {
fetchDebugInfo()
})
</script>
<style scoped>
.oidc-debug-view {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h1 {
font-size: 28px;
color: #303133;
margin: 0;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 60px;
font-size: 16px;
color: #909399;
}
.error-container {
margin: 20px 0;
}
.section-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 16px;
}
.section-content {
padding: 10px 0;
}
.info-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.info-row label {
font-weight: 500;
color: #606266;
min-width: 180px;
}
.info-row code {
background: #f4f4f5;
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.no-data {
padding: 40px;
text-align: center;
}
.provider-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.provider-item {
padding: 20px;
border: 2px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.provider-success {
border-color: #67c23a;
background: #f0f9ff;
}
.provider-error {
border-color: #f56c6c;
background: #fef0f0;
}
.provider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
}
.provider-header h3 {
margin: 0;
font-size: 20px;
color: #303133;
}
.provider-details {
display: flex;
flex-direction: column;
gap: 15px;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row label {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.url-display {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.url-display code {
flex: 1;
font-family: 'Courier New', monospace;
font-size: 13px;
word-break: break-all;
}
.error-message {
padding: 10px;
background: #fef0f0;
border-left: 4px solid #f56c6c;
border-radius: 4px;
color: #f56c6c;
font-size: 14px;
}
.endpoints-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.endpoints-section label {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.endpoints-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 15px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.endpoint-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 10px;
align-items: start;
}
.endpoint-label {
font-weight: 500;
color: #909399;
font-size: 13px;
}
.endpoint-row code {
background: #f4f4f5;
padding: 4px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.info-row {
flex-direction: column;
align-items: flex-start;
}
.endpoint-row {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -31,6 +31,13 @@
<el-alert type="info" :closable="false" style="margin-bottom: 20px">
<p>This page shows which OAuth2/OIDC identity providers are configured and available for login.</p>
<p><strong>Note:</strong> Client secrets are masked for security.</p>
<p>
<strong>Need more details?</strong> Visit the
<router-link to="/debug/oidc" style="color: #409eff; text-decoration: underline;">
OIDC Debug Page
</router-link>
for detailed discovery process information.
</p>
</el-alert>
<div v-if="loading" v-loading="loading" class="loading-container">