From 5139c43fcf6d3907434aea32b7ba9530062701b2 Mon Sep 17 00:00:00 2001 From: Kris Thom White Date: Tue, 28 May 2024 10:57:08 -0500 Subject: [PATCH] feat: support dev Okta JWT tokens (#1790) --- src/events/http/HttpServer.js | 2 +- src/events/http/createJWTAuthScheme.js | 9 +++---- .../LambdaProxyIntegrationEvent.js | 4 +-- .../LambdaProxyIntegrationEventV2.js | 4 +-- .../jwt-authorizer/jwt-authorizer.test.js | 27 +++++++++++++++++++ .../integration/jwt-authorizer/serverless.yml | 14 ++++++++++ 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 394f1a010..59e07b225 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -459,7 +459,7 @@ export default class HttpServer { request.payload = request.payload && request.payload.toString(encoding) request.rawPayload = request.payload - // incomming request message + // incoming request message log.notice() log.notice() diff --git a/src/events/http/createJWTAuthScheme.js b/src/events/http/createJWTAuthScheme.js index 086b10363..9e8c581ab 100644 --- a/src/events/http/createJWTAuthScheme.js +++ b/src/events/http/createJWTAuthScheme.js @@ -5,7 +5,7 @@ import { log } from "../../utils/log.js" const { isArray } = Array const { now } = Date -export default function createAuthScheme(jwtOptions) { +export default function createJWTAuthScheme(jwtOptions) { const authorizerName = jwtOptions.name const identitySourceMatch = /^\$request.header.((?:\w+-?)+\w+)$/.exec( @@ -43,7 +43,7 @@ export default function createAuthScheme(jwtOptions) { return Boom.unauthorized("JWT Token expired") } - const { aud, iss, scope, client_id: clientId } = claims + const { aud, iss, scope, scp, client_id: clientId } = claims if (iss !== jwtOptions.issuerUrl) { log.notice(`JWT Token not from correct issuer url`) @@ -68,13 +68,13 @@ export default function createAuthScheme(jwtOptions) { let scopes = null if (jwtOptions.scopes && jwtOptions.scopes.length > 0) { - if (!scope) { + if (!scope && !scp) { log.notice(`JWT Token missing valid scope`) return Boom.forbidden("JWT Token missing valid scope") } - scopes = scope.split(" ") + scopes = scp || scope.split(" ") if (scopes.every((s) => !jwtOptions.scopes.includes(s))) { log.notice(`JWT Token missing valid scope`) @@ -85,7 +85,6 @@ export default function createAuthScheme(jwtOptions) { log.notice(`JWT Token validated`) // Set the credentials for the rest of the pipeline - // return resolve( return h.authenticated({ credentials: { claims, diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js index 9cf1a2455..e3f25522d 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js @@ -134,8 +134,8 @@ export default class LambdaProxyIntegrationEvent { if (token) { try { claims = decodeJwt(token) - if (claims.scope) { - scopes = claims.scope.split(" ") + if (claims.scp || claims.scope) { + scopes = claims.scp || claims.scope.split(" ") // In AWS HTTP Api the scope property is removed from the decoded JWT // I'm leaving this property because I'm not sure how all of the authorizers // for AWS REST Api handle JWT. diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js index eda1027b5..0ef6462e0 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js @@ -120,8 +120,8 @@ export default class LambdaProxyIntegrationEventV2 { if (token) { try { claims = decodeJwt(token) - if (claims.scope) { - scopes = claims.scope.split(" ") + if (claims.scp || claims.scope) { + scopes = claims.scp || claims.scope.split(" ") // In AWS HTTP Api the scope property is removed from the decoded JWT // I'm leaving this property because I'm not sure how all of the authorizers // for AWS REST Api handle JWT. diff --git a/tests/integration/jwt-authorizer/jwt-authorizer.test.js b/tests/integration/jwt-authorizer/jwt-authorizer.test.js index fe1c5e58f..4dd3ad4ee 100644 --- a/tests/integration/jwt-authorizer/jwt-authorizer.test.js +++ b/tests/integration/jwt-authorizer/jwt-authorizer.test.js @@ -29,6 +29,20 @@ const baseJWT = { version: 2, } +const oktaJWT = { + aud: "api://default", + auth_time: floor(now() / 1000), + cid: "ZjE4ZGVlYzUtMDU1Ni00", + eid: "5d6f052a03414da69c500", + exp: floor(now() / 1000) + 5000, + iat: floor(now() / 1000), + iss: "https://dev-00000000.okta.com/oauth2/default", + jti: "9a2f8ae5-9a8d-4d88-be36-bc0a1e042718", + scp: ["email", "profile", "openid"], + sub: "user@example.com", + ver: 1, +} + const expiredJWT = { ...baseJWT, exp: floor(now() / 1000) - 2000, @@ -149,6 +163,19 @@ describe("jwt authorizer tests", function desc() { path: "/user2", status: 200, }, + { + description: "Valid Okta format JWT with scp for scopes", + expected: { + requestContext: { + claims: oktaJWT, + scopes: ["email", "profile", "openid"], + }, + status: "authorized", + }, + jwt: oktaJWT, + path: "/user3", + status: 200, + }, { description: "Expired JWT", expected: { diff --git a/tests/integration/jwt-authorizer/serverless.yml b/tests/integration/jwt-authorizer/serverless.yml index 65e7a432a..303a2b877 100644 --- a/tests/integration/jwt-authorizer/serverless.yml +++ b/tests/integration/jwt-authorizer/serverless.yml @@ -16,6 +16,11 @@ provider: - ZjE4ZGVlYzUtMDU1Ni00ZWM4LThkMDAtYTlkMmIzNWE4NTNj identitySource: $request.header.Authorization issuerUrl: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_notreal + oktaJwtAuthorizer: + audience: + - "api://default" + identitySource: $request.header.Authorization + issuerUrl: https://dev-00000000.okta.com/oauth2/default payload: "1.0" memorySize: 1024 name: aws @@ -39,4 +44,13 @@ functions: - email method: get path: /user2 + - httpApi: + authorizer: + name: oktaJwtAuthorizer + scopes: + - openid + - profile + - email + method: get + path: /user3 handler: src/handler.user