-
Notifications
You must be signed in to change notification settings - Fork 0
Role based auth backend #35
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
base: main
Are you sure you want to change the base?
Conversation
sam-schu
left a comment
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.
As discussed on Slack, best practice is to associate Cognito users with users in the database via the sub / user ID from Cognito, not the user's email (which can change!). Also, I don't think we'll need getUserRole or getUser in the auth service anymore (since role should just be stored in our database, and in our future signup flows the email should actually come to the backend first and we will have to pass it to Cognito when creating the user)
| }); | ||
| } | ||
|
|
||
| async validate(payload) { |
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.
Could we leave a comment here to clarify that when this function is called, we know the payload parameter represents a valid signed JWT (so the user's sub is actually what they say it is) because the Passport JWT strategy takes care of that for us? Given the name validate I initially assumed we would actually have to do the validation of the JWT here, although after looking at docs I can see that what we have here should be sufficient
| export class RolesGuard implements CanActivate { | ||
| constructor(private reflector: Reflector) {} | ||
|
|
||
| canActivate(context: ExecutionContext): 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.
Can we add a comment clarifying that if this returns false, Nest automatically throws a ForbiddenException (403)?
| import { AuthService } from './auth.service'; | ||
|
|
||
| @Injectable() | ||
| export class JwtStrategy extends PassportStrategy(Strategy) { |
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 think there's a types package we can add to get better typing here
| @@ -0,0 +1,5 @@ | |||
| import { SetMetadata } from '@nestjs/common'; | |||
| import { Role } from '../users/types'; | |||
|
|
|||
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.
Can we document this decorator to explain what it does and how it interacts with the roles guard? This will be confusing for people not familiar with how decorators and guards work in Nest
| import { ROLES_KEY } from './roles.decorator'; | ||
|
|
||
| @Injectable() | ||
| export class RolesGuard implements CanActivate { |
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.
Rather than adding this to every controller separately, can we set it up as a global guard (as long as we make sure to return true if a route doesn't have any roles metadata set)?
| UpdatePantriesTable1737906317154, | ||
| UpdateDonations1738697216020, | ||
| UpdateDonationColTypes1741708808976, | ||
| UpdateDonations1738697216020, |
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.
Was this intentional?
| constructor() { | ||
| this.axiosInstance = axios.create({ baseURL: defaultBaseUrl }); | ||
|
|
||
| // Attach the access token to each request if available |
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.
This is still sending a bearer token after you sign out!
|
|
||
| const LandingPage: React.FC = () => { | ||
| return <>Landing page</>; | ||
| const handleSignOut = async () => { |
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.
We don't need this, we already have a sign out button on the header for all the pages
| return this.pantriesService.addPantry(pantryData); | ||
| } | ||
|
|
||
| @Post('/approve/:pantryId') |
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.
Missed the role here
| import { Order } from '../orders/order.entity'; | ||
| import { OrdersService } from '../orders/order.service'; | ||
|
|
||
| @UseGuards(AuthGuard('jwt'), RolesGuard) |
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 endpoint for creating a pantry application (and the one for food manufacturer applications, once that's merged) needs to be accessible without being logged in. Even though that endpoint doesn't have a roles decorator, the auth guard is requiring the user to be logged in (as anyone) for the request to go through. But it would be annoying to have to apply the auth guard to every individual endpoint except for those two. Is there a way we could set up the auth guard as a global guard (even better than applying it to every controller separately), but then have the ability to disable it for particular endpoints with something like @NoLoginRequired?
ℹ️ Issue
Resolves Implement Role-Based Authentication (BACKEND)
Closes Implement Role Based Authentication (Backend)
NOTE: Please merge this PR
📝 Description
I implemented the role based authentication for the backend. This consisted of setting up the jwt strategy to be properly configured, as well as creating a role guard and role decorator. After this, I added the AuthGuard('jwt') to every backend controller, making it so nothing in the backend is accessible unless the user signs in. I then used the role decorator and guard, as well as the Role enum already defined to specify which roles could access which api endpoints (I just did this for the pantries and requests, since I am not sure who we want accessing the api endpoint outside of these). To avoid unauthorized users accessing pages via the frontend, I implemented a check that would redirect the user to an error page if, while making a backend call with the axios interceptor in the api client, it received a 403 (unauthorized user) error.
Briefly list the changes made to the code:
Explanation of some key features
Changes to modules: I adjusted a majority of the modules files to include the AuthModule on import. Since the AuthModule exports and provides both the AuthService and JwtStrategy, this allows us to use both them and their providers in all of our modules.
JwtStrategy file: This file serves as the purpose of validating jwts we send over when logging in through AWS Cognito. This file tells the Passport how to validate JWTs signed by Cognito on start up. We then also validate the payload we receive from the jwt, extracting the proper database user as such (NOTE: The actual ability to register a user is not implemented yet). You can see from the logs if you run it, our users in Cognito all have a role as VOLUNTEER, but we use their email to get their real info from the postgres database.
Majority of the auth files: Currently, we are not using any of the auth endpoints, and almost none of the authService functions. We should see how these end up being useful later on, but, since they were included with scaffolding and we are already using AWS Cognito, we may be able to delete a lot of the auth code.
CurrentUserInterceptor: We ended up not using this anywhere currently, as AWS Cognito gets the database user for us in the validate function within the JwtStrategy. Perhaps we will use it later, but for now it has been removed from all of our controllers.
ApiClient and frontend: We need to attach an access token to all of the api requests into the backend so that we can validate the user is who they say they are when retrieving their information. For this, we added a hooks folder and a way to just fetch the session info after they have logged in. We use the idToken to verify this information, so we attach that onto the
apiClient.accessToken, which is applied to every request we make (this enforces the user to be logged in to use any of our endpoints. We also added special response error handling from the api client too. When we receive a 403 error (which is special unauthorized error code), we know the user did not have a valid role, so we redirect them. There could be a better way to handle this down the road, but for now this is what I thought of.Process of registering an endpoint for role-based authentication:
@UseGuards(AuthGuard('jwt'), RolesGuard). The AuthGuard('jwt') allows us to get actual user information in our context, and the RolesGuard allows us to take the actual @roles decorator, and apply logic to check if the user's role is permittedpantries.controller.tsfileAuthModule, as this provides access to theJwtStrategyandAuthService, whch are both needed. See how thePantriesModulehas imported it for futher testing or development.✔️ Verification
To verify, I registered a user with an email and password on AWS Cognito. I then manually created this entry in the Postgres table (this will have to be changed later to allow for users to be automatically registered), using the same email, and assigning them a role. I gave the user a PANTRY role, and attempted to access an api endpoint that was only accessible to pantries. I then changed it to ADMIN, and tried accessing it again, to make sure that it redirected me to the error page.
Provide screenshots of any new components, styling changes, or pages.


Accessing food request form as a pantry
Accessing food request form as an admin


🏕️ (Optional) Future Work / Notes
This is just the basic implementation for the backend. As we figure out which users are able to access which endpoints, we will need to simply just add the role decorator and guards where we deem fit, but this is not hard to do. Additionally, manually implementing the user in the database is unrealistic, and we will at some point need to figure out how, when a user registers with Cognito, they can have their information logged within the database (default for VOLUNTEER, but also will need to create a new one for the pantry application approval, etc.).
See other comments from the description