From 46a0f16f139c021f3a9bcb847ad5baf65e318cc5 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Sat, 25 Nov 2023 22:19:19 +0000
Subject: [PATCH 1/6] inital port of OIDC mock
---
package-lock.json | 75 ++++++++
package.json | 1 +
.../oidc/controllers/authorize-controller.js | 61 +++++++
src/api/oidc/controllers/token-controller.js | 162 ++++++++++++++++++
src/api/oidc/controllers/well-known-jwks.js | 10 ++
.../well-known-openid-configuration.js | 31 ++++
src/api/oidc/helpers/oidc-crypto.js | 113 ++++++++++++
src/api/oidc/helpers/session-store.js | 37 ++++
src/api/oidc/helpers/user-defaults.js | 28 +++
src/api/oidc/helpers/validate-scope.js | 8 +
src/api/oidc/index.js | 37 ++++
src/api/oidc/oidc.js | 68 ++++++++
src/api/router.js | 3 +-
13 files changed, 633 insertions(+), 1 deletion(-)
create mode 100644 src/api/oidc/controllers/authorize-controller.js
create mode 100644 src/api/oidc/controllers/token-controller.js
create mode 100644 src/api/oidc/controllers/well-known-jwks.js
create mode 100644 src/api/oidc/controllers/well-known-openid-configuration.js
create mode 100644 src/api/oidc/helpers/oidc-crypto.js
create mode 100644 src/api/oidc/helpers/session-store.js
create mode 100644 src/api/oidc/helpers/user-defaults.js
create mode 100644 src/api/oidc/helpers/validate-scope.js
create mode 100644 src/api/oidc/index.js
create mode 100644 src/api/oidc/oidc.js
diff --git a/package-lock.json b/package-lock.json
index 6fbcdbd..334a055 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"mongodb": "6.0.0",
"node-fetch": "2.7.0",
"node-jose": "2.2.0",
+ "pem-jwk": "2.0.0",
"pino": "8.15.0"
},
"devDependencies": {
@@ -4953,6 +4954,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -5437,6 +5449,11 @@
"node": ">=8"
}
},
+ "node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
"node_modules/bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
@@ -10393,6 +10410,11 @@
"node": ">=6"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -11144,6 +11166,20 @@
"node": ">=4"
}
},
+ "node_modules/pem-jwk": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pem-jwk/-/pem-jwk-2.0.0.tgz",
+ "integrity": "sha512-rFxu7rVoHgQ5H9YsP50dDWf0rHjreVA2z0yPiWr5WdH/UHb29hKtF7h6l8vNd1cbYR1t0QL+JKhW55a2ZV4KtA==",
+ "dependencies": {
+ "asn1.js": "^5.0.1"
+ },
+ "bin": {
+ "pem-jwk": "bin/pem-jwk.js"
+ },
+ "engines": {
+ "node": ">=5.10.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -11942,6 +11978,11 @@
"node": ">=10"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -17095,6 +17136,17 @@
"is-string": "^1.0.7"
}
},
+ "asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
"atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -17431,6 +17483,11 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
"bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
@@ -21039,6 +21096,11 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -21579,6 +21641,14 @@
}
}
},
+ "pem-jwk": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pem-jwk/-/pem-jwk-2.0.0.tgz",
+ "integrity": "sha512-rFxu7rVoHgQ5H9YsP50dDWf0rHjreVA2z0yPiWr5WdH/UHb29hKtF7h6l8vNd1cbYR1t0QL+JKhW55a2ZV4KtA==",
+ "requires": {
+ "asn1.js": "^5.0.1"
+ }
+ },
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -22135,6 +22205,11 @@
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g=="
},
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
"schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
diff --git a/package.json b/package.json
index 4402008..46d51f8 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"mongodb": "6.0.0",
"node-fetch": "2.7.0",
"node-jose": "2.2.0",
+ "pem-jwk": "2.0.0",
"pino": "8.15.0"
},
"devDependencies": {
diff --git a/src/api/oidc/controllers/authorize-controller.js b/src/api/oidc/controllers/authorize-controller.js
new file mode 100644
index 0000000..c5c9ba6
--- /dev/null
+++ b/src/api/oidc/controllers/authorize-controller.js
@@ -0,0 +1,61 @@
+import { oidcConfig } from '~/src/api/oidc/oidc'
+import { validateScope } from '~/src/api/oidc/helpers/validate-scope'
+import { newSession } from '~/src/api/oidc/helpers/session-store'
+
+const authorizeController = {
+ handler: (request, h) => {
+ // TODO check these are all set, use joi or something
+ const clientId = request.query.client_id
+ const responseType = request.query.response_type
+ const redirectUri = request.query.redirect_uri
+ const state = request.query.state
+ const scope = request.query.scope
+ const codeChallengeMethod = request.query.code_challenge_method
+
+ // validate request
+ const unsupportedScopes = validateScope(scope)
+ if (unsupportedScopes.length > 0) {
+ return h
+ .response(`Unsupported scopes ${unsupportedScopes.join(',')}`)
+ .code(400)
+ }
+
+ if (clientId !== oidcConfig.clientId) {
+ return h.response(`Invalid client id ${clientId}`).code(401)
+ }
+
+ if (!oidcConfig.responseTypesSupported.includes(responseType)) {
+ return h.response(`Unsupported response type ${responseType}`).code(400)
+ }
+
+ if (
+ codeChallengeMethod &&
+ !oidcConfig.codeChallengeMethodsSupported.includes(codeChallengeMethod)
+ ) {
+ return h
+ .response(`Unsupported code_challenge_method ${codeChallengeMethod}`)
+ .code(400)
+ }
+
+ // create session
+ // TODO we should load the users from some sort of config
+ const user = {
+ id: '62bb35d2-d4f2-4cf6-abd3-262d99727677', // TODO: what should this be, refer to example configs
+ username: 'Test User'
+ }
+ const session = newSession(
+ scope,
+ request.query.nonce,
+ user,
+ request.query.code_challenge,
+ request.query.code_challenge_method
+ )
+
+ const location = new URL(redirectUri)
+ location.searchParams.append('code', session.sessionId)
+ location.searchParams.append('state', state)
+ return h.redirect(location.toString())
+ }
+}
+
+export { authorizeController }
diff --git a/src/api/oidc/controllers/token-controller.js b/src/api/oidc/controllers/token-controller.js
new file mode 100644
index 0000000..6c3f881
--- /dev/null
+++ b/src/api/oidc/controllers/token-controller.js
@@ -0,0 +1,162 @@
+import { keyPair, oidcConfig } from '~/src/api/oidc/oidc'
+import {
+ getSessionByToken,
+ sessions
+} from '~/src/api/oidc/helpers/session-store'
+import {
+ generateCodeChallenge,
+ generateIDToken,
+ generateRefreshToken,
+ generateToken
+} from '~/src/api/oidc/helpers/oidc-crypto'
+
+const tokenController = {
+ handler: (request, h) => {
+ const logger = request.logger
+
+ const clientId = request.payload.client_id
+ const clientSecret = request.payload.client_secret
+ const grantType = request.payload.grant_type
+ const code = request.payload.code
+ const refreshToken = request.payload.refresh_token
+ const codeVerifier = request.payload.code_verifier
+
+ // validate client id
+ if (clientId !== oidcConfig.clientId) {
+ logger.error(`Invalid client id ${clientId}`)
+ return h.response(`Invalid client id ${clientId}`).code(401)
+ }
+
+ // validate secret
+ if (clientSecret !== oidcConfig.clientSecret) {
+ logger.error(`Invalid client secret`)
+ return h.response(`Invalid client secret`).code(401)
+ }
+
+ // get the session depending on the grant type
+ let result = null
+
+ if (grantType === 'authorization_code') {
+ logger.info('handling authorization code')
+ result = getSessionForAuthorizationCode(code)
+ // TODO: validate code challenge
+ const { valid, err } = validateCodeChallenge(result.session, codeVerifier)
+ if (!valid) {
+ logger.error(err)
+ return h.response(err).code(401)
+ }
+ } else if (grantType === 'refresh_token') {
+ logger.info('handling refresh token code')
+ result = getSessionForRefreshToken(refreshToken)
+ } else {
+ return h.response(`invalid grant type ${grantType}`).code(400)
+ }
+
+ const { session, valid } = result
+
+ if (!valid) {
+ logger.error('session was missing or invalid')
+ return h
+ .response(`invalid code/token for grant type ${grantType}`)
+ .code(400)
+ }
+
+ // build the token response
+
+ const tokenResponse = {
+ token_type: 'bearer',
+ expires_in: oidcConfig.ttl
+ }
+
+ // copy over the refresh token if its not there
+ if (refreshToken && refreshToken !== 'null') {
+ logger.info(`refresh token ${refreshToken}`)
+ tokenResponse.refresh_token = refreshToken
+ }
+
+ tokenResponse.access_token = generateToken(keyPair, session, grantType)
+
+ // the example only checked the first item in the scope, unsure if this is correct or an oversight?
+ if (session.scopes[0] === 'openid') {
+ tokenResponse.id_token = generateIDToken(keyPair, session)
+ }
+
+ if (grantType !== 'refresh_token') {
+ logger.info('generating a refresh token')
+ tokenResponse.refresh_token = generateRefreshToken(keyPair, session)
+ }
+
+ logger.info(tokenResponse)
+
+ return h
+ .response(tokenResponse)
+ .header('Cache-Control', 'no-cache, no-store, must-revalidate')
+ .code(200)
+ }
+}
+
+function getSessionForAuthorizationCode(code) {
+ const session = sessions[code]
+ if (!session || session.granted) {
+ return { session: null, valid: false }
+ }
+ sessions[code].granted = true
+ return {
+ session,
+ valid: true
+ }
+}
+
+function getSessionForRefreshToken(refreshToken) {
+ const session = getSessionByToken(refreshToken)
+ return {
+ session,
+ valid: session !== undefined
+ }
+}
+
+function validateCodeChallenge(session, codeVerifier) {
+ if (
+ session.codeChallenge === undefined ||
+ session.codeChallenge === '' ||
+ session.codeChallengeMethod === undefined ||
+ session.codeChallengeMethod === ''
+ ) {
+ return {
+ valid: true,
+ err: ''
+ }
+ }
+
+ if (codeVerifier === '') {
+ return {
+ valid: false,
+ err: 'Invalid code verifier. Expected code but client sent none.'
+ }
+ }
+
+ const challenge = generateCodeChallenge(
+ session.codeChallengeMethod,
+ codeVerifier
+ )
+ if (challenge === null) {
+ return {
+ valid: false,
+ err: `failed to generate code challenge for ${session.codeChallengeMethod} ${codeVerifier}`
+ }
+ }
+
+ if (challenge !== session.codeChallenge) {
+ return {
+ valid: false,
+ err: 'Invalid code verifier. Code challenge did not match hashed code verifier.'
+ }
+ }
+
+ return {
+ valid: true,
+ err: ''
+ }
+}
+
+export { tokenController }
diff --git a/src/api/oidc/controllers/well-known-jwks.js b/src/api/oidc/controllers/well-known-jwks.js
new file mode 100644
index 0000000..4671e01
--- /dev/null
+++ b/src/api/oidc/controllers/well-known-jwks.js
@@ -0,0 +1,10 @@
+import { JWKS } from '~/src/api/oidc/helpers/oidc-crypto'
+import { keyPair } from '~/src/api/oidc/oidc'
+
+const jwksController = {
+ handler: (request, h) => {
+ return h.response(JWKS(keyPair.publicKey)).code(200)
+ }
+}
+
+export { jwksController }
diff --git a/src/api/oidc/controllers/well-known-openid-configuration.js b/src/api/oidc/controllers/well-known-openid-configuration.js
new file mode 100644
index 0000000..1c94a33
--- /dev/null
+++ b/src/api/oidc/controllers/well-known-openid-configuration.js
@@ -0,0 +1,31 @@
+import { oidcConfig } from '~/src/api/oidc/oidc'
+
+const generateConfig = () => {
+ return {
+ issuer: oidcConfig.issuerBase,
+ authorization_endpoint: oidcConfig.authorizationEndpoint,
+ pushed_authorization_request_endpoint: `${oidcConfig.issuerBase}/par`,
+ token_endpoint: oidcConfig.tokenEndpoint,
+ jwks_uri: oidcConfig.jwksEndpoint,
+ userinfo_endpoint: oidcConfig.userinfoEndpoint,
+ introspection_endpoint: `${oidcConfig.issuerBase}/introspect`,
+ grant_types_supported: oidcConfig.grantTypesSupported,
+ response_types_supported: oidcConfig.responseTypesSupported,
+ subject_types_supported: oidcConfig.subjectTypesSupported,
+ id_token_signing_alg_values_supported:
+ oidcConfig.idTokenSigningAlgValuesSupported,
+ scopes_supported: oidcConfig.scopesSupported,
+ token_endpoint_auth_methods_supported:
+ oidcConfig.tokenEndpointAuthMethodsSupported,
+ claims_supported: oidcConfig.claimsSupported,
+ code_challenge_methods_supported: oidcConfig.claimsSupported
+ }
+}
+
+const openIdConfigurationController = {
+ handler: (request, h) => {
+ return h.response(generateConfig()).code(200)
+ }
+}
+
+export { openIdConfigurationController }
diff --git a/src/api/oidc/helpers/oidc-crypto.js b/src/api/oidc/helpers/oidc-crypto.js
new file mode 100644
index 0000000..e49beb0
--- /dev/null
+++ b/src/api/oidc/helpers/oidc-crypto.js
@@ -0,0 +1,113 @@
+import * as crypto from 'crypto'
+import { standardClaims } from '~/src/api/oidc/helpers/user-defaults'
+import { keyId, oidcConfig } from '~/src/api/oidc/oidc'
+import jsonwebtoken from 'jsonwebtoken'
+import { jwk2pem } from 'pem-jwk'
+
+function generateRSAKeyPair() {
+ // TODO: load these from file so they remain the same between restarts
+ return crypto.generateKeyPairSync('rsa', {
+ modulusLength: 2048, // 2048 bits is recommended for RSA keys
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'jwk'
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'jwk'
+ }
+ })
+}
+
+function rsaKeyToJwk(key) {
+ return {
+ kty: 'RSA',
+ n: Buffer.from(key.n, 'base64').toString('base64url'),
+ e: Buffer.from(key.e, 'base64').toString('base64url'),
+ alg: 'RS256',
+ use: 'sig',
+ kid: keyId
+ }
+}
+
+function JWKS(publicKey) {
+ return {
+ keys: [rsaKeyToJwk(publicKey)]
+ }
+}
+
+function generateToken(keyPair, session) {
+ const privateKey = jwk2pem(keyPair.privateKey)
+ const claim = standardClaims(session, oidcConfig.ttl)
+
+ return jsonwebtoken.sign(claim, privateKey, {
+ algorithm: 'RS256',
+ keyid: keyId
+ })
+}
+
+function generateIDToken(keyPair, session) {
+ const privateKey = jwk2pem(keyPair.privateKey)
+ const claims = standardClaims(session, oidcConfig.ttl)
+ claims.nonce = session.nonce
+
+ // TODO: add claims for user here
+ return jsonwebtoken.sign(claims, privateKey, {
+ algorithm: 'RS256',
+ keyid: keyId
+ })
+}
+
+function generateRefreshToken(keyPair, session) {
+ const privateKey = jwk2pem(keyPair.privateKey)
+
+ const claim = standardClaims(session, oidcConfig.refreshTtl)
+ return jsonwebtoken.sign(claim, privateKey, {
+ algorithm: 'RS256',
+ keyid: keyId
+ })
+}
+
+function generateCodeChallenge(method, codeVerifier) {
+ switch (method) {
+ case 'plain':
+ return codeVerifier
+ case 'S256':
+ return sha256(codeVerifier)
+ default:
+ return null
+ }
+}
+
+// If not manually set, computes the JWT headers' `kid`
+function keyID(keypair) {
+ const pem = jwk2pem(keypair.publicKey)
+ const publicKey = crypto.createPublicKey({
+ key: pem,
+ format: 'pem',
+ type: 'spki'
+ })
+ const publicKeyDER = publicKey.export({ type: 'spki', format: 'der' })
+
+ return sha256(publicKeyDER)
+}
+
+function sha256(input) {
+ const sha256 = crypto.createHash('sha256').update(input).digest()
+ return sha256
+ .toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '')
+}
+
+export {
+ generateRSAKeyPair,
+ rsaKeyToJwk,
+ JWKS,
+ generateToken,
+ generateIDToken,
+ generateRefreshToken,
+ generateCodeChallenge,
+ keyID
+}
diff --git a/src/api/oidc/helpers/session-store.js b/src/api/oidc/helpers/session-store.js
new file mode 100644
index 0000000..7a59905
--- /dev/null
+++ b/src/api/oidc/helpers/session-store.js
@@ -0,0 +1,37 @@
+import * as crypto from 'crypto'
+import jsonwebtoken from 'jsonwebtoken'
+import { createLogger } from '~/src/helpers/logging/logger'
+
+const logger = createLogger()
+
+const sessions = {}
+
+function getSessionId() {
+ return crypto.randomUUID()
+}
+
+function newSession(scope, nonce, user, challenge, challengeMethod) {
+ const id = getSessionId()
+
+ sessions[id] = {
+ sessionId: id,
+ scopes: scope.split(' '),
+ oidcNonce: nonce,
+ user,
+ granted: false,
+ codeChallenge: challenge,
+ codeChallengeMethod: challengeMethod
+ }
+
+ logger.info(`Creating a new session ${JSON.stringify(sessions[id])}`)
+
+ return sessions[id]
+}
+
+function getSessionByToken(token) {
+ const decodedToken = jsonwebtoken.decode(token)
+ const sessionId = decodedToken?.jti
+ return sessions[sessionId]
+}
+
+export { sessions, newSession, getSessionByToken }
diff --git a/src/api/oidc/helpers/user-defaults.js b/src/api/oidc/helpers/user-defaults.js
new file mode 100644
index 0000000..19bb665
--- /dev/null
+++ b/src/api/oidc/helpers/user-defaults.js
@@ -0,0 +1,28 @@
+import { oidcConfig } from '~/src/api/oidc/oidc'
+
+function standardClaims(session, ttl) {
+ const now = Math.floor(Date.now() / 1000)
+ return {
+ aud: [oidcConfig.clientId], // Audience should this be an array?
+ exp: now + ttl, // expires at
+ jti: session.sessionId, // session id
+ iat: now, // issued at
+ iss: oidcConfig.issuerBase, // TODO: is this right or is it just the host, wireshark to find out i guess
+ nbf: now, // not before
+ sub: session.user.id, // subject (i.e. user)
+ amr: ['eid', 'urn:be:fedict:iam:fas:Level500'],
+ azp: '63983fc2-cfff-45bb-8ec2-959e21062b9a', // azure something?
+ azpacr: 1,
+ groups: ['aabe63e7-87ef-4beb-a596-c810631fc474'],
+ name: session.user.username,
+ preferred_username: 'test@user.com',
+ oid: '62bb35d2-d4f2-4cf6-abd3-262d99727677', // ??
+ scope: 'openid read api',
+ upn: 'test@user.com',
+ uti: 'E_C59uEkm0ZORgAi0cAA',
+ ver: '2.0',
+ wids: ['13bd1c72-6f4a-4dcf-985f-18d3b80f208a']
+ }
+}
+
+export { standardClaims }
diff --git a/src/api/oidc/helpers/validate-scope.js b/src/api/oidc/helpers/validate-scope.js
new file mode 100644
index 0000000..89b2e2c
--- /dev/null
+++ b/src/api/oidc/helpers/validate-scope.js
@@ -0,0 +1,8 @@
+import { oidcConfig } from '~/src/api/oidc/oidc'
+
+const validateScope = (scope) => {
+ const scopes = scope.split(' ')
+ return scopes.filter((s) => !oidcConfig.scopesSupported.includes(s))
+}
+
+export { validateScope }
diff --git a/src/api/oidc/index.js b/src/api/oidc/index.js
new file mode 100644
index 0000000..e8f7262
--- /dev/null
+++ b/src/api/oidc/index.js
@@ -0,0 +1,37 @@
+import { oidcBasePath } from '~/src/api/oidc/oidc'
+import { openIdConfigurationController } from '~/src/api/oidc/controllers/well-known-openid-configuration'
+import { jwksController } from '~/src/api/oidc/controllers/well-known-jwks'
+import { authorizeController } from '~/src/api/oidc/controllers/authorize-controller'
+import { tokenController } from '~/src/api/oidc/controllers/token-controller'
+
+const oidc = {
+ plugin: {
+ name: 'oidc',
+ register: async (server) => {
+ server.route([
+ {
+ method: 'GET',
+ path: `${oidcBasePath}/.well-known/openid-configuration`,
+ ...openIdConfigurationController
+ },
+ {
+ method: 'GET',
+ path: `${oidcBasePath}/.well-known/jwks.json`,
+ ...jwksController
+ },
+ {
+ method: 'GET',
+ path: `${oidcBasePath}/authorize`,
+ ...authorizeController
+ },
+ {
+ method: 'POST',
+ path: `${oidcBasePath}/token`,
+ ...tokenController
+ }
+ ])
+ }
+ }
+}
+
+export { oidc }
diff --git a/src/api/oidc/oidc.js b/src/api/oidc/oidc.js
new file mode 100644
index 0000000..0d61c48
--- /dev/null
+++ b/src/api/oidc/oidc.js
@@ -0,0 +1,68 @@
+import { generateRSAKeyPair, keyID } from '~/src/api/oidc/helpers/oidc-crypto'
+
+const serverHost = 'http://localhost:3939'
+const oidcBasePath = '/oidc'
+
+const oidcConfig = {
+ clientId: '63983fc2-cfff-45bb-8ec2-959e21062b9a',
+ clientSecret: 'test_value',
+ issuerBase: `${serverHost}${oidcBasePath}`,
+ authorizationEndpoint: `${serverHost}${oidcBasePath}/authorize`,
+ tokenEndpoint: `${serverHost}${oidcBasePath}/token`,
+ userinfoEndpoint: `${serverHost}${oidcBasePath}/userinfo`,
+ jwksEndpoint: `${serverHost}${oidcBasePath}/.well-known/jwks.json`,
+ discoveryEndpoint: `${serverHost}${oidcBasePath}/.well-known/openid-configuration`,
+
+ invalidRequest: 'invalid_request',
+ invalidClient: 'invalid_client',
+ invalidGrant: 'invalid_grant',
+ unsupportedGrantType: 'unsupported_grant_type',
+ invalidScope: 'invalid_scope',
+ internalServerError: 'internal_server_error',
+ openidScope: 'openid',
+
+ grantTypesSupported: [
+ 'authorization_code',
+ 'refresh_token',
+ 'client_credentials'
+ ],
+ responseTypesSupported: ['code'],
+ subjectTypesSupported: ['public'],
+
+ idTokenSigningAlgValuesSupported: ['RS256'],
+
+ scopesSupported: [
+ 'openid',
+ 'email',
+ 'groups',
+ 'profile',
+ 'api://63983fc2-cfff-45bb-8ec2-959e21062b9a/cdp.user',
+ 'offline_access',
+ 'user.read'
+ ],
+
+ tokenEndpointAuthMethodsSupported: [
+ 'client_secret_basic',
+ 'client_secret_post'
+ ],
+
+ claimsSupported: [
+ 'sub',
+ 'email',
+ 'email_verified',
+ 'preferred_username',
+ 'phone_number',
+ 'address',
+ 'groups',
+ 'iss',
+ 'aud'
+ ],
+ codeChallengeMethodsSupported: ['plain', 'S256'],
+ ttl: 3600, // TODO: what unit are these? ms/sec?
+ refreshTtl: 3600
+}
+
+const keyPair = generateRSAKeyPair()
+const keyId = keyID(keyPair)
+
+export { oidcConfig, oidcBasePath, keyPair, keyId }
diff --git a/src/api/router.js b/src/api/router.js
index a2a2d12..550748c 100644
--- a/src/api/router.js
+++ b/src/api/router.js
@@ -2,12 +2,13 @@ import { health } from '~/src/api/health'
import { githubStub } from '~/src/api/github'
import { ecrStub } from '~/src/api/ecr'
import { adminStub } from '~/src/api/admin'
+import { oidc } from '~/src/api/oidc'
const router = {
plugin: {
name: 'Router',
register: async (server) => {
- await server.register([health, githubStub, ecrStub, adminStub])
+ await server.register([health, githubStub, ecrStub, adminStub, oidc])
}
}
}
From 05d86a70d15242efed0929302d3a5fb209ced252 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Sun, 26 Nov 2023 14:51:28 +0000
Subject: [PATCH 2/6] refactor key usage, user selection page
---
.../oidc/controllers/authorize-controller.js | 36 +++++++--
src/api/oidc/controllers/login-controller.js | 14 ++++
src/api/oidc/controllers/token-controller.js | 56 ++------------
src/api/oidc/controllers/well-known-jwks.js | 3 +-
.../well-known-openid-configuration.js | 2 +-
src/api/oidc/helpers/default-claims.js | 28 +++++++
src/api/oidc/helpers/oidc-crypto.js | 73 ++++++++++---------
src/api/oidc/helpers/oidc-crypto.test.js | 31 ++++++++
src/api/oidc/helpers/user-defaults.js | 28 -------
src/api/oidc/helpers/users.js | 20 +++++
.../oidc/helpers/validate-code-challenge.js | 47 ++++++++++++
src/api/oidc/helpers/validate-scope.js | 2 +-
src/api/oidc/index.js | 14 +++-
src/api/oidc/{oidc.js => oidc-config.js} | 7 +-
14 files changed, 231 insertions(+), 130 deletions(-)
create mode 100644 src/api/oidc/controllers/login-controller.js
create mode 100644 src/api/oidc/helpers/default-claims.js
create mode 100644 src/api/oidc/helpers/oidc-crypto.test.js
delete mode 100644 src/api/oidc/helpers/user-defaults.js
create mode 100644 src/api/oidc/helpers/users.js
create mode 100644 src/api/oidc/helpers/validate-code-challenge.js
rename src/api/oidc/{oidc.js => oidc-config.js} (89%)
diff --git a/src/api/oidc/controllers/authorize-controller.js b/src/api/oidc/controllers/authorize-controller.js
index c5c9ba6..e2c3c85 100644
--- a/src/api/oidc/controllers/authorize-controller.js
+++ b/src/api/oidc/controllers/authorize-controller.js
@@ -1,9 +1,32 @@
-import { oidcConfig } from '~/src/api/oidc/oidc'
+import { oidcBasePath, oidcConfig } from '~/src/api/oidc/oidc-config'
import { validateScope } from '~/src/api/oidc/helpers/validate-scope'
import { newSession } from '~/src/api/oidc/helpers/session-store'
+import { allUsers } from '~/src/api/oidc/helpers/users'
const authorizeController = {
handler: (request, h) => {
+ // a bit of a hack, but basically if the user propery hasn't been set
+ // show a 'login' page where they can select which fake user they want
+ if (request.query.user === undefined) {
+ const fullUrl = new URL(request.url)
+ const page = `
+
Login
+ ${Object.keys(allUsers).map(
+ (u) =>
+ '' +
+ u +
+ ''
+ )}
+ `
+ return h.response(page).header('content-type', 'text/html').code(200)
+ }
+
// TODO check these are all set, use joi or something
const clientId = request.query.client_id
const responseType = request.query.response_type
@@ -37,12 +60,13 @@ const authorizeController = {
.code(400)
}
- // create session
- // TODO we should load the users from some sort of config
- const user = {
- id: '62bb35d2-d4f2-4cf6-abd3-262d99727677', // TODO: what should this be, refer to example configs
- username: 'Test User'
+ const user = allUsers[request.query.user]
+ if (user === undefined) {
+ request.logger.error(`Invalid user selected ${request.query.user}`)
+ return h.response(`Invalid user selection!`).code(400)
}
+
+ // create session
const session = newSession(
scope,
request.query.nonce,
diff --git a/src/api/oidc/controllers/login-controller.js b/src/api/oidc/controllers/login-controller.js
new file mode 100644
index 0000000..0ee4da2
--- /dev/null
+++ b/src/api/oidc/controllers/login-controller.js
@@ -0,0 +1,14 @@
+import { oidcBasePath } from '~/src/api/oidc/oidc-config'
+
+const loginController = {
+ handler: (request, h) => {
+ const fullUrl = new URL(request.url)
+ const page = `
+ Login
+ Admin
+ `
+ return h.response(page).header('content-type', 'text/html').code(200)
+ }
+}
+
+export { loginController }
diff --git a/src/api/oidc/controllers/token-controller.js b/src/api/oidc/controllers/token-controller.js
index 6c3f881..00e3998 100644
--- a/src/api/oidc/controllers/token-controller.js
+++ b/src/api/oidc/controllers/token-controller.js
@@ -1,14 +1,14 @@
-import { keyPair, oidcConfig } from '~/src/api/oidc/oidc'
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
import {
getSessionByToken,
sessions
} from '~/src/api/oidc/helpers/session-store'
import {
- generateCodeChallenge,
generateIDToken,
generateRefreshToken,
generateToken
} from '~/src/api/oidc/helpers/oidc-crypto'
+import { validateCodeChallenge } from '~/src/api/oidc/helpers/validate-code-challenge'
const tokenController = {
handler: (request, h) => {
@@ -39,7 +39,6 @@ const tokenController = {
if (grantType === 'authorization_code') {
logger.info('handling authorization code')
result = getSessionForAuthorizationCode(code)
- // TODO: validate code challenge
const { valid, err } = validateCodeChallenge(result.session, codeVerifier)
if (!valid) {
logger.error(err)
@@ -69,21 +68,22 @@ const tokenController = {
}
// copy over the refresh token if its not there
+ // unsure if we still need the !== string 'null' check anymore
if (refreshToken && refreshToken !== 'null') {
logger.info(`refresh token ${refreshToken}`)
tokenResponse.refresh_token = refreshToken
}
- tokenResponse.access_token = generateToken(keyPair, session, grantType)
+ tokenResponse.access_token = generateToken(request.keys, session)
// the example only checked the first item in the scope, unsure if this is correct or an oversight?
if (session.scopes[0] === 'openid') {
- tokenResponse.id_token = generateIDToken(keyPair, session)
+ tokenResponse.id_token = generateIDToken(request.keys, session)
}
if (grantType !== 'refresh_token') {
logger.info('generating a refresh token')
- tokenResponse.refresh_token = generateRefreshToken(keyPair, session)
+ tokenResponse.refresh_token = generateRefreshToken(request.keys, session)
}
logger.info(tokenResponse)
@@ -115,48 +115,4 @@ function getSessionForRefreshToken(refreshToken) {
}
}
-function validateCodeChallenge(session, codeVerifier) {
- if (
- session.codeChallenge === undefined ||
- session.codeChallenge === '' ||
- session.codeChallengeMethod === undefined ||
- session.codeChallengeMethod === ''
- ) {
- return {
- valid: true,
- err: ''
- }
- }
-
- if (codeVerifier === '') {
- return {
- valid: false,
- err: 'Invalid code verifier. Expected code but client sent none.'
- }
- }
-
- const challenge = generateCodeChallenge(
- session.codeChallengeMethod,
- codeVerifier
- )
- if (challenge === null) {
- return {
- valid: false,
- err: `failed to generate code challenge for ${session.codeChallengeMethod} ${codeVerifier}`
- }
- }
-
- if (challenge !== session.codeChallenge) {
- return {
- valid: false,
- err: 'Invalid code verifier. Code challenge did not match hashed code verifier.'
- }
- }
-
- return {
- valid: true,
- err: ''
- }
-}
-
export { tokenController }
diff --git a/src/api/oidc/controllers/well-known-jwks.js b/src/api/oidc/controllers/well-known-jwks.js
index 4671e01..15c0dcd 100644
--- a/src/api/oidc/controllers/well-known-jwks.js
+++ b/src/api/oidc/controllers/well-known-jwks.js
@@ -1,9 +1,8 @@
import { JWKS } from '~/src/api/oidc/helpers/oidc-crypto'
-import { keyPair } from '~/src/api/oidc/oidc'
const jwksController = {
handler: (request, h) => {
- return h.response(JWKS(keyPair.publicKey)).code(200)
+ return h.response(JWKS(request.keys)).code(200)
}
}
diff --git a/src/api/oidc/controllers/well-known-openid-configuration.js b/src/api/oidc/controllers/well-known-openid-configuration.js
index 1c94a33..e779267 100644
--- a/src/api/oidc/controllers/well-known-openid-configuration.js
+++ b/src/api/oidc/controllers/well-known-openid-configuration.js
@@ -1,4 +1,4 @@
-import { oidcConfig } from '~/src/api/oidc/oidc'
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
const generateConfig = () => {
return {
diff --git a/src/api/oidc/helpers/default-claims.js b/src/api/oidc/helpers/default-claims.js
new file mode 100644
index 0000000..40d39db
--- /dev/null
+++ b/src/api/oidc/helpers/default-claims.js
@@ -0,0 +1,28 @@
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
+
+function defaultClaims(session, ttl) {
+ const now = Math.floor(Date.now() / 1000)
+ return {
+ aud: [oidcConfig.clientId], // Audience
+ exp: now + ttl, // expires at
+ jti: session.sessionId, // session id
+ iat: now, // issued at
+ iss: oidcConfig.issuerBase, // issuer
+ nbf: now, // not before
+ sub: session.user.id, // subject (i.e. user)
+ amr: ['eid', 'urn:be:fedict:iam:fas:Level500'],
+ azp: '63983fc2-cfff-45bb-8ec2-959e21062b9a', // azure something?
+ azpacr: 1, // more azure stuff?
+ groups: session.user.teams,
+ name: session.user.username,
+ preferred_username: session.user.email,
+ oid: session.user.id, // should this be user id or a unique one
+ scope: 'openid read api',
+ upn: session.user.email,
+ uti: 'E_C59uEkm0ZORgAi0cAA', // Token identifier claim, unique, per-token identifier that is case-sensitive
+ ver: '2.0',
+ wids: ['13bd1c72-6f4a-4dcf-985f-18d3b80f208a'] // ??
+ }
+}
+
+export { defaultClaims }
diff --git a/src/api/oidc/helpers/oidc-crypto.js b/src/api/oidc/helpers/oidc-crypto.js
index e49beb0..6f69747 100644
--- a/src/api/oidc/helpers/oidc-crypto.js
+++ b/src/api/oidc/helpers/oidc-crypto.js
@@ -1,9 +1,23 @@
import * as crypto from 'crypto'
-import { standardClaims } from '~/src/api/oidc/helpers/user-defaults'
-import { keyId, oidcConfig } from '~/src/api/oidc/oidc'
+import { defaultClaims } from '~/src/api/oidc/helpers/default-claims'
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
import jsonwebtoken from 'jsonwebtoken'
import { jwk2pem } from 'pem-jwk'
+function loadOIDCKeys() {
+ const jwk = generateRSAKeyPair()
+ const pem = {
+ publicKey: jwk2pem(jwk.publicKey),
+ privateKey: jwk2pem(jwk.privateKey)
+ }
+ const keyId = keyID(pem.publicKey)
+ return {
+ jwk,
+ keyId,
+ pem
+ }
+}
+
function generateRSAKeyPair() {
// TODO: load these from file so they remain the same between restarts
return crypto.generateKeyPairSync('rsa', {
@@ -19,52 +33,43 @@ function generateRSAKeyPair() {
})
}
-function rsaKeyToJwk(key) {
- return {
+function JWKS(keys) {
+ const jwks = {
kty: 'RSA',
- n: Buffer.from(key.n, 'base64').toString('base64url'),
- e: Buffer.from(key.e, 'base64').toString('base64url'),
+ n: Buffer.from(keys.jwk.publicKey.n, 'base64').toString('base64url'),
+ e: Buffer.from(keys.jwk.publicKey.e, 'base64').toString('base64url'),
alg: 'RS256',
use: 'sig',
- kid: keyId
+ kid: keys.keyId
}
-}
-function JWKS(publicKey) {
return {
- keys: [rsaKeyToJwk(publicKey)]
+ keys: [jwks]
}
}
-function generateToken(keyPair, session) {
- const privateKey = jwk2pem(keyPair.privateKey)
- const claim = standardClaims(session, oidcConfig.ttl)
-
- return jsonwebtoken.sign(claim, privateKey, {
+function generateToken(keys, session) {
+ const claim = defaultClaims(session, oidcConfig.ttl)
+ return jsonwebtoken.sign(claim, keys.pem.privateKey, {
algorithm: 'RS256',
- keyid: keyId
+ keyid: keys.keyId
})
}
-function generateIDToken(keyPair, session) {
- const privateKey = jwk2pem(keyPair.privateKey)
- const claims = standardClaims(session, oidcConfig.ttl)
- claims.nonce = session.nonce
-
- // TODO: add claims for user here
- return jsonwebtoken.sign(claims, privateKey, {
+function generateIDToken(keys, session) {
+ const claim = defaultClaims(session, oidcConfig.ttl)
+ claim.nonce = session.nonce
+ return jsonwebtoken.sign(claim, keys.pem.privateKey, {
algorithm: 'RS256',
- keyid: keyId
+ keyid: keys.keyId
})
}
-function generateRefreshToken(keyPair, session) {
- const privateKey = jwk2pem(keyPair.privateKey)
-
- const claim = standardClaims(session, oidcConfig.refreshTtl)
- return jsonwebtoken.sign(claim, privateKey, {
+function generateRefreshToken(keys, session) {
+ const claim = defaultClaims(session, oidcConfig.refreshTtl)
+ return jsonwebtoken.sign(claim, keys.pem.privateKey, {
algorithm: 'RS256',
- keyid: keyId
+ keyid: keys.keyId
})
}
@@ -80,8 +85,7 @@ function generateCodeChallenge(method, codeVerifier) {
}
// If not manually set, computes the JWT headers' `kid`
-function keyID(keypair) {
- const pem = jwk2pem(keypair.publicKey)
+function keyID(pem) {
const publicKey = crypto.createPublicKey({
key: pem,
format: 'pem',
@@ -102,12 +106,11 @@ function sha256(input) {
}
export {
- generateRSAKeyPair,
- rsaKeyToJwk,
+ loadOIDCKeys,
JWKS,
generateToken,
generateIDToken,
generateRefreshToken,
generateCodeChallenge,
- keyID
+ sha256
}
diff --git a/src/api/oidc/helpers/oidc-crypto.test.js b/src/api/oidc/helpers/oidc-crypto.test.js
new file mode 100644
index 0000000..75e4979
--- /dev/null
+++ b/src/api/oidc/helpers/oidc-crypto.test.js
@@ -0,0 +1,31 @@
+import {
+ generateCodeChallenge,
+ sha256
+} from '~/src/api/oidc/helpers/oidc-crypto'
+
+describe('sha256', () => {
+ it('should produce a valid url-safe base 64 hash in', () => {
+ const hash = sha256('foobarbaz')
+ expect(hash).toBe('l981iLWj8kurw4UbNy8Lpxqdzd7UOxS50Glhv8FwfZ0')
+
+ const hash2 = sha256('foobar')
+ expect(hash2).toBe('w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI')
+ })
+})
+
+describe('generateCodeChallenge', () => {
+ it('should return the codeVerifier as plain text in plain mode', () => {
+ const result = generateCodeChallenge('plain', 'foobarbaz')
+ expect(result).toBe('foobarbaz')
+ })
+
+ it('should return the codeVerifier as a sha256 encoded codeVerifier in S256 mode', () => {
+ const result = generateCodeChallenge('S256', 'foobarbaz')
+ expect(result).toBe('l981iLWj8kurw4UbNy8Lpxqdzd7UOxS50Glhv8FwfZ0')
+ })
+
+ it('should return null when the mode is invalid', () => {
+ const result = generateCodeChallenge('foo', 'foobarbaz')
+ expect(result).toBeNull()
+ })
+})
diff --git a/src/api/oidc/helpers/user-defaults.js b/src/api/oidc/helpers/user-defaults.js
deleted file mode 100644
index 19bb665..0000000
--- a/src/api/oidc/helpers/user-defaults.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { oidcConfig } from '~/src/api/oidc/oidc'
-
-function standardClaims(session, ttl) {
- const now = Math.floor(Date.now() / 1000)
- return {
- aud: [oidcConfig.clientId], // Audience should this be an array?
- exp: now + ttl, // expires at
- jti: session.sessionId, // session id
- iat: now, // issued at
- iss: oidcConfig.issuerBase, // TODO: is this right or is it just the host, wireshark to find out i guess
- nbf: now, // not before
- sub: session.user.id, // subject (i.e. user)
- amr: ['eid', 'urn:be:fedict:iam:fas:Level500'],
- azp: '63983fc2-cfff-45bb-8ec2-959e21062b9a', // azure something?
- azpacr: 1,
- groups: ['aabe63e7-87ef-4beb-a596-c810631fc474'],
- name: session.user.username,
- preferred_username: 'test@user.com',
- oid: '62bb35d2-d4f2-4cf6-abd3-262d99727677', // ??
- scope: 'openid read api',
- upn: 'test@user.com',
- uti: 'E_C59uEkm0ZORgAi0cAA',
- ver: '2.0',
- wids: ['13bd1c72-6f4a-4dcf-985f-18d3b80f208a']
- }
-}
-
-export { standardClaims }
diff --git a/src/api/oidc/helpers/users.js b/src/api/oidc/helpers/users.js
new file mode 100644
index 0000000..3509a14
--- /dev/null
+++ b/src/api/oidc/helpers/users.js
@@ -0,0 +1,20 @@
+const userAdmin = {
+ username: 'admin',
+ preferred_username: 'admin@oidc.mock',
+ id: '90552794-0613-4023-819a-512aa9d40023',
+ teams: ['aabe63e7-87ef-4beb-a596-c810631fc474']
+}
+
+const userNonAdmin = {
+ username: 'Test User',
+ email: 'test.user@oidc.mock',
+ id: 'dfa791eb-76b2-434c-ad1f-bb9dc1dd8b48',
+ teams: ['aabe63e7-87ef-4beb-a596-c810631fc474'] // TODO: swap this for a non-admin team
+}
+
+const allUsers = {
+ admin: userAdmin,
+ nonAdmin: userNonAdmin
+}
+
+export { userAdmin, userNonAdmin, allUsers }
diff --git a/src/api/oidc/helpers/validate-code-challenge.js b/src/api/oidc/helpers/validate-code-challenge.js
new file mode 100644
index 0000000..b0b0ca2
--- /dev/null
+++ b/src/api/oidc/helpers/validate-code-challenge.js
@@ -0,0 +1,47 @@
+import { generateCodeChallenge } from '~/src/api/oidc/helpers/oidc-crypto'
+
+function validateCodeChallenge(session, codeVerifier) {
+ if (
+ session.codeChallenge === undefined ||
+ session.codeChallenge === '' ||
+ session.codeChallengeMethod === undefined ||
+ session.codeChallengeMethod === ''
+ ) {
+ return {
+ valid: true,
+ err: ''
+ }
+ }
+
+ if (codeVerifier === '') {
+ return {
+ valid: false,
+ err: 'Invalid code verifier. Expected code but client sent none.'
+ }
+ }
+
+ const challenge = generateCodeChallenge(
+ session.codeChallengeMethod,
+ codeVerifier
+ )
+ if (challenge === null) {
+ return {
+ valid: false,
+ err: `failed to generate code challenge for ${session.codeChallengeMethod} ${codeVerifier}`
+ }
+ }
+
+ if (challenge !== session.codeChallenge) {
+ return {
+ valid: false,
+ err: 'Invalid code verifier. Code challenge did not match hashed code verifier.'
+ }
+ }
+
+ return {
+ valid: true,
+ err: ''
+ }
+}
+
+export { validateCodeChallenge }
diff --git a/src/api/oidc/helpers/validate-scope.js b/src/api/oidc/helpers/validate-scope.js
index 89b2e2c..a437825 100644
--- a/src/api/oidc/helpers/validate-scope.js
+++ b/src/api/oidc/helpers/validate-scope.js
@@ -1,4 +1,4 @@
-import { oidcConfig } from '~/src/api/oidc/oidc'
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
const validateScope = (scope) => {
const scopes = scope.split(' ')
diff --git a/src/api/oidc/index.js b/src/api/oidc/index.js
index e8f7262..67ece5b 100644
--- a/src/api/oidc/index.js
+++ b/src/api/oidc/index.js
@@ -1,13 +1,20 @@
-import { oidcBasePath } from '~/src/api/oidc/oidc'
+import { oidcBasePath } from '~/src/api/oidc/oidc-config'
import { openIdConfigurationController } from '~/src/api/oidc/controllers/well-known-openid-configuration'
import { jwksController } from '~/src/api/oidc/controllers/well-known-jwks'
import { authorizeController } from '~/src/api/oidc/controllers/authorize-controller'
import { tokenController } from '~/src/api/oidc/controllers/token-controller'
+import { loadOIDCKeys } from '~/src/api/oidc/helpers/oidc-crypto'
+import { loginController } from '~/src/api/oidc/controllers/login-controller'
const oidc = {
plugin: {
name: 'oidc',
register: async (server) => {
+ // generate/load the keys and decorate the server/request with them
+ const keys = loadOIDCKeys()
+ server.decorate('server', 'keys', keys)
+ server.decorate('request', 'keys', keys)
+
server.route([
{
method: 'GET',
@@ -24,6 +31,11 @@ const oidc = {
path: `${oidcBasePath}/authorize`,
...authorizeController
},
+ {
+ method: 'GET',
+ path: `${oidcBasePath}/login`,
+ ...loginController
+ },
{
method: 'POST',
path: `${oidcBasePath}/token`,
diff --git a/src/api/oidc/oidc.js b/src/api/oidc/oidc-config.js
similarity index 89%
rename from src/api/oidc/oidc.js
rename to src/api/oidc/oidc-config.js
index 0d61c48..30e9c1f 100644
--- a/src/api/oidc/oidc.js
+++ b/src/api/oidc/oidc-config.js
@@ -1,5 +1,3 @@
-import { generateRSAKeyPair, keyID } from '~/src/api/oidc/helpers/oidc-crypto'
-
const serverHost = 'http://localhost:3939'
const oidcBasePath = '/oidc'
@@ -62,7 +60,4 @@ const oidcConfig = {
refreshTtl: 3600
}
-const keyPair = generateRSAKeyPair()
-const keyId = keyID(keyPair)
-
-export { oidcConfig, oidcBasePath, keyPair, keyId }
+export { oidcConfig, oidcBasePath }
From ccc4a45a65d1b88cb7eed116d0c53522b8e65057 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Mon, 27 Nov 2023 07:12:36 +0000
Subject: [PATCH 3/6] user-info endpoint
---
.../oidc/controllers/authorize-controller.js | 21 +++++++-----------
src/api/oidc/controllers/login-controller.js | 14 ------------
.../oidc/controllers/user-info-controller.js | 22 +++++++++++++++++++
src/api/oidc/helpers/oidc-crypto.js | 2 --
src/api/oidc/index.js | 12 +++++-----
5 files changed, 36 insertions(+), 35 deletions(-)
delete mode 100644 src/api/oidc/controllers/login-controller.js
create mode 100644 src/api/oidc/controllers/user-info-controller.js
diff --git a/src/api/oidc/controllers/authorize-controller.js b/src/api/oidc/controllers/authorize-controller.js
index e2c3c85..8fad9bf 100644
--- a/src/api/oidc/controllers/authorize-controller.js
+++ b/src/api/oidc/controllers/authorize-controller.js
@@ -3,26 +3,21 @@ import { validateScope } from '~/src/api/oidc/helpers/validate-scope'
import { newSession } from '~/src/api/oidc/helpers/session-store'
import { allUsers } from '~/src/api/oidc/helpers/users'
+const userToLink = (user, query) => {
+ return `${user}`
+}
+
const authorizeController = {
handler: (request, h) => {
- // a bit of a hack, but basically if the user propery hasn't been set
+ // a bit of a hack, but basically if the user param hasn't been set
// show a 'login' page where they can select which fake user they want
if (request.query.user === undefined) {
const fullUrl = new URL(request.url)
const page = `
Login
- ${Object.keys(allUsers).map(
- (u) =>
- '' +
- u +
- ''
- )}
+ ${Object.keys(allUsers)
+ .map((user) => userToLink(user, fullUrl.search))
+ .join('
')}
`
return h.response(page).header('content-type', 'text/html').code(200)
}
diff --git a/src/api/oidc/controllers/login-controller.js b/src/api/oidc/controllers/login-controller.js
deleted file mode 100644
index 0ee4da2..0000000
--- a/src/api/oidc/controllers/login-controller.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { oidcBasePath } from '~/src/api/oidc/oidc-config'
-
-const loginController = {
- handler: (request, h) => {
- const fullUrl = new URL(request.url)
- const page = `
- Login
- Admin
- `
- return h.response(page).header('content-type', 'text/html').code(200)
- }
-}
-
-export { loginController }
diff --git a/src/api/oidc/controllers/user-info-controller.js b/src/api/oidc/controllers/user-info-controller.js
new file mode 100644
index 0000000..f9e2dd3
--- /dev/null
+++ b/src/api/oidc/controllers/user-info-controller.js
@@ -0,0 +1,22 @@
+import jsonwebtoken from 'jsonwebtoken'
+
+const userInfoController = {
+ handler: (request, h) => {
+ const authHeader = request.headers.authorization
+ if (authHeader === undefined || !authHeader.startsWith('Bearer ')) {
+ return h.response('Missing bearer token').code(401)
+ }
+ // drop 'Bearer '
+ const token = authHeader.slice(7)
+
+ try {
+ const decoded = jsonwebtoken.verify(token, request.keys.pem.publicKey)
+ return h.response(decoded).code(200)
+ } catch (e) {
+ request.logger.error(e)
+ return h.response('invalid token').code(401)
+ }
+ }
+}
+
+export { userInfoController }
diff --git a/src/api/oidc/helpers/oidc-crypto.js b/src/api/oidc/helpers/oidc-crypto.js
index 6f69747..45c40d6 100644
--- a/src/api/oidc/helpers/oidc-crypto.js
+++ b/src/api/oidc/helpers/oidc-crypto.js
@@ -84,7 +84,6 @@ function generateCodeChallenge(method, codeVerifier) {
}
}
-// If not manually set, computes the JWT headers' `kid`
function keyID(pem) {
const publicKey = crypto.createPublicKey({
key: pem,
@@ -92,7 +91,6 @@ function keyID(pem) {
type: 'spki'
})
const publicKeyDER = publicKey.export({ type: 'spki', format: 'der' })
-
return sha256(publicKeyDER)
}
diff --git a/src/api/oidc/index.js b/src/api/oidc/index.js
index 67ece5b..68c5d52 100644
--- a/src/api/oidc/index.js
+++ b/src/api/oidc/index.js
@@ -4,7 +4,7 @@ import { jwksController } from '~/src/api/oidc/controllers/well-known-jwks'
import { authorizeController } from '~/src/api/oidc/controllers/authorize-controller'
import { tokenController } from '~/src/api/oidc/controllers/token-controller'
import { loadOIDCKeys } from '~/src/api/oidc/helpers/oidc-crypto'
-import { loginController } from '~/src/api/oidc/controllers/login-controller'
+import { userInfoController } from '~/src/api/oidc/controllers/user-info-controller'
const oidc = {
plugin: {
@@ -31,15 +31,15 @@ const oidc = {
path: `${oidcBasePath}/authorize`,
...authorizeController
},
- {
- method: 'GET',
- path: `${oidcBasePath}/login`,
- ...loginController
- },
{
method: 'POST',
path: `${oidcBasePath}/token`,
...tokenController
+ },
+ {
+ method: 'GET',
+ path: `${oidcBasePath}/user-info`,
+ ...userInfoController
}
])
}
From 16207eccbd232d542b4878a158879edd969697a0 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Mon, 27 Nov 2023 07:44:05 +0000
Subject: [PATCH 4/6] moves oidc settings to config
---
src/api/oidc/oidc-config.js | 16 ++++++----------
src/config/index.js | 18 ++++++++++++++++++
2 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/src/api/oidc/oidc-config.js b/src/api/oidc/oidc-config.js
index 30e9c1f..c5755e6 100644
--- a/src/api/oidc/oidc-config.js
+++ b/src/api/oidc/oidc-config.js
@@ -1,9 +1,11 @@
+import { config } from '~/src/config'
+
const serverHost = 'http://localhost:3939'
-const oidcBasePath = '/oidc'
+const oidcBasePath = config.get('oidcBasePath')
const oidcConfig = {
- clientId: '63983fc2-cfff-45bb-8ec2-959e21062b9a',
- clientSecret: 'test_value',
+ clientId: config.get('oidcClientId'),
+ clientSecret: config.get('oidcClientSecret'),
issuerBase: `${serverHost}${oidcBasePath}`,
authorizationEndpoint: `${serverHost}${oidcBasePath}/authorize`,
tokenEndpoint: `${serverHost}${oidcBasePath}/token`,
@@ -11,12 +13,6 @@ const oidcConfig = {
jwksEndpoint: `${serverHost}${oidcBasePath}/.well-known/jwks.json`,
discoveryEndpoint: `${serverHost}${oidcBasePath}/.well-known/openid-configuration`,
- invalidRequest: 'invalid_request',
- invalidClient: 'invalid_client',
- invalidGrant: 'invalid_grant',
- unsupportedGrantType: 'unsupported_grant_type',
- invalidScope: 'invalid_scope',
- internalServerError: 'internal_server_error',
openidScope: 'openid',
grantTypesSupported: [
@@ -56,7 +52,7 @@ const oidcConfig = {
'aud'
],
codeChallengeMethodsSupported: ['plain', 'S256'],
- ttl: 3600, // TODO: what unit are these? ms/sec?
+ ttl: 3600, // seconds
refreshTtl: 3600
}
diff --git a/src/config/index.js b/src/config/index.js
index 1400072..fb49827 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -109,6 +109,24 @@ const config = convict({
format: String,
default: 'http://127.0.0.1:4566/000000000000/ecs-deployments',
env: 'SQS_ECS_QUEUE'
+ },
+ oidcBasePath: {
+ doc: 'the base path all oidc stubs will be served from',
+ format: String,
+ default: '/oidc',
+ env: 'OIDC_BASE_PATH'
+ },
+ oidcClientId: {
+ doc: 'client id to use in the oidc stub',
+ format: String,
+ default: '63983fc2-cfff-45bb-8ec2-959e21062b9a',
+ env: 'OIDC_CLIENT_ID'
+ },
+ oidcClientSecret: {
+ doc: 'the client secret key for the oidc stub',
+ format: String,
+ default: 'test_value',
+ env: 'OIDC_CLIENT_SECRET'
}
})
From 454577fafdf3993da901fd386b528195c23baeef Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Mon, 27 Nov 2023 09:49:57 +0000
Subject: [PATCH 5/6] config for server host
---
src/api/oidc/oidc-config.js | 2 +-
src/config/index.js | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/api/oidc/oidc-config.js b/src/api/oidc/oidc-config.js
index c5755e6..2d82b07 100644
--- a/src/api/oidc/oidc-config.js
+++ b/src/api/oidc/oidc-config.js
@@ -1,6 +1,6 @@
import { config } from '~/src/config'
-const serverHost = 'http://localhost:3939'
+const serverHost = `${config.get('oidcServerHost')}:${config.get('port')}`
const oidcBasePath = config.get('oidcBasePath')
const oidcConfig = {
diff --git a/src/config/index.js b/src/config/index.js
index fb49827..cf0c30f 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -127,6 +127,12 @@ const config = convict({
format: String,
default: 'test_value',
env: 'OIDC_CLIENT_SECRET'
+ },
+ oidcServerHost: {
+ doc: 'hostname of the stub server, used in wellknown urls endpoint',
+ format: String,
+ default: 'http://localhost',
+ env: 'OIDC_SERVER_HOST'
}
})
From 914a5c36f8630a7d9bb63583c54746cd2e509e52 Mon Sep 17 00:00:00 2001
From: Chris Turner <23338096+christopherjturner@users.noreply.github.com>
Date: Wed, 29 Nov 2023 08:52:34 +0000
Subject: [PATCH 6/6] allows for pre-defined b64 encoded certs for oidc. also
session admin endpoint for debugging
---
.../controllers/oidc-session-controller.js | 9 ++++
src/api/admin/index.js | 6 +++
.../oidc/controllers/authorize-controller.js | 16 ++----
src/api/oidc/controllers/token-controller.js | 3 +-
src/api/oidc/helpers/oidc-crypto.js | 51 +++++++++++++++----
src/api/oidc/helpers/render-login-page.js | 29 +++++++++++
src/api/oidc/index.js | 22 ++++++--
src/config/index.js | 12 +++++
8 files changed, 119 insertions(+), 29 deletions(-)
create mode 100644 src/api/admin/controllers/oidc-session-controller.js
create mode 100644 src/api/oidc/helpers/render-login-page.js
diff --git a/src/api/admin/controllers/oidc-session-controller.js b/src/api/admin/controllers/oidc-session-controller.js
new file mode 100644
index 0000000..4b7f164
--- /dev/null
+++ b/src/api/admin/controllers/oidc-session-controller.js
@@ -0,0 +1,9 @@
+import { sessions } from '~/src/api/oidc/helpers/session-store'
+
+const oidcSessionController = {
+ handler: async (request, h) => {
+ return h.response(sessions).code(200)
+ }
+}
+
+export { oidcSessionController }
diff --git a/src/api/admin/index.js b/src/api/admin/index.js
index c1b5f6e..adac0c0 100644
--- a/src/api/admin/index.js
+++ b/src/api/admin/index.js
@@ -1,4 +1,5 @@
import { triggerEcrPush } from '~/src/api/admin/controllers/trigger-ecr-push'
+import { oidcSessionController } from '~/src/api/admin/controllers/oidc-session-controller'
const adminStub = {
plugin: {
@@ -9,6 +10,11 @@ const adminStub = {
method: 'POST',
path: '/_admin/trigger-ecr-push/{repo}/{tag}',
...triggerEcrPush
+ },
+ {
+ method: 'GET',
+ path: '/_admin/oidc/sessions',
+ ...oidcSessionController
}
])
}
diff --git a/src/api/oidc/controllers/authorize-controller.js b/src/api/oidc/controllers/authorize-controller.js
index 8fad9bf..d4a1336 100644
--- a/src/api/oidc/controllers/authorize-controller.js
+++ b/src/api/oidc/controllers/authorize-controller.js
@@ -1,25 +1,15 @@
-import { oidcBasePath, oidcConfig } from '~/src/api/oidc/oidc-config'
+import { oidcConfig } from '~/src/api/oidc/oidc-config'
import { validateScope } from '~/src/api/oidc/helpers/validate-scope'
import { newSession } from '~/src/api/oidc/helpers/session-store'
import { allUsers } from '~/src/api/oidc/helpers/users'
-
-const userToLink = (user, query) => {
- return `${user}`
-}
+import { renderLoginPage } from '~/src/api/oidc/helpers/render-login-page'
const authorizeController = {
handler: (request, h) => {
// a bit of a hack, but basically if the user param hasn't been set
// show a 'login' page where they can select which fake user they want
if (request.query.user === undefined) {
- const fullUrl = new URL(request.url)
- const page = `
- Login
- ${Object.keys(allUsers)
- .map((user) => userToLink(user, fullUrl.search))
- .join('
')}
- `
- return h.response(page).header('content-type', 'text/html').code(200)
+ return renderLoginPage(request.url, h)
}
// TODO check these are all set, use joi or something
diff --git a/src/api/oidc/controllers/token-controller.js b/src/api/oidc/controllers/token-controller.js
index 00e3998..07cdf27 100644
--- a/src/api/oidc/controllers/token-controller.js
+++ b/src/api/oidc/controllers/token-controller.js
@@ -76,12 +76,11 @@ const tokenController = {
tokenResponse.access_token = generateToken(request.keys, session)
- // the example only checked the first item in the scope, unsure if this is correct or an oversight?
if (session.scopes[0] === 'openid') {
tokenResponse.id_token = generateIDToken(request.keys, session)
}
- if (grantType !== 'refresh_token') {
+ if (session.scopes[0] === 'refresh') {
logger.info('generating a refresh token')
tokenResponse.refresh_token = generateRefreshToken(request.keys, session)
}
diff --git a/src/api/oidc/helpers/oidc-crypto.js b/src/api/oidc/helpers/oidc-crypto.js
index 45c40d6..8a5a6ed 100644
--- a/src/api/oidc/helpers/oidc-crypto.js
+++ b/src/api/oidc/helpers/oidc-crypto.js
@@ -4,13 +4,31 @@ import { oidcConfig } from '~/src/api/oidc/oidc-config'
import jsonwebtoken from 'jsonwebtoken'
import { jwk2pem } from 'pem-jwk'
-function loadOIDCKeys() {
- const jwk = generateRSAKeyPair()
+function loadKeyPair(pub, priv) {
+ const privatePem = crypto.createPrivateKey({
+ key: priv,
+ format: 'pem',
+ encoding: 'utf8'
+ })
+
+ const publicPem = crypto.createPublicKey({
+ key: pub,
+ format: 'pem',
+ encoding: 'utf8'
+ })
+
const pem = {
- publicKey: jwk2pem(jwk.publicKey),
- privateKey: jwk2pem(jwk.privateKey)
+ publicKey: publicPem,
+ privateKey: privatePem
}
- const keyId = keyID(pem.publicKey)
+
+ const jwk = {
+ publicKey: pem.publicKey.export({ format: 'jwk' }),
+ privatePem: pem.privateKey.export({ format: 'jwk' })
+ }
+
+ const keyId = sha256(publicPem.export({ type: 'spki', format: 'der' }))
+
return {
jwk,
keyId,
@@ -18,9 +36,8 @@ function loadOIDCKeys() {
}
}
-function generateRSAKeyPair() {
- // TODO: load these from file so they remain the same between restarts
- return crypto.generateKeyPairSync('rsa', {
+function generateRandomKeypair() {
+ const jwk = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // 2048 bits is recommended for RSA keys
publicKeyEncoding: {
type: 'spki',
@@ -31,6 +48,17 @@ function generateRSAKeyPair() {
format: 'jwk'
}
})
+
+ const pem = {
+ publicKey: jwk2pem(jwk.publicKey),
+ privateKey: jwk2pem(jwk.privateKey)
+ }
+ const keyId = keyID(pem.publicKey)
+ return {
+ jwk,
+ keyId,
+ pem
+ }
}
function JWKS(keys) {
@@ -84,9 +112,9 @@ function generateCodeChallenge(method, codeVerifier) {
}
}
-function keyID(pem) {
+function keyID(pemPublicKey) {
const publicKey = crypto.createPublicKey({
- key: pem,
+ key: pemPublicKey,
format: 'pem',
type: 'spki'
})
@@ -104,7 +132,8 @@ function sha256(input) {
}
export {
- loadOIDCKeys,
+ loadKeyPair,
+ generateRandomKeypair,
JWKS,
generateToken,
generateIDToken,
diff --git a/src/api/oidc/helpers/render-login-page.js b/src/api/oidc/helpers/render-login-page.js
new file mode 100644
index 0000000..9e1648c
--- /dev/null
+++ b/src/api/oidc/helpers/render-login-page.js
@@ -0,0 +1,29 @@
+import { allUsers } from '~/src/api/oidc/helpers/users'
+import { oidcBasePath } from '~/src/api/oidc/oidc-config'
+
+const userToLink = (user, query) => {
+ return `${allUsers[user].username} - ${allUsers[user].id}`
+}
+
+const renderLoginPage = (url, h) => {
+ const queryParams = new URL(url).search
+ const page = `
+
+
CDP-Portal-Stubs - Login Stub
+
Select a user to login as:
+
+ ${Object.keys(allUsers)
+ .map((user) => userToLink(user, queryParams))
+ .join('')}
+
+
+
+
DEBUG:\n${decodeURIComponent(
+ queryParams.replace('?', '').replaceAll('&', '\n')
+ )}
+
+ `
+ return h.response(page).header('content-type', 'text/html').code(200)
+}
+
+export { renderLoginPage }
diff --git a/src/api/oidc/index.js b/src/api/oidc/index.js
index 68c5d52..d85251c 100644
--- a/src/api/oidc/index.js
+++ b/src/api/oidc/index.js
@@ -3,15 +3,31 @@ import { openIdConfigurationController } from '~/src/api/oidc/controllers/well-k
import { jwksController } from '~/src/api/oidc/controllers/well-known-jwks'
import { authorizeController } from '~/src/api/oidc/controllers/authorize-controller'
import { tokenController } from '~/src/api/oidc/controllers/token-controller'
-import { loadOIDCKeys } from '~/src/api/oidc/helpers/oidc-crypto'
+import {
+ generateRandomKeypair,
+ loadKeyPair
+} from '~/src/api/oidc/helpers/oidc-crypto'
import { userInfoController } from '~/src/api/oidc/controllers/user-info-controller'
+import { config } from '~/src/config'
const oidc = {
plugin: {
name: 'oidc',
register: async (server) => {
- // generate/load the keys and decorate the server/request with them
- const keys = loadOIDCKeys()
+ const cfgPubKey = config.get('oidcPublicKeyBase64')
+ const cfgPrivKey = config.get('oidcPrivateKeyBase64')
+
+ let keys
+ if (cfgPubKey && cfgPrivKey) {
+ server.logger.info('loading keys from config')
+ keys = loadKeyPair(
+ Buffer.from(cfgPubKey, 'base64'),
+ Buffer.from(cfgPrivKey, 'base64')
+ )
+ } else {
+ server.logger.info('generating random keys')
+ keys = generateRandomKeypair()
+ }
server.decorate('server', 'keys', keys)
server.decorate('request', 'keys', keys)
diff --git a/src/config/index.js b/src/config/index.js
index cf0c30f..26e5f5c 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -133,6 +133,18 @@ const config = convict({
format: String,
default: 'http://localhost',
env: 'OIDC_SERVER_HOST'
+ },
+ oidcPublicKeyBase64: {
+ doc: 'base 64 encoded public pem',
+ format: String,
+ default: undefined,
+ env: 'OIDC_PUBLIC_KEY_B64'
+ },
+ oidcPrivateKeyBase64: {
+ doc: 'base 64 encoded private pem',
+ format: String,
+ default: undefined,
+ env: 'OIDC_PRIVATE_KEY_B64'
}
})