From cff5dba6f21124f650ddc3acdd20043f27d9e7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emiliano=20Su=C3=B1=C3=A9?= Date: Tue, 3 Nov 2020 15:19:04 -0800 Subject: [PATCH 1/3] Clean up hooks code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Emiliano Suñé --- api/src/utils/hooks.ts | 98 +++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/api/src/utils/hooks.ts b/api/src/utils/hooks.ts index 0fc598bb..6441ee4b 100644 --- a/api/src/utils/hooks.ts +++ b/api/src/utils/hooks.ts @@ -1,8 +1,4 @@ -import { - Forbidden, - MethodNotAllowed, - NotAuthenticated, -} from "@feathersjs/errors"; +import { Forbidden, MethodNotAllowed } from "@feathersjs/errors"; import { HookContext } from "@feathersjs/feathers"; import { decode, verify } from "jsonwebtoken"; import jwks, { ClientOptions, JwksClient, SigningKey } from "jwks-rsa"; @@ -52,33 +48,19 @@ export async function verifyJWT(context: HookContext) { return Promise.reject(new Forbidden("The authorization header is missing")); } const token = authHeader.split(" ")[1]; - - // fetch public key from JWKS url and verify token - const jwksOptions = { - jwksUri: context.app.get("authentication").jwksUri, - } as ClientOptions; - const client = jwks(jwksOptions) as JwksClient; - const keys = (await client.getSigningKeysAsync()) as SigningKey[]; - - let decoded; - try { - decoded = verify(token, keys[0].getPublicKey(), { - algorithms: context.app.get("authentication").algorithms, - }); - } catch (error) { - throw new NotAuthenticated(`Authentication failed: ${error.message}`); - } + const keys = await getAuthSigningKeys(context); + decodeIdToken(token, keys, context); return context; } export function verifyJWTRoles(roles: string[]) { return async (context: HookContext) => { - const authHeader = context.params.headers?.authorization as string; - if (!authHeader) { + const token = extractIdToken( + context.params.headers?.authorization as string + ); + if (!token) { throw new Forbidden("The authorization header is missing"); } - const token = authHeader.split(" ")[1]; - const decoded = decode(token) as { [key: string]: any; }; @@ -95,12 +77,12 @@ export function verifyJWTRoles(roles: string[]) { export function setRequestUser(field: string) { return async (context: HookContext) => { - const authHeader = context.params.headers?.authorization as string; - if (!authHeader) { + const token = extractIdToken( + context.params.headers?.authorization as string + ); + if (!token) { throw new Forbidden("The authorization header is missing"); } - const token = authHeader.split(" ")[1]; - const decoded = decode(token) as { [key: string]: any; }; @@ -112,16 +94,50 @@ export function setRequestUser(field: string) { export async function validateCredentialRequest(context: HookContext) { const dbClient = (await context.app.get("mongoClient")) as Db; - const authHeader = context.params.headers?.authorization as string; - const idToken = authHeader.split(" ")[1]; + const idToken = extractIdToken( + context.params.headers?.authorization as string + ); + const keys = await getAuthSigningKeys(context); + + const decoded = decodeIdToken(idToken, keys, context); + const inviteToken = await dbClient + .collection("issuer-invite") + .findOne({ token: context.data.token }); + const requestedSchema = context.app + .get("public-schemas") + .get(context.data.schema_id || "default"); + if (!decoded && !inviteToken && !requestedSchema) { + throw new Forbidden( + "The requested action could not be completed. Please check your settings and ensure you are either authenticated or requesting a public schema." + ); + } + return context; +} + +async function getAuthSigningKeys(context: HookContext): Promise { + if (!context.app.get("authentication")) { + logger.debug( + "The [authentication] section is missing in the configuration" + ); + return [] as SigningKey[]; + } // fetch public key from JWKS url and verify token const jwksOptions = { jwksUri: context.app.get("authentication").jwksUri, } as ClientOptions; const oidcClient = jwks(jwksOptions) as JwksClient; - const keys = (await oidcClient.getSigningKeysAsync()) as SigningKey[]; + return (await oidcClient.getSigningKeysAsync()) as SigningKey[]; +} +function decodeIdToken( + idToken: string | undefined, + keys: SigningKey[], + context: HookContext +): string | undefined | object { + if (!idToken || !context.app.get("authentication") || keys.length === 0) { + return undefined; + } let decoded; try { decoded = verify(idToken, keys[0].getPublicKey(), { @@ -130,18 +146,12 @@ export async function validateCredentialRequest(context: HookContext) { } catch (error) { decoded = undefined; } + return decoded; +} - const inviteToken = await dbClient - .collection("issuer-invite") - .findOne({ token: context.data.token }); - const requestedSchema = context.app - .get("public-schemas") - .get(context.data.schema_id || "default"); - - if (!decoded && !inviteToken && !requestedSchema) { - throw new Forbidden( - "The requested action could not be completed. Please check your settings and ensure you are either authenticated or requesting a public schema." - ); +function extractIdToken(authHeader: string | undefined): string | undefined { + if (!authHeader || authHeader.split(" ").length === 1) { + return undefined; } - return context; + return authHeader.split(" ")[1]; } From cd099342b07b0e919c86ba7d48f6b2b9b5dde55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emiliano=20Su=C3=B1=C3=A9?= Date: Wed, 4 Nov 2020 10:52:52 -0800 Subject: [PATCH 2/3] Tweaks to utils/hooks.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Emiliano Suñé --- api/src/utils/hooks.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/api/src/utils/hooks.ts b/api/src/utils/hooks.ts index 6441ee4b..bbc52ada 100644 --- a/api/src/utils/hooks.ts +++ b/api/src/utils/hooks.ts @@ -43,13 +43,14 @@ export async function canDeleteInvite(context: HookContext) { } export async function verifyJWT(context: HookContext) { - const authHeader = context.params.headers?.authorization as string; - if (!authHeader) { - return Promise.reject(new Forbidden("The authorization header is missing")); + const token = extractIdToken( + context.params.headers?.authorization as string + ); + if (!token) { + throw new Forbidden("The authorization header is missing"); } - const token = authHeader.split(" ")[1]; const keys = await getAuthSigningKeys(context); - decodeIdToken(token, keys, context); + verifyIdToken(token, keys, context); return context; } @@ -99,7 +100,7 @@ export async function validateCredentialRequest(context: HookContext) { ); const keys = await getAuthSigningKeys(context); - const decoded = decodeIdToken(idToken, keys, context); + const decoded = verifyIdToken(idToken, keys, context); const inviteToken = await dbClient .collection("issuer-invite") .findOne({ token: context.data.token }); @@ -130,7 +131,7 @@ async function getAuthSigningKeys(context: HookContext): Promise { return (await oidcClient.getSigningKeysAsync()) as SigningKey[]; } -function decodeIdToken( +function verifyIdToken( idToken: string | undefined, keys: SigningKey[], context: HookContext From 01c8e0e0fbbde2df1db2c3e3fb9e206ed4ee5f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emiliano=20Su=C3=B1=C3=A9?= Date: Wed, 4 Nov 2020 12:41:28 -0800 Subject: [PATCH 3/3] Log token validation error to console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Emiliano Suñé --- api/src/utils/hooks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/utils/hooks.ts b/api/src/utils/hooks.ts index bbc52ada..2b5416a7 100644 --- a/api/src/utils/hooks.ts +++ b/api/src/utils/hooks.ts @@ -145,6 +145,7 @@ function verifyIdToken( algorithms: context.app.get("authentication").algorithms, }); } catch (error) { + logger.error(error); decoded = undefined; } return decoded;