Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a hasRole() and isAuthenticated() to determine the role membership or if the request is authenticated #3006

Merged
merged 22 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c1b52b8
Move parseJWT
dthyresson Jul 6, 2021
2647b54
Adds isAuthenticated and hasRole to auth.js
dthyresson Jul 7, 2021
b590801
Moves parseJWT from api into api/auth
dthyresson Jul 7, 2021
48473a2
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 7, 2021
c61cac5
Reformat auth.js template
dthyresson Jul 8, 2021
606a7b0
Update role and auth check logic
dthyresson Jul 9, 2021
ba55c0e
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 9, 2021
82d8eb9
Use roles instead of role
dthyresson Jul 9, 2021
66fcde5
Use roles
dthyresson Jul 9, 2021
a99adb9
Default auth ts uses roles
dthyresson Jul 9, 2021
1a15912
Update auth templates to use roles instead of role
dthyresson Jul 9, 2021
0b13eee
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 12, 2021
2b19a12
Updates templates for other providers
dthyresson Jul 12, 2021
aad85c7
Updates dbauth auth template
dthyresson Jul 12, 2021
c24647c
Simplify hasRole logic
dthyresson Jul 12, 2021
ab20476
Update packages/cli/src/commands/setup/auth/templates/auth.js.template
dthyresson Jul 16, 2021
9369829
Update packages/cli/src/commands/setup/auth/templates/auth.js.template
dthyresson Jul 16, 2021
9a08234
Clarify hasRole return value if no roles provided
dthyresson Jul 19, 2021
7523859
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 19, 2021
228bd1a
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 19, 2021
c6bd7e3
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 19, 2021
81650b6
Merge branch 'main' into dt-auth-has-role
dthyresson Jul 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
}