From ecf8aa983a2ead57593e201e922ee589ecf5e19f Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Mon, 19 Jul 2021 20:43:19 -0400 Subject: [PATCH] Adds a hasRole() and isAuthenticated() to determine the role membership or if the request is authenticated (#3006) * Move parseJWT * Adds isAuthenticated and hasRole to auth.js * Moves parseJWT from api into api/auth * Reformat auth.js template * Update role and auth check logic * Use roles instead of role * Use roles * Default auth ts uses roles * Update auth templates to use roles instead of role * Updates templates for other providers * Updates dbauth auth template * Simplify hasRole logic * Update packages/cli/src/commands/setup/auth/templates/auth.js.template Co-authored-by: Tobbe Lundberg * Update packages/cli/src/commands/setup/auth/templates/auth.js.template Co-authored-by: Tobbe Lundberg * Clarify hasRole return value if no roles provided Co-authored-by: Tobbe Lundberg --- packages/api/src/auth/index.ts | 2 + .../src/{functions => auth}/parseJWT.test.ts | 10 +- packages/api/src/{ => auth}/parseJWT.ts | 0 packages/api/src/index.ts | 1 - .../setup/auth/templates/auth.js.template | 69 ++++++++---- .../azureActiveDirectory.auth.js.template | 103 +++++++++++------- .../auth/templates/dbAuth.auth.js.template | 67 +++++++++--- .../auth/templates/ethereum.auth.js.template | 9 ++ .../auth/templates/firebase.auth.js.template | 9 ++ .../template/api/src/lib/auth.ts | 10 +- 10 files changed, 198 insertions(+), 82 deletions(-) rename packages/api/src/{functions => auth}/parseJWT.test.ts (94%) rename packages/api/src/{ => auth}/parseJWT.ts (100%) diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index 30d2e6ea2f05..34e070e93e47 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -1,3 +1,5 @@ +export * from './parseJWT' + import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' import type { SupportedAuthTypes } from '@redwoodjs/auth' diff --git a/packages/api/src/functions/parseJWT.test.ts b/packages/api/src/auth/parseJWT.test.ts similarity index 94% rename from packages/api/src/functions/parseJWT.test.ts rename to packages/api/src/auth/parseJWT.test.ts index ce397c685c11..8d4d652b14f2 100644 --- a/packages/api/src/functions/parseJWT.test.ts +++ b/packages/api/src/auth/parseJWT.test.ts @@ -1,4 +1,4 @@ -import { parseJWT } from '../parseJWT' +import { parseJWT } from './parseJWT' const JWT_CLAIMS = { iss: 'https://app.us.auth0.com/', @@ -40,22 +40,22 @@ const JWT_WITH_ROLES_CLAIM = { describe('parseJWT', () => { describe('handle empty token cases', () => { - test('it handles null token and returns empty appMetdata and roles', () => { + test('it handles null token and returns empty appMetadata and roles', () => { const token = { decoded: null, namespace: null } expect(parseJWT(token)).toEqual({ appMetadata: {}, roles: [] }) }) - test('it handles an undefined token and returns empty appMetdata and roles', () => { + test('it handles an undefined token and returns empty appMetadata and roles', () => { const token = { decoded: undefined, namespace: undefined } expect(parseJWT(token)).toEqual({ appMetadata: {}, roles: [] }) }) - test('it handles an undefined decoded token and returns empty appMetdata and roles', () => { + test('it handles an undefined decoded token and returns empty appMetadata and roles', () => { const token = { decoded: undefined, namespace: null } expect(parseJWT(token)).toEqual({ appMetadata: {}, roles: [] }) }) - test('it handles an undefined namespace in token and returns empty appMetdata and roles', () => { + test('it handles an undefined namespace in token and returns empty appMetadata and roles', () => { const token = { decoded: null, namespace: undefined } expect(parseJWT(token)).toEqual({ appMetadata: {}, roles: [] }) }) diff --git a/packages/api/src/parseJWT.ts b/packages/api/src/auth/parseJWT.ts similarity index 100% rename from packages/api/src/parseJWT.ts rename to packages/api/src/auth/parseJWT.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 3d33e8cbf2ba..673d0e204507 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,7 +8,6 @@ export * from './makeServices' export * from './makeMergedSchema/makeMergedSchema' export * from './functions/graphql' export * from './globalContext' -export * from './parseJWT' export * from './types' export * from './functions/dbAuth/DbAuthHandler' export { dbAuthSession } from './functions/dbAuth/shared' diff --git a/packages/cli/src/commands/setup/auth/templates/auth.js.template b/packages/cli/src/commands/setup/auth/templates/auth.js.template index deffa2ef1d42..05306b32684a 100644 --- a/packages/cli/src/commands/setup/auth/templates/auth.js.template +++ b/packages/cli/src/commands/setup/auth/templates/auth.js.template @@ -1,4 +1,3 @@ - import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api' /** @@ -13,42 +12,72 @@ import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api' * * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples */ -export const getCurrentUser = async (decoded, { _token, _type }, { _event, _context }) => { +export const getCurrentUser = async ( + decoded, + { _token, _type }, + { _event, _context } +) => { return { ...decoded, roles: parseJWT({ decoded }).roles } } +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param {string | string[]} roles - A single role or list of roles to check if the user belongs to + * + * @returns {boolean} - Returns true if the currentUser is authenticated (and assigned one of the given roles) + * or false if not (or no roles provided) + */ +export const hasRole = ({ roles }) => { + if (!isAuthenticated()) { + return false + } + + if(!!roles) { + if (Array.isArray(roles) { + return context.currentUser.roles?.some((r) => roles.includes(r)) + } + + if (typeof roles === 'string') { + return context.currentUser.roles?.includes(roles) + } + + // roles not found + return false + } + + return false +} + /** * Use requireAuth in your services to check that a user is logged in, * whether or not they are assigned a role, and optionally raise an * error if they're not. * - * @param {string=} roles - An optional role or list of roles - * @param {string[]=} roles - An optional list of roles - * @returns {boolean} - If the currentUser is authenticated (and assigned one of the given roles) + * @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to + * + * @returns - If the currentUser is authenticated (and assigned one of the given roles) * * @throws {AuthenticationError} - If the currentUser is not authenticated * @throws {ForbiddenError} If the currentUser is not allowed due to role permissions * * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples */ -export const requireAuth = ({ role } = {}) => { - if (!context.currentUser) { +export const requireAuth = ({ roles } = {}) => { + if (!isAuthenticated) { throw new AuthenticationError("You don't have permission to do that.") } - if ( - typeof role !== 'undefined' && - typeof role === 'string' && - !context.currentUser.roles?.includes(role) - ) { - throw new ForbiddenError("You don't have access to do that.") - } - - if ( - typeof role !== 'undefined' && - Array.isArray(role) && - !context.currentUser.roles?.some((r) => role.includes(r)) - ) { + if (!hasRole({ roles })) { throw new ForbiddenError("You don't have access to do that.") } } diff --git a/packages/cli/src/commands/setup/auth/templates/azureActiveDirectory.auth.js.template b/packages/cli/src/commands/setup/auth/templates/azureActiveDirectory.auth.js.template index 110654b49418..babb240f2a2b 100644 --- a/packages/cli/src/commands/setup/auth/templates/azureActiveDirectory.auth.js.template +++ b/packages/cli/src/commands/setup/auth/templates/azureActiveDirectory.auth.js.template @@ -1,61 +1,82 @@ -// Define what you want `currentUser` to return throughout your app. For example, -// to return a real user from your database, you could do something like: -// -// export const getCurrentUser = async ({ email }) => { -// return await db.user.findUnique({ where: { email } }) -// } - import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api' -export const getCurrentUser = async (decoded, { token, type }) => { - return { - email: decoded.preferred_username ?? null, - ...decoded, - roles: parseJWT({ decoded }).roles - } +/** + * getCurrentUser returns the user information together with + * an optional collection of roles used by requireAuth() to check + * if the user is authenticated or has role-based access + * + * @param decoded - The decoded access token containing user info and JWT claims like `sub` + * @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type + * @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker + * such as headers and cookies, and the context information about the invocation such as IP Address + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const getCurrentUser = async ( + decoded, + { _token, _type }, + { _event, _context } +) => { + return { ...decoded, roles: parseJWT({ decoded }).roles } } -// Use this function in your services to check that a user is logged in, and -// optionally raise an error if they're not. +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to + * + * @returns {boolean} - Returns true if the currentUser is authenticated (and assigned one of the given roles) + */ +export const hasRole = ({ roles }) => { + if (!isAuthenticated()) { + return false + } + + if(!!roles) { + if (Array.isArray(roles) { + return context.currentUser.roles?.some((r) => roles.includes(r)) + } + + if (typeof roles === 'string') { + return context.currentUser.roles?.includes(roles) + } + + // roles not found + return false + } + + return false +} /** * Use requireAuth in your services to check that a user is logged in, * whether or not they are assigned a role, and optionally raise an * error if they're not. * - * @param {string=} roles - An optional role or list of roles - * @param {string[]=} roles - An optional list of roles - - * @example + * @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to * - * // checks if currentUser is authenticated - * requireAuth() + * @returns - If the currentUser is authenticated (and assigned one of the given roles) * - * @example + * @throws {AuthenticationError} - If the currentUser is not authenticated + * @throws {ForbiddenError} If the currentUser is not allowed due to role permissions * - * // checks if currentUser is authenticated and assigned one of the given roles - * requireAuth({ role: 'admin' }) - * requireAuth({ role: ['editor', 'author'] }) - * requireAuth({ role: ['publisher'] }) + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples */ -export const requireAuth = ({ role } = {}) => { - if (!context.currentUser) { +export const requireAuth = ({ roles } = {}) => { + if (!isAuthenticated) { throw new AuthenticationError("You don't have permission to do that.") } - if ( - typeof role !== 'undefined' && - typeof role === 'string' && - !context.currentUser.roles?.includes(role) - ) { - throw new ForbiddenError("You don't have access to do that.") - } - - if ( - typeof role !== 'undefined' && - Array.isArray(role) && - !context.currentUser.roles?.some((r) => role.includes(r)) - ) { + if (!hasRole({ roles })) { throw new ForbiddenError("You don't have access to do that.") } } diff --git a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.js.template b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.js.template index 31b8eeb6ec3c..b1f4d39910b2 100644 --- a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.js.template +++ b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.js.template @@ -15,24 +15,63 @@ export const getCurrentUser = async (session) => { return await db.user.findUnique({ where: { id: session.id } }) } -export const requireAuth = ({ role } = {}) => { - if (!context.currentUser) { - throw new AuthenticationError("You don't have permission to do that.") +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to + * + * @returns {boolean} - Returns true if the currentUser is authenticated (and assigned one of the given roles) + */ +export const hasRole = ({ roles }) => { + if (!isAuthenticated()) { + return false } - if ( - typeof role !== 'undefined' && - typeof role === 'string' && - !context.currentUser.roles?.includes(role) - ) { - throw new ForbiddenError("You don't have access to do that.") + if(!!roles) { + if (Array.isArray(roles) { + return context.currentUser.roles?.some((r) => roles.includes(r)) + } + + if (typeof roles === 'string') { + return context.currentUser.roles?.includes(roles) + } + + // roles not found + return false + } + + return false +} + +/** + * Use requireAuth in your services to check that a user is logged in, + * whether or not they are assigned a role, and optionally raise an + * error if they're not. + * + * @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to + * + * @returns - If the currentUser is authenticated (and assigned one of the given roles) + * + * @throws {AuthenticationError} - If the currentUser is not authenticated + * @throws {ForbiddenError} If the currentUser is not allowed due to role permissions + * + * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples + */ +export const requireAuth = ({ roles } = {}) => { + if (!isAuthenticated) { + throw new AuthenticationError("You don't have permission to do that.") } - if ( - typeof role !== 'undefined' && - Array.isArray(role) && - !context.currentUser.roles?.some((r) => role.includes(r)) - ) { + if (!hasRole({ roles })) { throw new ForbiddenError("You don't have access to do that.") } } diff --git a/packages/cli/src/commands/setup/auth/templates/ethereum.auth.js.template b/packages/cli/src/commands/setup/auth/templates/ethereum.auth.js.template index 3059fa9e2066..0a635668a220 100644 --- a/packages/cli/src/commands/setup/auth/templates/ethereum.auth.js.template +++ b/packages/cli/src/commands/setup/auth/templates/ethereum.auth.js.template @@ -9,6 +9,15 @@ export const getCurrentUser = async (decoded) => { return db.user.findUnique({ where: { address: decoded.address } }) } +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + // Use this function in your services to check that a user is logged in, and // optionally raise an error if they're not. diff --git a/packages/cli/src/commands/setup/auth/templates/firebase.auth.js.template b/packages/cli/src/commands/setup/auth/templates/firebase.auth.js.template index 6210e0a3da13..340772e17b35 100644 --- a/packages/cli/src/commands/setup/auth/templates/firebase.auth.js.template +++ b/packages/cli/src/commands/setup/auth/templates/firebase.auth.js.template @@ -27,6 +27,15 @@ export const getCurrentUser = async (decoded, { token, type }) => { return { email, uid } } +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = () => { + return !!context.currentUser +} + // Use this function in your services to check that a user is logged in, and // optionally raise an error if they're not. diff --git a/packages/create-redwood-app/template/api/src/lib/auth.ts b/packages/create-redwood-app/template/api/src/lib/auth.ts index 6ac573934480..3cc4c0b1b699 100644 --- a/packages/create-redwood-app/template/api/src/lib/auth.ts +++ b/packages/create-redwood-app/template/api/src/lib/auth.ts @@ -8,6 +8,14 @@ * See https://redwoodjs.com/docs/authentication for more info. */ -export const requireAuth = () => { +export const isAuthenticated = () => { return true } + +export const hasRole = ({ roles }) => { + return roles !== undefined +} + +export const requireAuth = () => { + return isAuthenticated() +}