Skip to content

Commit

Permalink
feat: Add support for jwks_uri in the discovery endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasdao committed Sep 30, 2020
1 parent b7cdcb2 commit c06db94
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 105 deletions.
223 changes: 131 additions & 92 deletions README.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"puffy": "^1.2.1",
"raw-body": "^2.4.1",
"simple-template-utils": "0.0.6",
"userin-core": "^1.6.2"
"userin-core": "^1.9.3"
},
"devDependencies": {
"chai": "^4.2.0",
Expand Down
73 changes: 64 additions & 9 deletions src/discovery/_core.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
const { error: { catchErrors, wrapErrors } } = require('puffy')
const { request: { getFullUrl } } = require('../_utils')
const { getOpenIdEvents, getLoginSignupEvents } = require('userin-core')

const OPENID_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat']
const USERIN_OPENID_CLAIMS = ['scope', 'client_id' ]
const OPENID_SCOPES = ['openid']

const AUTH_CODE_FLOW_REQUIRED_EVENTS = [...getLoginSignupEvents(), 'generate_authorization_code', 'get_authorization_code_claims']

const isOpenIdReady = eventHandlerStore => getOpenIdEvents({ required:true }).every(e => eventHandlerStore[e])

const supportsAuthCodeFlow = eventHandlerStore => AUTH_CODE_FLOW_REQUIRED_EVENTS.every(e => eventHandlerStore[e])

/**
* Gets the OpenID Connect discovery data.
Expand All @@ -11,9 +22,16 @@ const { request: { getFullUrl } } = require('../_utils')
*/
const getOpenIdDiscoveryData = (req, endpoints, eventHandlerStore) => catchErrors((async () => {
const errorMsg = 'Failed to get the OpenID Connect discovery data'

const [configErrors, config={}] = await eventHandlerStore.get_config.exec()
if (configErrors)
throw wrapErrors(errorMsg, configErrors)

const discovery = {
issuer: endpoints.issuer||null
issuer: config.iss||null,
claims_supported:[],
scopes_supported:[],
grant_types_supported:[]
}

if (endpoints.introspection_endpoint)
Expand All @@ -22,15 +40,52 @@ const getOpenIdDiscoveryData = (req, endpoints, eventHandlerStore) => catchError
discovery.token_endpoint = getFullUrl(req, endpoints.token_endpoint)
if (endpoints.userinfo_endpoint)
discovery.userinfo_endpoint = getFullUrl(req, endpoints.userinfo_endpoint)
if (endpoints.jwks_uri)
discovery.jwks_uri = getFullUrl(req, endpoints.jwks_uri)

// const jwks_uri
// const response_types_supported
// const id_token_signing_alg_values_supported
// const scopes_supported
// const token_endpoint_auth_methods_supported
// const claims_supported
// const code_challenge_methods_supported
// const grant_types_supported
const openIdReady = isOpenIdReady(eventHandlerStore)
const authCodeFlowReady = supportsAuthCodeFlow(eventHandlerStore)

if (openIdReady || authCodeFlowReady) {
discovery.code_challenge_methods_supported = ['plain', 'S256']
discovery.response_types_supported = ['code', 'token', 'code token']
discovery.grant_types_supported.push('authorization_code', 'refresh_token')
}

if (openIdReady) {
discovery.response_types_supported = [ 'code', 'token', 'id_token', 'code token', 'code id_token', 'token id_token', 'code token id_token']
discovery.token_endpoint_auth_methods_supported = ['client_secret_post']
discovery.grant_types_supported.push('password', 'client_credentials')
discovery.claims_supported.push(...OPENID_CLAIMS, ...USERIN_OPENID_CLAIMS)
discovery.scopes_supported.push(...OPENID_SCOPES)
}

if (eventHandlerStore.get_jwks) {
const [errors, keys=[]] = await eventHandlerStore.get_jwks.exec()
if (errors) throw wrapErrors(errorMsg, errors)
const algs = Array.from(new Set(keys.filter(k => k.alg).map(k => k.alg)))
discovery.id_token_signing_alg_values_supported = algs
}

if (eventHandlerStore.get_scopes_supported) {
const [errors, values=[]] = await eventHandlerStore.get_scopes_supported.exec()
if (errors) throw wrapErrors(errorMsg, errors)
discovery.scopes_supported = values
discovery.scopes_supported = Array.from(new Set([...discovery.scopes_supported, ...values]))
}

if (eventHandlerStore.get_claims_supported) {
const [errors, values=[]] = await eventHandlerStore.get_claims_supported.exec()
if (errors) throw wrapErrors(errorMsg, errors)
discovery.claims_supported = values
discovery.claims_supported = Array.from(new Set([...discovery.claims_supported, ...values]))
}

if (eventHandlerStore.get_grant_types_supported) {
const [errors, values=[]] = await eventHandlerStore.get_grant_types_supported.exec()
if (errors) throw wrapErrors(errorMsg, errors)
discovery.grant_types_supported = Array.from(new Set([...discovery.grant_types_supported, ...values]))
}

return discovery
})())
Expand Down
1 change: 1 addition & 0 deletions src/eventRegister.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ module.exports = eventHandlerStore => {
registerEvent('get_config', () => strategyHandler.config)
// 4. Regsiter all the strategy's events handler
const events = getEvents()

events.forEach(eventName => {
if (strategyHandler[eventName])
registerEvent(eventName, strategyHandler[eventName])
Expand Down
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const introspectApi = require('./introspect')
const discoveryApi = require('./discovery')
const tokenApi = require('./token')
const userinfoApi = require('./userinfo')
const jwksUriApi = require('./jwks_uri')
const loginApi = require('./login')
const signupApi = require('./signup')
const eventRegister = require('./eventRegister')
Expand Down Expand Up @@ -168,6 +169,9 @@ class UserIn extends express.Router {
createHttpHandler('browse_endpoint', 'get', browseApi)
createHttpHandler('browse_redirect_endpoint', 'get', browseApi.redirect)

if (eventHandlerStore.get_jwks)
createOauth2HttpHandler('jwks_uri', 'get', jwksUriApi, { formatJSON:true })

// 6. Create the HTTP endpoint based on the modes.
if (openIdModeOn) {
createOauth2HttpHandler('introspection_endpoint', 'post', introspectApi)
Expand Down
43 changes: 43 additions & 0 deletions src/jwks_uri/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const { co } = require('core-async')
const { error: { catchErrors, wrapErrors } } = require('puffy')

const endpoint = 'certs'

const TRACE_ON = process.env.LOG_LEVEL == 'trace'

/**
* Verifies that a token is valid and returns the claims associated with that token if it is valid.
*
* @param {Object} _
* @param {Object} eventHandlerStore
* @param {Object} context.endpoints Object containing all the OIDC endpoints (pathname only)
* @param {Request} context.req Express Request
* @param {Response} context.res Express Response
* @param {String} context.authorization HTTP Authorization header value (e.g., 'Bearer 12345')
*
* @yield {[Error]} output[0] Array of errors
* @return {[JWK]} output[1].keys
*/
const handler = (_, eventHandlerStore={}) => catchErrors(co(function *() {
if (TRACE_ON)
console.log('INFO - Request to get the JWK public keys')

const errorMsg = 'Failed to get the JWK public keys'

if (eventHandlerStore.get_jwks) {
const [errors, jwks] = yield eventHandlerStore.get_jwks.exec()
if (errors)
throw wrapErrors(errorMsg, errors)

return { keys:jwks }
} else
return { keys:[] }
}))

module.exports = {
endpoint,
handler
}



32 changes: 32 additions & 0 deletions test/mock/ExhaustiveMockStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,38 @@ ExhaustiveMockStrategy.prototype.get_access_token_claims = OpenIdMockStrategy.pr
*/
ExhaustiveMockStrategy.prototype.get_identity_claims = OpenIdMockStrategy.prototype.get_identity_claims

/**
* Gets all supported claims.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[String]} claims e.g., ['given_name', 'family_name', 'zoneinfo', 'email', 'email_verified', 'address', 'phone', 'phone_number_verified']
*/
ExhaustiveMockStrategy.prototype.get_claims_supported = OpenIdMockStrategy.prototype.get_claims_supported

/**
* Gets all supported scopes
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[String]} scopes e.g., ['profile', 'email', 'phone', 'address', 'openid']
*/
ExhaustiveMockStrategy.prototype.get_scopes_supported = OpenIdMockStrategy.prototype.get_scopes_supported

/**
* Gets the public JWKs that can be used to verify the id_token.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[JWK]} jwks
*/
ExhaustiveMockStrategy.prototype.get_jwks = OpenIdMockStrategy.prototype.get_jwks

module.exports = ExhaustiveMockStrategy

Expand Down
57 changes: 57 additions & 0 deletions test/mock/OpenIdMockStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,63 @@ OpenIdMockStrategy.prototype.get_identity_claims = (root, { user_id, scopes }, c
}
}

/**
* Gets all supported claims.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[String]} claims e.g., ['given_name', 'family_name', 'zoneinfo', 'email', 'email_verified', 'address', 'phone', 'phone_number_verified']
*/
OpenIdMockStrategy.prototype.get_claims_supported = () => {
// Note: You do not need to include the OpenID claims (e.g., 'iss', 'aud'). UserIn takes care of these.

return ['given_name', 'family_name', 'zoneinfo', 'email', 'email_verified', 'address', 'phone', 'phone_number_verified']
}

/**
* Gets all supported scopes
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[String]} scopes e.g., ['profile', 'email', 'phone', 'address', 'openid']
*/
OpenIdMockStrategy.prototype.get_scopes_supported = () => {
// Note: You do not need to include the OpenID scope 'openid'. UserIn takes care of that one.

return ['profile', 'email', 'phone', 'address', 'openid']
}

/**
* Gets the public JWKs that can be used to verify the id_token.
*
* @param {Object} root Previous handler's response. Occurs when there are multiple handlers defined for the same event.
* @param {Object} payload
* @param {Object} context Strategy's configuration
*
* @return {[JWK]} jwks
*/
OpenIdMockStrategy.prototype.get_jwks = () => {
return [{
n: 'mvj-0waJ2owQlFWrlC06goLs9PcNehIzCF0QrkdsYZJXOsipcHCFlXBsgQIdTdLvlCzNI07jSYA-zggycYi96lfDX-FYv_CqC8dRLf9TBOPvUgCyFMCFNUTC69hsrEYMR_J79Wj0MIOffiVr6eX-AaCG3KhBMZMh15KCdn3uVrl9coQivy7bk2Uw-aUJ_b26C0gWYj1DnpO4UEEKBk1X-lpeUMh0B_XorqWeq0NYK2pN6CoEIh0UrzYKlGfdnMU1pJJCsNxMiha-Vw3qqxez6oytOV_AswlWvQc7TkSX6cHfqepNskQb7pGxpgQpy9sA34oIxB_S-O7VS7_h0Qh4vQ',
kty: 'RSA',
e: 'AQAB',
use: 'sig',
alg: 'RS256',
kid: '2c6fa6f5950a7ce465fcf247aa0b094828ac952c'
}, {
kty: 'RSA',
kid: '5effa76ef33ecb5e346bd512d7d89b30e47d8e98',
e: 'AQAB',
alg: 'RS256',
n: 'teG3wvigoU_KPbPAiEVERFmlGeHWPsnqbEk1pAhz69B0kGHJXU8l8tPHpTw0Gy_M9BJ5WAe9FvXL41xSFbqMGiJ7DIZ32ejlncrf2vGkMl26C5p8OOvuS6ThFjREUzWbV0sYtJL0nNjzmQNCQeb90tDQDZW229ZeUNlM2yN0QRisKlGFSK7uL8X0dRUbXnfgS6eI4mvSAK6tqq3n8IcPA0PxBr-R81rtdG70C2zxlPQ4Wp_MJzjb81d-RPdcYd64loOMhhHFbbfq2bTS9TSn_Y16lYA7gyRGSPhwcsdqOH2qqon7QOiF8gtrvztwd9TpxecPd7mleGGWVFlN6pTQYQ',
use: 'sig'
}]
}


module.exports = OpenIdMockStrategy

Expand Down

0 comments on commit c06db94

Please sign in to comment.