|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +/* |
| 4 | + * Module dependencies. |
| 5 | + */ |
| 6 | + |
| 7 | +const AbstractGrantType = require('./abstract-grant-type'); |
| 8 | +const InvalidArgumentError = require('../errors/invalid-argument-error'); |
| 9 | +const InvalidGrantError = require('../errors/invalid-grant-error'); |
| 10 | +const InvalidRequestError = require('../errors/invalid-request-error'); |
| 11 | +const ServerError = require('../errors/server-error'); |
| 12 | +const { decodeJwt, decodeProtectedHeader, jwtVerify, createLocalJWKSet } = require('jose'); |
| 13 | +const { algorithmsForHeader, isHmac, isValidationError, replayId, getRemoteJwks } = require('../utils/jws-util'); |
| 14 | + |
| 15 | +/** |
| 16 | + * The `grant_type` value for the JWT bearer authorization grant. |
| 17 | + * @see https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 |
| 18 | + */ |
| 19 | +const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; |
| 20 | + |
| 21 | +/** |
| 22 | + * @class JwtBearerGrantType |
| 23 | + * @classdesc |
| 24 | + * The JWT bearer authorization grant (RFC 7521 §4.1, RFC 7523 §2.1/§3): a signed |
| 25 | + * JWT *is* the authorization grant. The assertion's `iss` identifies a trusted |
| 26 | + * issuer (whose key verifies the assertion) and `sub` identifies the principal |
| 27 | + * the access token is issued for. Unlike JWT *client authentication*, `sub` is |
| 28 | + * the user/principal — not the client — and `iss`/`sub` are not bound to the |
| 29 | + * client id. |
| 30 | + * |
| 31 | + * This is an extension grant; register it via `extendedGrantTypes`. The |
| 32 | + * requested `scope` is taken from the body parameter (RFC 7521 §4.1), and no |
| 33 | + * refresh token is issued (RFC 7521 §5.2). |
| 34 | + * |
| 35 | + * The model must implement: |
| 36 | + * - `getJWTBearerIssuer(issuer)` → `{ audience, jwks | jwksUri | secret }` (or |
| 37 | + * falsy for an untrusted issuer) — the verification key material and the |
| 38 | + * expected `aud`. |
| 39 | + * - `getJWTBearerUser({ issuer, subject, client, scope, jti, assertionId, exp })` → the |
| 40 | + * authorized user (or falsy to deny). Replay (`jti`) can be enforced here. |
| 41 | + * |
| 42 | + * @example |
| 43 | + * new OAuth2Server({ |
| 44 | + * model, |
| 45 | + * extendedGrantTypes: { |
| 46 | + * 'urn:ietf:params:oauth:grant-type:jwt-bearer': JwtBearerGrantType |
| 47 | + * }, |
| 48 | + * // typically a public requester; identify it with `client_id`: |
| 49 | + * requireClientAuthentication: { 'urn:ietf:params:oauth:grant-type:jwt-bearer': false } |
| 50 | + * }); |
| 51 | + * |
| 52 | + * @see https://datatracker.ietf.org/doc/html/rfc7521#section-4.1 |
| 53 | + * @see https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 |
| 54 | + * @see https://datatracker.ietf.org/doc/html/rfc7523#section-3 |
| 55 | + * @extends AbstractGrantType |
| 56 | + */ |
| 57 | +class JwtBearerGrantType extends AbstractGrantType { |
| 58 | + constructor(options = {}) { |
| 59 | + if (!options.model) { |
| 60 | + throw new InvalidArgumentError('Missing parameter: `model`'); |
| 61 | + } |
| 62 | + |
| 63 | + if (!options.model.getJWTBearerIssuer) { |
| 64 | + throw new InvalidArgumentError('Invalid argument: model does not implement `getJWTBearerIssuer()`'); |
| 65 | + } |
| 66 | + |
| 67 | + if (!options.model.getJWTBearerUser) { |
| 68 | + throw new InvalidArgumentError('Invalid argument: model does not implement `getJWTBearerUser()`'); |
| 69 | + } |
| 70 | + |
| 71 | + if (!options.model.saveToken) { |
| 72 | + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); |
| 73 | + } |
| 74 | + |
| 75 | + super(options); |
| 76 | + |
| 77 | + this.maxTokenAge = options.maxTokenAge; |
| 78 | + this.clockTolerance = options.clockTolerance; |
| 79 | + this.algorithms = options.algorithms; |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * Handle the JWT bearer grant. |
| 84 | + * |
| 85 | + * @param request {Request} |
| 86 | + * @param client {ClientData} |
| 87 | + * @see https://datatracker.ietf.org/doc/html/rfc7523#section-2.1 |
| 88 | + */ |
| 89 | + async handle(request, client) { |
| 90 | + if (!request) { |
| 91 | + throw new InvalidArgumentError('Missing parameter: `request`'); |
| 92 | + } |
| 93 | + |
| 94 | + if (!client) { |
| 95 | + throw new InvalidArgumentError('Missing parameter: `client`'); |
| 96 | + } |
| 97 | + |
| 98 | + const payload = await this.verifyAssertion(request); |
| 99 | + const scope = this.getScope(request); |
| 100 | + const user = await this.getUser(payload, client, scope, request.body.assertion); |
| 101 | + |
| 102 | + return this.saveToken(user, client, scope); |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Verify the `assertion` and return its (trusted) claims. |
| 107 | + */ |
| 108 | + async verifyAssertion(request) { |
| 109 | + const assertion = request.body.assertion; |
| 110 | + |
| 111 | + if (!assertion) { |
| 112 | + throw new InvalidRequestError('Missing parameter: `assertion`'); |
| 113 | + } |
| 114 | + |
| 115 | + // Decode (without verifying) only to discover the issuer we must verify |
| 116 | + // against; nothing here is trusted until the signature is checked. |
| 117 | + let issuer; |
| 118 | + let header; |
| 119 | + try { |
| 120 | + issuer = decodeJwt(assertion).iss; |
| 121 | + header = decodeProtectedHeader(assertion); |
| 122 | + } catch (e) { |
| 123 | + throw new InvalidGrantError('Invalid grant: `assertion` is malformed'); |
| 124 | + } |
| 125 | + |
| 126 | + if (!issuer) { |
| 127 | + throw new InvalidGrantError('Invalid grant: `assertion` is missing the `iss` claim'); |
| 128 | + } |
| 129 | + |
| 130 | + const issuerData = await this.model.getJWTBearerIssuer(issuer); |
| 131 | + if (!issuerData) { |
| 132 | + throw new InvalidGrantError('Invalid grant: `assertion` issuer is not trusted'); |
| 133 | + } |
| 134 | + |
| 135 | + if (!issuerData.audience || (Array.isArray(issuerData.audience) && issuerData.audience.length === 0)) { |
| 136 | + throw new ServerError('Server error: `getJWTBearerIssuer()` did not return an `audience`'); |
| 137 | + } |
| 138 | + |
| 139 | + const algorithms = this.algorithms || algorithmsForHeader(header); |
| 140 | + const key = await this.getKey(issuerData, header); |
| 141 | + |
| 142 | + try { |
| 143 | + const { payload } = await jwtVerify(assertion, key, { |
| 144 | + algorithms, |
| 145 | + audience: issuerData.audience, |
| 146 | + issuer, |
| 147 | + requiredClaims: ['iss', 'sub', 'aud', 'exp'], |
| 148 | + maxTokenAge: this.maxTokenAge, |
| 149 | + clockTolerance: this.clockTolerance, |
| 150 | + }); |
| 151 | + |
| 152 | + return payload; |
| 153 | + } catch (e) { |
| 154 | + if (isValidationError(e)) { |
| 155 | + throw new InvalidGrantError('Invalid grant: ' + e.message); |
| 156 | + } |
| 157 | + throw new ServerError(e); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * Resolve the verification key for the issuer (HMAC secret or asymmetric JWKS). |
| 163 | + */ |
| 164 | + async getKey(issuerData, header) { |
| 165 | + const hasSecret = typeof issuerData.secret === 'string' && issuerData.secret.length > 0; |
| 166 | + const hasJwks = !!issuerData.jwks || !!issuerData.jwksUri; |
| 167 | + |
| 168 | + // No key material at all is a server-side misconfiguration of the issuer. |
| 169 | + if (!hasSecret && !hasJwks) { |
| 170 | + throw new ServerError('Server error: issuer has no key material for assertion verification'); |
| 171 | + } |
| 172 | + |
| 173 | + if (isHmac(header.alg)) { |
| 174 | + // The issuer has key material, just not for this algorithm family — that |
| 175 | + // makes the assertion unverifiable against this issuer, i.e. the client's |
| 176 | + // fault, not the server's. |
| 177 | + if (!hasSecret) { |
| 178 | + throw new InvalidGrantError('Invalid grant: assertion algorithm does not match the issuer key material'); |
| 179 | + } |
| 180 | + return new TextEncoder().encode(issuerData.secret); |
| 181 | + } |
| 182 | + |
| 183 | + if (!hasJwks) { |
| 184 | + throw new InvalidGrantError('Invalid grant: assertion algorithm does not match the issuer key material'); |
| 185 | + } |
| 186 | + |
| 187 | + if (issuerData.jwks) { |
| 188 | + return createLocalJWKSet(issuerData.jwks); |
| 189 | + } |
| 190 | + |
| 191 | + return getRemoteJwks(issuerData.jwksUri); |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * Resolve and authorize the principal (`sub`) the token is issued for. |
| 196 | + */ |
| 197 | + async getUser(payload, client, scope, assertion) { |
| 198 | + const user = await this.model.getJWTBearerUser({ |
| 199 | + issuer: payload.iss, |
| 200 | + subject: payload.sub, |
| 201 | + client, |
| 202 | + scope, |
| 203 | + jti: payload.jti, |
| 204 | + assertionId: replayId(assertion, payload.jti), |
| 205 | + exp: payload.exp, |
| 206 | + }); |
| 207 | + |
| 208 | + if (!user) { |
| 209 | + throw new InvalidGrantError('Invalid grant: the assertion subject is not authorized'); |
| 210 | + } |
| 211 | + |
| 212 | + return user; |
| 213 | + } |
| 214 | + |
| 215 | + /** |
| 216 | + * Save and return the access token. No refresh token is issued (RFC 7521 §5.2). |
| 217 | + */ |
| 218 | + async saveToken(user, client, requestedScope) { |
| 219 | + const validatedScope = await this.validateScope(user, client, requestedScope); |
| 220 | + const accessToken = await this.generateAccessToken(client, user, validatedScope); |
| 221 | + const accessTokenExpiresAt = this.getAccessTokenExpiresAt(client, user, validatedScope); |
| 222 | + |
| 223 | + const token = { |
| 224 | + accessToken, |
| 225 | + accessTokenExpiresAt, |
| 226 | + scope: validatedScope, |
| 227 | + }; |
| 228 | + |
| 229 | + return this.model.saveToken(token, client, user); |
| 230 | + } |
| 231 | +} |
| 232 | + |
| 233 | +JwtBearerGrantType.GRANT_TYPE = GRANT_TYPE; |
| 234 | + |
| 235 | +module.exports = JwtBearerGrantType; |
0 commit comments