-
Notifications
You must be signed in to change notification settings - Fork 970
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
Add hasRole to Auth for RBAC #939
Conversation
Note: once I clean up my test app UI, I'll try to include a screen recording of role auth being enforced. |
Some thoughts:
The way Or, could |
packages/auth/README.md
Outdated
// return appMetadata(decoded).authorization?.roles || [] | ||
// } | ||
// | ||
// export const getCurrentUser = async (decoded) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we can't add types here, but is there a nice way to add some comments to show the shape of this function? e.g. mine looks like this in TS
interface TokenHeader {
type: 'cli' | 'jwt'
token: string
}
export const getCurrentUser = async (
decodedToken: string,
tokenHeader: TokenHeader
) => {
let user
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️ Definitely a question best answered by @peterp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been dealing with this exact problem when creating serverless functions, which currently look like this:
export const handler = (event, context, callback) => {
return { statusCode: '404' }
}
Type-safety be damned! Unless I define those types myself... I don't know what's going on!?
One pattern that I've stumbled upon is to introduce a wrapping function:
export const handler = createServerlessFunction(({event, context })=> {
return {
statusCode: 200,
body: JSON.stringify('pew pew pew'),
}
})
This is nice, because I can provide types for event and context, and can specify exactly what the user can return! I can also introduce some lifecycle things into this function that give me as a framework creator a bit more control. It's not nice because it's a bit redundant for just adding types.
Maybe, maybe, maybe we can introduce a wrapper for getCurrentUser
? (These are always optional if you "understand" the structure that needs to be exported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No related @peterp but if you wanted typescript magic in JS, have you tried adding // @ts-check
to the top of the file? VSCode picks this up and does type checking if it can infer things.
One idea for this function is to use JS doc style comments
** /**
* Handle how to auth a user with the decoded bearer token
* @param decoded a decoded token on the type of Auth Provider used. Custom auth just returns the token
* @param tokenDetails ...
* @example ......
**//**
I don’t think we need this everywhere, but since this function is particularly an important one, it might be worth consideration!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we have to add that in here... it could be done in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we can just add an issue and we can discuss it further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I already added it ... I am just reworking all the docs and then testing. Have to step away for a bit but will commit soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Strange now seeing an error in CI that not seeing local. I'll have to investigate.
3:41:43 PM - Project 'tsconfig.json' is out of date because output file '.buildcache/AuthProvider.d.ts' does not exist
3:41:43 PM - Building project '/home/runner/work/redwood/redwood/packages/auth/tsconfig.json'...
##[error]src/AuthProvider.tsx(137,36): error TS2339: Property 'roles' does not exist on type 'CurrentUser'.
##[error]src/AuthProvider.tsx(198,11): error TS2322: Type '(role: string) => any' is not assignable to type '() => boolean'.
- I am also going to setup an app w/ Netlify Identity and try out role access
packages/cli/src/commands/generate/auth/templates/auth.js.template
Outdated
Show resolved
Hide resolved
packages/cli/src/commands/generate/auth/templates/auth.js.template
Outdated
Show resolved
Hide resolved
Hey @dthyresson, top job with this one 💯! I'd like to spend some time testing this. Left some comments - as always, please take what I'm saying as suggestions - it's hard to convey that without adding "fluff" around what I'm saying |
Question maybe for @aldonline, does |
I think so, we could make it something like this: requireAuth({ role: 'admin' }) I would love to keep the surface area of the functions that we introduce into So, I don't think we we should introduce a That said I do think the other functions you've introduced add a bunch of value for parsing and extracting values from a JWT. Maybe there's a way we can wrap that into a module that a user could import? As an example: import parseJWT from '@redwoodjs/api'
// ...
const { roles } = parseJWT(token).appMetadata |
Could not agree more @peterp . My I've refactored to:
There is still a hasRole on the context used in useAuth() hook on web side. Edit: BTW - The reason I did not implement
is because I think there is a case where the decoded token will have just a |
packages/auth/README.md
Outdated
// throw new AuthenticationError("You don't have permission to do that.") | ||
// } | ||
// | ||
// if (!context.currentUser.roles?.includes(role)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// if (!context.currentUser.roles?.includes(role)) { | |
// if (typeof role !== 'undefined' && !context.currentUser.roles?.includes(role)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The specification of a role could be optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, I noticed that as well and had not yet committed:
if (role && !context.currentUser.roles?.includes(role)) {
but will use your suggestion,
packages/auth/README.md
Outdated
// whether or not they are assigned a role, and optionally raise an | ||
// error if they're not. | ||
// | ||
// export const requireAuth = ({ role }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// export const requireAuth = ({ role }) => { | |
// export const requireAuth = ({ role } = {}) => { |
packages/api/src/parseJWT.ts
Outdated
} | ||
|
||
const roles = (token: DecodedToken): { roles: string[] } => { | ||
console.log(token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log(token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sneaky sneaky console.log 🦕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Snuck in as part of my unit test.
@@ -12,6 +12,8 @@ export type GetCurrentUser = ( | |||
raw: AuthContextPayload[1] | |||
) => Promise<null | object | string> | |||
|
|||
export type HasRole = (role: string) => Promise<boolean> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this HasRole
stuff still relevant? I thought you're overloading requireAuth
as requireAuth({ role })
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:) I removed this from my app's implementation ... but forgot to here.
It is not needed.
That's what happens when I try to code at 3am.
I'll do a full walkthrough my next push.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hahaha! Nice! 3am coding sessions are the best
packages/auth/src/AuthProvider.tsx
Outdated
* Checks if the "currentUser" from the api side | ||
* is assigned a role | ||
**/ | ||
hasRole(): Promise<boolean> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasRole(): Promise<boolean> | |
hasRole(): boolean |
Since it's checking against the data that's already in currentUser
this probably doesn't have to be a promise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes. An earlier implementation has hasRole
calling getCurrentUser
hence the async but then realized did not need to do that.
FYI - I went through the Blog Tutorial again today with the intent to set Netlify Identity and test auth and RBAC with new rw changes. Currently seeing a 500 error somewhere with
Even though the Netlify widget thinks I am logged it and the token is in localStorage. But, Trying to track down. |
I found the error via graphql playground: Somehow my
|
Ok, fixed it. My PR code for auth.js worked fine ... but it had not reloaded because All good now ;) |
Nice! New Post and Edit are protected by "author" role, while I only have "admin" role:
But if I give myself "author" as well ... in Netlify Identity. |
Implement hasRole in AuthProvider Begin hasRole tests Cloning route needs role prop
3576b97
to
aa0fd5b
Compare
@dthyresson Could you please implement authentication that uses Email and password, which not rely on 3rd party auth providers? |
Implement Role-based Authorization
Resolves: #806
See the included updated docs for the full run down -- it is not for the faint of heart.
I have
hasRole
working with Auth0 and have documented how the setup and configure access.In short:
app_metadata
app_metadata
getCurrentUser
, extractroles
and assign tocurrentUser.roles=[]
hasRole
check in auth.jshasRole
to graphqlhasRole
to auth provider context anduseAuth
hookhasRole
toPrivate
route to check if assigned roleA
auth.js
could look likeYou would protect a service like:
You can also protect routes:
And also protect content in pages or components via the
userAuth()
hook:Have updated generators, too.
Notes
Want to get Pr out for initial implementation review.
Edit: Updated to use
ForbiddenError
.Edit: Tested with a newly created 15.3 app and
yarn rw g auth auth0
and graphql and auth.js look ok