From 80903243dfee61d7d2c16b5674a531f886b96f71 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sun, 20 Jul 2025 08:46:47 +0200 Subject: [PATCH 1/3] add .well-known routes for openid connect protocol, show jwks.json and openid-configuration --- backend/src/config/const.ts | 1 + backend/src/openIDConnect/index.ts | 55 +++++++++++++++++++ backend/src/server/createServer.ts | 5 ++ .../sites-available/gradido.conf.ssl.template | 18 ++++++ .../sites-available/gradido.conf.template | 18 ++++++ nginx/gradido.conf | 13 +++++ 6 files changed, 110 insertions(+) create mode 100644 backend/src/openIDConnect/index.ts diff --git a/backend/src/config/const.ts b/backend/src/config/const.ts index 68bd124a8..de9f094b2 100644 --- a/backend/src/config/const.ts +++ b/backend/src/config/const.ts @@ -1 +1,2 @@ export const LOG4JS_BASE_CATEGORY_NAME = 'backend' +export const FRONTEND_LOGIN_ROUTE = 'login' \ No newline at end of file diff --git a/backend/src/openIDConnect/index.ts b/backend/src/openIDConnect/index.ts new file mode 100644 index 000000000..7bfdde040 --- /dev/null +++ b/backend/src/openIDConnect/index.ts @@ -0,0 +1,55 @@ +import { CONFIG } from '@/config' +import { FRONTEND_LOGIN_ROUTE, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getHomeCommunity } from 'database' +import { importSPKI, exportJWK } from 'jose' +import { createHash } from 'crypto' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.openIDConnect`) +const defaultErrorForCaller = 'Internal Server Error' + +export const openidConfiguration = async (req: any, res: any): Promise => { + res.setHeader('Content-Type', 'application/json') + res.status(200).json({ + issuer: new URL(FRONTEND_LOGIN_ROUTE, CONFIG.COMMUNITY_URL).toString(), + jwks_uri: new URL('/.well-known/jwks.json', CONFIG.COMMUNITY_URL).toString(), + }) +} + +export const jwks = async (req: any, res: any): Promise => { + const homeCommunity = await getHomeCommunity() + if (!homeCommunity) { + logger.error('HomeCommunity not found') + throw new Error(defaultErrorForCaller) + } + if (!homeCommunity.publicJwtKey) { + logger.error('HomeCommunity publicJwtKey not found') + throw new Error(defaultErrorForCaller) + } + try { + const publicKey = await importSPKI(homeCommunity.publicJwtKey, 'RS256') + const jwk = await exportJWK(publicKey) + + // Optional: calculate Key ID (z.B. SHA-256 Fingerprint) + const kid = createHash('sha256') + .update(homeCommunity.publicJwtKey) + .digest('base64url') + + const jwks = { + keys: [ + { + ...jwk, + alg: 'RS256', + use: 'sig', + kid, + }, + ], + } + res.setHeader('Cache-Control', 'public, max-age=3600, immutable') + res.setHeader('Content-Type', 'application/json') + res.status(200).json(jwks) + } catch (err) { + logger.error('Failed to convert publicJwtKey to JWK', err) + res.status(500).json({ error: 'Failed to generate JWKS' }) + } +} \ No newline at end of file diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 60f6fc31e..81c5c1cb6 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -15,6 +15,7 @@ import { context as serverContext } from './context' import { cors } from './cors' import { i18n } from './localization' import { plugins } from './plugins' +import { jwks, openidConfiguration } from '@/openIDConnect' // TODO implement // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; @@ -84,6 +85,10 @@ export const createServer = async ( app.get('/hook/gms/' + CONFIG.GMS_WEBHOOK_SECRET, gmsWebhook) + // OpenID Connect + app.get('/.well-known/openid-configuration', openidConfiguration) + app.get('/.well-known/jwks.json', jwks) + // Apollo Server const apollo = new ApolloServer({ schema: await schema(), diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index 1eb01f09e..3d0e72cec 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -129,6 +129,24 @@ server { error_log $GRADIDO_LOG_PATH/nginx-error.hooks.log warn; } + # Well-Known for openid connect + location /.well-known/ { + limit_req zone=backend burst=20 nodelay; + limit_conn addr 10; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://127.0.0.1:4000/.well-known/; + proxy_redirect off; + + access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; + error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn; + } + # Admin Frontend location /admin { limit_req zone=frontend burst=30 nodelay; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index 1f5ca2304..f420d7059 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -114,6 +114,24 @@ server { error_log $GRADIDO_LOG_PATH/nginx-error.hooks.log warn; } + # Well-Known for openid connect + location /.well-known/ { + limit_req zone=backend burst=20 nodelay; + limit_conn addr 10; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://127.0.0.1:4000/.well-known/; + proxy_redirect off; + + access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; + error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn; + } + # Admin Frontend location /admin { limit_req zone=frontend burst=30 nodelay; diff --git a/nginx/gradido.conf b/nginx/gradido.conf index 1f4788ae2..be10a499f 100644 --- a/nginx/gradido.conf +++ b/nginx/gradido.conf @@ -43,6 +43,19 @@ server { proxy_redirect off; } + # Well-Known for openid connect + location /.well-known/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://backend:4000/.well-known/; + proxy_redirect off; + } + # Admin Frontend location /admin { proxy_http_version 1.1; From 7e6242a8f7507baed831fedb4307e7f0db69ec7d Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sun, 20 Jul 2025 09:52:55 +0200 Subject: [PATCH 2/3] add encryption key to jwks.json --- backend/src/openIDConnect/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/src/openIDConnect/index.ts b/backend/src/openIDConnect/index.ts index 7bfdde040..11653afbb 100644 --- a/backend/src/openIDConnect/index.ts +++ b/backend/src/openIDConnect/index.ts @@ -27,8 +27,10 @@ export const jwks = async (req: any, res: any): Promise => { throw new Error(defaultErrorForCaller) } try { - const publicKey = await importSPKI(homeCommunity.publicJwtKey, 'RS256') - const jwk = await exportJWK(publicKey) + const rs256Key = await importSPKI(homeCommunity.publicJwtKey, 'RS256') + const rsaKey = await importSPKI(homeCommunity.publicJwtKey, 'RSA-OAEP-256') + const jwkRs256 = await exportJWK(rs256Key) + const jwkRsa = await exportJWK(rsaKey) // Optional: calculate Key ID (z.B. SHA-256 Fingerprint) const kid = createHash('sha256') @@ -38,11 +40,17 @@ export const jwks = async (req: any, res: any): Promise => { const jwks = { keys: [ { - ...jwk, + ...jwkRs256, alg: 'RS256', use: 'sig', kid, }, + { + ...jwkRsa, + alg: 'RSA-OAEP-256', + use: 'sig', + kid, + }, ], } res.setHeader('Cache-Control', 'public, max-age=3600, immutable') From 7d164f3ef2c5b3f86fe9193b037aa88d3dca35d3 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 30 Jul 2025 12:15:43 +0200 Subject: [PATCH 3/3] add realms to make it easier to switch out later with keycloak --- backend/src/config/const.ts | 3 ++- backend/src/openIDConnect/index.ts | 4 ++-- backend/src/server/createServer.ts | 6 ++--- .../sites-available/gradido.conf.ssl.template | 24 ++++++++++++++++--- .../sites-available/gradido.conf.template | 24 ++++++++++++++++--- nginx/gradido.conf | 16 +++++++++++-- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/backend/src/config/const.ts b/backend/src/config/const.ts index de9f094b2..05d413da4 100644 --- a/backend/src/config/const.ts +++ b/backend/src/config/const.ts @@ -1,2 +1,3 @@ export const LOG4JS_BASE_CATEGORY_NAME = 'backend' -export const FRONTEND_LOGIN_ROUTE = 'login' \ No newline at end of file +export const FRONTEND_LOGIN_ROUTE = 'login' +export const GRADIDO_REALM = 'gradido' \ No newline at end of file diff --git a/backend/src/openIDConnect/index.ts b/backend/src/openIDConnect/index.ts index 11653afbb..f6ef5a88d 100644 --- a/backend/src/openIDConnect/index.ts +++ b/backend/src/openIDConnect/index.ts @@ -1,5 +1,5 @@ import { CONFIG } from '@/config' -import { FRONTEND_LOGIN_ROUTE, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { FRONTEND_LOGIN_ROUTE, GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getHomeCommunity } from 'database' import { importSPKI, exportJWK } from 'jose' import { createHash } from 'crypto' @@ -12,7 +12,7 @@ export const openidConfiguration = async (req: any, res: any): Promise => res.setHeader('Content-Type', 'application/json') res.status(200).json({ issuer: new URL(FRONTEND_LOGIN_ROUTE, CONFIG.COMMUNITY_URL).toString(), - jwks_uri: new URL('/.well-known/jwks.json', CONFIG.COMMUNITY_URL).toString(), + jwks_uri: new URL(`/realms/${GRADIDO_REALM}/protocol/openid-connect/certs`, CONFIG.COMMUNITY_URL).toString(), }) } diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 81c5c1cb6..5f3bb02ef 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -9,7 +9,7 @@ import helmet from 'helmet' import { Logger, getLogger } from 'log4js' import { DataSource } from 'typeorm' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AppDatabase } from 'database' import { context as serverContext } from './context' import { cors } from './cors' @@ -86,8 +86,8 @@ export const createServer = async ( app.get('/hook/gms/' + CONFIG.GMS_WEBHOOK_SECRET, gmsWebhook) // OpenID Connect - app.get('/.well-known/openid-configuration', openidConfiguration) - app.get('/.well-known/jwks.json', jwks) + app.get(`/realms/${GRADIDO_REALM}/.well-known/openid-configuration`, openidConfiguration) + app.get(`/realms/${GRADIDO_REALM}/protocol/openid-connect/certs`, jwks) // Apollo Server const apollo = new ApolloServer({ diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index 3d0e72cec..3bc911d39 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -131,8 +131,8 @@ server { # Well-Known for openid connect location /.well-known/ { - limit_req zone=backend burst=20 nodelay; - limit_conn addr 10; + limit_req zone=backend burst=10 nodelay; + limit_conn addr 5; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -140,7 +140,25 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; - proxy_pass http://127.0.0.1:4000/.well-known/; + proxy_pass http://127.0.0.1:4000/realms/gradido/.well-known; + proxy_redirect off; + + access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; + error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn; + } + + # Well-Known for openid connect + location /realms/gradido { + limit_req zone=backend burst=10 nodelay; + limit_conn addr 5; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://127.0.0.1:4000/realms/gradido; proxy_redirect off; access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index f420d7059..15e66046c 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -116,8 +116,8 @@ server { # Well-Known for openid connect location /.well-known/ { - limit_req zone=backend burst=20 nodelay; - limit_conn addr 10; + limit_req zone=backend burst=10 nodelay; + limit_conn addr 5; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -125,7 +125,25 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; - proxy_pass http://127.0.0.1:4000/.well-known/; + proxy_pass http://127.0.0.1:4000/realms/gradido/.well-known; + proxy_redirect off; + + access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; + error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn; + } + + # Well-Known for openid connect + location /realms/gradido { + limit_req zone=backend burst=10 nodelay; + limit_conn addr 5; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://127.0.0.1:4000/realms/gradido; proxy_redirect off; access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log; diff --git a/nginx/gradido.conf b/nginx/gradido.conf index be10a499f..bbfd8db51 100644 --- a/nginx/gradido.conf +++ b/nginx/gradido.conf @@ -44,7 +44,7 @@ server { } # Well-Known for openid connect - location /.well-known/ { + location /.well-known { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -52,7 +52,19 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; - proxy_pass http://backend:4000/.well-known/; + proxy_pass http://backend:4000/realms/gradido/.well-known; + proxy_redirect off; + } + + location /realms/gradido { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://backend:4000/realms/gradido; proxy_redirect off; }