Skip to content

Commit

Permalink
Adds a hasRole() and isAuthenticated() to determine the role membersh…
Browse files Browse the repository at this point in the history
…ip 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 <tobbe@tlundberg.com>

* Update packages/cli/src/commands/setup/auth/templates/auth.js.template

Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>

* Clarify hasRole return value if no roles provided

Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
  • Loading branch information
dthyresson and Tobbe committed Jul 20, 2021
1 parent f61940a commit ecf8aa9
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 82 deletions.
2 changes: 2 additions & 0 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './parseJWT'

import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'

import type { SupportedAuthTypes } from '@redwoodjs/auth'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseJWT } from '../parseJWT'
import { parseJWT } from './parseJWT'

const JWT_CLAIMS = {
iss: 'https://app.us.auth0.com/',
Expand Down Expand Up @@ -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: [] })
})
Expand Down
File renamed without changes.
1 change: 0 additions & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
69 changes: 49 additions & 20 deletions packages/cli/src/commands/setup/auth/templates/auth.js.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'

/**
Expand All @@ -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.")
}
}
Original file line number Diff line number Diff line change
@@ -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.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 9 additions & 1 deletion packages/create-redwood-app/template/api/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

0 comments on commit ecf8aa9

Please sign in to comment.