From 7ed7fc9c3de119c4143e500d5b14378a055f0cac Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 29 Jun 2023 16:13:22 +1200 Subject: [PATCH 1/3] validate id token --- package.json | 1 + spec/unit/oidc/authorize.spec.ts | 62 +++++++++++++++++- spec/unit/oidc/validate.spec.ts | 104 ++++++++++++++++++++++++++++++- src/oidc/authorize.ts | 6 +- src/oidc/error.ts | 3 +- src/oidc/validate.ts | 87 ++++++++++++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 262 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0e682fda0e7..0bb3a830300 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", + "jwt-decode": "^3.1.2", "loglevel": "^1.7.1", "matrix-events-sdk": "0.0.1", "matrix-widget-api": "^1.3.1", diff --git a/spec/unit/oidc/authorize.spec.ts b/spec/unit/oidc/authorize.spec.ts index 51f46aadf8d..d773954889a 100644 --- a/spec/unit/oidc/authorize.spec.ts +++ b/spec/unit/oidc/authorize.spec.ts @@ -15,6 +15,8 @@ limitations under the License. */ import fetchMock from "fetch-mock-jest"; +import { mocked } from "jest-mock"; +import jwtDecode from "jwt-decode"; import { Method } from "../../../src"; import * as crypto from "../../../src/crypto/crypto"; @@ -26,6 +28,8 @@ import { } from "../../../src/oidc/authorize"; import { OidcError } from "../../../src/oidc/error"; +jest.mock("jwt-decode"); + // save for resetting mocks const realSubtleCrypto = crypto.subtleCrypto; @@ -112,15 +116,28 @@ describe("oidc authorization", () => { describe("completeAuthorizationCodeGrant", () => { const codeVerifier = "abc123"; + const nonce = "test-nonce"; const redirectUri = baseUrl; const code = "auth_code_xyz"; const validBearerTokenResponse = { token_type: "Bearer", access_token: "test_access_token", refresh_token: "test_refresh_token", + id_token: "valid.id.token", expires_in: 12345, }; + const validDecodedIdToken = { + // nonce matches + nonce, + // not expired + exp: Date.now() / 1000 + 100000, + // audience is this client + aud: clientId, + // issuer matches + iss: delegatedAuthConfig.issuer, + }; + beforeEach(() => { fetchMock.mockClear(); fetchMock.resetBehavior(); @@ -129,10 +146,18 @@ describe("oidc authorization", () => { status: 200, body: JSON.stringify(validBearerTokenResponse), }); + + mocked(jwtDecode).mockReturnValue(validDecodedIdToken); }); it("should make correct request to the token endpoint", async () => { - await completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig }); + await completeAuthorizationCodeGrant(code, { + clientId, + codeVerifier, + redirectUri, + delegatedAuthConfig, + nonce, + }); expect(fetchMock).toHaveBeenCalledWith(tokenEndpoint, { method: Method.Post, @@ -147,6 +172,7 @@ describe("oidc authorization", () => { codeVerifier, redirectUri, delegatedAuthConfig, + nonce, }); expect(result).toEqual(validBearerTokenResponse); @@ -171,6 +197,7 @@ describe("oidc authorization", () => { codeVerifier, redirectUri, delegatedAuthConfig, + nonce, }); // results in token that uses 'Bearer' token type @@ -187,7 +214,13 @@ describe("oidc authorization", () => { { overwriteRoutes: true }, ); await expect(() => - completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig }), + completeAuthorizationCodeGrant(code, { + clientId, + codeVerifier, + redirectUri, + delegatedAuthConfig, + nonce, + }), ).rejects.toThrow(new Error(OidcError.CodeExchangeFailed)); }); @@ -202,8 +235,31 @@ describe("oidc authorization", () => { { overwriteRoutes: true }, ); await expect(() => - completeAuthorizationCodeGrant(code, { clientId, codeVerifier, redirectUri, delegatedAuthConfig }), + completeAuthorizationCodeGrant(code, { + clientId, + codeVerifier, + redirectUri, + delegatedAuthConfig, + nonce, + }), ).rejects.toThrow(new Error(OidcError.InvalidBearerTokenResponse)); }); + + it("should throw invalid id token error when id_token is invalid", async () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + // invalid audience + aud: "something-else", + }); + await expect(() => + completeAuthorizationCodeGrant(code, { + clientId, + codeVerifier, + redirectUri, + delegatedAuthConfig, + nonce, + }), + ).rejects.toThrow(new Error(OidcError.InvalidIdToken)); + }); }); }); diff --git a/spec/unit/oidc/validate.spec.ts b/spec/unit/oidc/validate.spec.ts index d5091ed89f9..71e4aeb7c0a 100644 --- a/spec/unit/oidc/validate.spec.ts +++ b/spec/unit/oidc/validate.spec.ts @@ -14,11 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; +import jwtDecode from "jwt-decode"; + import { M_AUTHENTICATION } from "../../../src"; import { logger } from "../../../src/logger"; -import { validateOIDCIssuerWellKnown, validateWellKnownAuthentication } from "../../../src/oidc/validate"; +import { + validateIdToken, + validateOIDCIssuerWellKnown, + validateWellKnownAuthentication, +} from "../../../src/oidc/validate"; import { OidcError } from "../../../src/oidc/error"; +jest.mock("jwt-decode"); + describe("validateWellKnownAuthentication()", () => { const baseWk = { "m.homeserver": { @@ -194,3 +203,96 @@ describe("validateOIDCIssuerWellKnown", () => { expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport); }); }); + +describe("validateIdToken()", () => { + const nonce = "test-nonce"; + const issuer = "https://auth.org/issuer"; + const clientId = "test-client-id"; + const idToken = "test-id-token"; + + const validDecodedIdToken = { + // nonce matches + nonce, + // not expired + exp: Date.now() / 1000 + 5555, + // audience is this client + aud: clientId, + // issuer matches + iss: issuer, + }; + beforeEach(() => { + mocked(jwtDecode).mockClear().mockReturnValue(validDecodedIdToken); + + jest.spyOn(logger, "error").mockClear(); + }); + + it("should throw when idToken is falsy", () => { + expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + }); + + it("should throw when idToken cannot be decoded", () => { + mocked(jwtDecode).mockImplementation(() => { + throw new Error("oh no!"); + }); + expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + }); + + it("should throw when issuer does not match", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + iss: "https://badissuer.com", + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid issuer")); + }); + + it("should throw when audience does not include clientId", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + aud: "qwerty,uiop,asdf", + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience")); + }); + + it("should throw when audience includes clientId and other audiences", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + aud: `${clientId},uiop,asdf`, + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience")); + }); + + it("should throw when nonce does not match", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + nonce: "something else", + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid nonce")); + }); + + it("should throw when token does not have an expiry", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + exp: undefined, + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry")); + }); + + it("should throw when token is expired", () => { + mocked(jwtDecode).mockReturnValue({ + ...validDecodedIdToken, + // expired in the past + exp: Date.now() / 1000 - 777, + }); + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken)); + expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry")); + }); + + it("should not throw for a valid id token", () => { + expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow(); + }); +}); diff --git a/src/oidc/authorize.ts b/src/oidc/authorize.ts index 26211a3d486..9b7fc19a47c 100644 --- a/src/oidc/authorize.ts +++ b/src/oidc/authorize.ts @@ -20,7 +20,7 @@ import { subtleCrypto, TextEncoder } from "../crypto/crypto"; import { logger } from "../logger"; import { randomString } from "../randomstring"; import { OidcError } from "./error"; -import { ValidatedIssuerConfig } from "./validate"; +import { validateIdToken, ValidatedIssuerConfig } from "./validate"; /** * Authorization parameters which are used in the authentication request of an OIDC auth code flow. @@ -173,11 +173,13 @@ export const completeAuthorizationCodeGrant = async ( codeVerifier, redirectUri, delegatedAuthConfig, + nonce, }: { clientId: string; codeVerifier: string; redirectUri: string; delegatedAuthConfig: IDelegatedAuthConfig & ValidatedIssuerConfig; + nonce: string; }, ): Promise => { const params = new URLSearchParams(); @@ -203,6 +205,8 @@ export const completeAuthorizationCodeGrant = async ( const token = await response.json(); if (isValidBearerTokenResponse(token)) { + // throws when token is invalid + validateIdToken(token.id_token, delegatedAuthConfig.issuer, clientId, nonce); return normalizeBearerTokenResponseTokenType(token); } diff --git a/src/oidc/error.ts b/src/oidc/error.ts index 6e70283a6ca..233ae34c519 100644 --- a/src/oidc/error.ts +++ b/src/oidc/error.ts @@ -23,5 +23,6 @@ export enum OidcError { DynamicRegistrationFailed = "Dynamic registration failed", DynamicRegistrationInvalid = "Dynamic registration invalid response", CodeExchangeFailed = "Failed to exchange code for token", - InvalidBearerTokenResponse = "Invalid bearer token", + InvalidBearerTokenResponse = "Invalid bearer token response", + InvalidIdToken = "Invalid ID token", } diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 09ecf5e609d..a378b4fd76c 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import jwtDecode from "jwt-decode"; + import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client"; import { logger } from "../logger"; import { OidcError } from "./error"; @@ -116,3 +118,88 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer logger.error("Issuer configuration not valid"); throw new Error(OidcError.OpSupport); }; + +/** + * Standard ID Token claims. + * + * @public + * @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ +export interface IdTokenClaims extends JwtClaims { + /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string. */ + nonce?: string; +} + +/** + * Standard JWT claims. + * + * @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 + */ +export interface JwtClaims { + [claim: string]: unknown; + + /** The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. */ + iss?: string; + /** The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. */ + sub?: string; + /** The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. */ + aud?: string | string[]; + /** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. */ + exp?: number; + /** The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. */ + nbf?: number; + /** The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. */ + iat?: number; + /** The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" claim can be used to prevent the JWT from being replayed. The "jti" value is a case-sensitive string. */ + jti?: string; +} + +const decodeIdToken = (token: string): IdTokenClaims => { + try { + return jwtDecode(token); + } catch (error) { + logger.error("Could not decode id_token", error); + throw error; + } +}; + +/** + * https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + * @param idToken - id token from token endpoint + * @param issuer - issuer for the OP as found during discovery + * @param clientId - this client's id as registered with the OP + * @param nonce - nonce used in the authentication request + * @throws when id token is invalid + */ +export const validateIdToken = (idToken: string | undefined, issuer: string, clientId: string, nonce: string): void => { + try { + if (!idToken) { + throw new Error("No ID token"); + } + const claims = decodeIdToken(idToken); + + // The Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim. + if (claims.iss !== issuer) { + throw new Error("Invalid issuer"); + } + // The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. + // Don't accept tokens with other untrusted audiences + if (claims.aud !== clientId) { + throw new Error("Invalid audience"); + } + + // If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific. + if (claims.nonce !== nonce) { + throw new Error("Invalid nonce"); + } + + // The current time MUST be before the time represented by the exp Claim. + // exp is an epoch timestamp in seconds + if (!claims.exp || Date.now() > claims.exp * 1000) { + throw new Error("Invalid expiry"); + } + } catch (error) { + logger.error("Invalid ID token", error); + throw new Error(OidcError.InvalidIdToken); + } +}; diff --git a/yarn.lock b/yarn.lock index 43ad61ac39a..f6c1580c5a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5393,6 +5393,11 @@ jstransformer@1.0.0: is-promise "^2.0.0" promise "^7.0.1" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" From 9b1c67f76b448c2fcf3eb08fc143ff2e57fc6299 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 29 Jun 2023 16:21:17 +1200 Subject: [PATCH 2/3] comments --- src/oidc/validate.ts | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index a378b4fd76c..928335784ad 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -119,39 +119,25 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer throw new Error(OidcError.OpSupport); }; -/** - * Standard ID Token claims. - * - * @public - * @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken - */ -export interface IdTokenClaims extends JwtClaims { - /** String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string. */ - nonce?: string; -} - /** * Standard JWT claims. * * @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 */ -export interface JwtClaims { +interface JwtClaims { [claim: string]: unknown; - - /** The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. */ + /** The "iss" (issuer) claim identifies the principal that issued the JWT. */ iss?: string; - /** The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. */ + /** The "sub" (subject) claim identifies the principal that is the subject of the JWT. */ sub?: string; - /** The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. */ + /** The "aud" (audience) claim identifies the recipients that the JWT is intended for. */ aud?: string | string[]; - /** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. */ + /** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. */ exp?: number; - /** The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the "nbf" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. */ - nbf?: number; - /** The "iat" (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. */ - iat?: number; - /** The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" claim can be used to prevent the JWT from being replayed. The "jti" value is a case-sensitive string. */ - jti?: string; + // unused claims excluded +} +interface IdTokenClaims extends JwtClaims { + nonce?: string; } const decodeIdToken = (token: string): IdTokenClaims => { @@ -164,6 +150,7 @@ const decodeIdToken = (token: string): IdTokenClaims => { }; /** + * Validate idToken * https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation * @param idToken - id token from token endpoint * @param issuer - issuer for the OP as found during discovery From 48053c521c5d7ca8c72fb3a7901497cc3c8f4927 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 29 Jun 2023 16:30:55 +1200 Subject: [PATCH 3/3] tidy comments --- src/oidc/validate.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/oidc/validate.ts b/src/oidc/validate.ts index 928335784ad..8519b2bf118 100644 --- a/src/oidc/validate.ts +++ b/src/oidc/validate.ts @@ -85,7 +85,7 @@ const requiredArrayValue = (wellKnown: Record, key: string, val }; /** - * Validates issue `.well-known/openid-configuration` + * Validates issuer `.well-known/openid-configuration` * As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html * validates that OP is compatible with Element's OIDC flow * @param wellKnown - json object @@ -169,19 +169,28 @@ export const validateIdToken = (idToken: string | undefined, issuer: string, cli if (claims.iss !== issuer) { throw new Error("Invalid issuer"); } - // The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. - // Don't accept tokens with other untrusted audiences + /** + * The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. + * The aud (audience) Claim MAY contain an array with more than one element. + * The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. + * EW: Don't accept tokens with other untrusted audiences + * */ if (claims.aud !== clientId) { throw new Error("Invalid audience"); } - // If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific. + /** + * If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked + * to verify that it is the same value as the one that was sent in the Authentication Request. + */ if (claims.nonce !== nonce) { throw new Error("Invalid nonce"); } - // The current time MUST be before the time represented by the exp Claim. - // exp is an epoch timestamp in seconds + /** + * The current time MUST be before the time represented by the exp Claim. + * exp is an epoch timestamp in seconds + * */ if (!claims.exp || Date.now() > claims.exp * 1000) { throw new Error("Invalid expiry"); }