Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add user defined domain validation #227

Merged
merged 8 commits into from
Aug 8, 2023
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const getJwks = buildGetJwks({
ttl: 60 * 1000,
timeout: 5000,
allowedDomains: ['https://example.com'],
checkIssuer: (domain) => {
return domain === 'https://example.com'
},
eugenio-oddone marked this conversation as resolved.
Show resolved Hide resolved
providerDiscovery: false,
agent: new https.Agent({
keepAlive: true,
Expand All @@ -34,6 +37,7 @@ const getJwks = buildGetJwks({
- `ttl`: Milliseconds an item will remain in cache. Defaults to 60s.
- `timeout`: Specifies how long it should wait to retrieve a JWK before it fails. The time is set in milliseconds. Defaults to 5s.
- `allowedDomains`: Array of allowed domains. By default all domains are allowed.
- `checkIssuer`: Optional user defined function to validate a token's domain
- `providerDiscovery`: Indicates if the Provider Configuration Information is used to automatically get the jwks_uri from the [OpenID Provider Discovery Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). This endpoint is exposing the [Provider Metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). With this flag set to true the domain will be treated as the OpenID Issuer which is the iss property in the token. Defaults to false. Ignored if jwksPath is specified.
- `jwksPath`: Specify a relative path to the jwks_uri. Example `/otherdir/jwks.json`. Takes precedence over providerDiscovery. Optional.
- `agent`: The custom agent to use for requests, as specified in [node-fetch documentation](https://github.com/node-fetch/node-fetch#custom-agent). Defaults to `null`.
Expand Down
6 changes: 6 additions & 0 deletions src/get-jwks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function buildGetJwks(options = {}) {
const ttl = options.ttl || 60 * 1000 /* 1 minute */
const timeout = options.timeout || 5 * 1000 /* 5 seconds */
const allowedDomains = (options.allowedDomains || []).map(ensureTrailingSlash)
const checkIssuer = options.checkIssuer
const providerDiscovery = options.providerDiscovery || false
const jwksPath = options.jwksPath
? ensureNoLeadingSlash(options.jwksPath)
Expand Down Expand Up @@ -69,6 +70,11 @@ function buildGetJwks(options = {}) {
return Promise.reject(error)
}

if (checkIssuer && !checkIssuer(normalizedDomain)) {
const error = new GetJwksError(errorCode.DOMAIN_NOT_ALLOWED)
simoneb marked this conversation as resolved.
Show resolved Hide resolved
return Promise.reject(error)
}

const cacheKey = `${alg}:${kid}:${normalizedDomain}`
const cachedJwk = cache.get(cacheKey)

Expand Down
35 changes: 35 additions & 0 deletions test/getJwk.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,41 @@ t.test('allowed domains', async t => {
t.ok(await getJwks.getJwk({ domain: domain2, alg, kid }))
})

t.test('checks token issuer', async t => {
const domain = 'https://example.com/realms/REALM_NAME'

nock(domain).get('/.well-known/jwks.json').reply(200, jwks)

const getJwks = buildGetJwks({
checkIssuer: (issuer) => {
const url = new URL(issuer)
const baseUrl = `${url.protocol}//${url.hostname}/`
return baseUrl === 'https://example.com/'
}
})

const [{ alg, kid }] = jwks.keys

t.ok(await getJwks.getJwk({ domain, alg, kid }))
})

t.test('forbids invalid issuer', async t => {
const getJwks = buildGetJwks({
checkIssuer: (issuer) => {
const url = new URL(issuer)
const baseUrl = `${url.protocol}//${url.hostname}/`
return baseUrl === 'https://example.com/'
}
})

const [{ alg, kid }] = jwks.keys

return t.rejects(
getJwks.getJwk({ domain, alg, kid }),
'Issuer is not allowed.',
)
})

t.test('forbids domain outside of the allow list', async t => {
const getJwks = buildGetJwks({
allowedDomains: ['https://example.com/'],
Expand Down