⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@dburkhart07
Copy link

@dburkhart07 dburkhart07 commented Apr 1, 2025

ℹ️ 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:

  1. Added jwt authentication to all api backend controllers to ensure user is sigend in
  2. Added role decorators and guards to specify which roles could access which endpoints
  3. Created an axios interceptor in the frontend to redirect users to an error page if they are unauthorized
  4. Refactored api logic in request form to put the actual request within apiClient.ts as opposed to having an axios call in the file itself

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:

  1. Make sure the specific controller is using the @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 permitted
  2. We can assign roles to specific endpoints, as seen in the pantries.controller.ts file
  3. Additionlly, you will want to make sure the controller you are using has access to the AuthModule, as this provides access to the JwtStrategy and AuthService, whch are both needed. See how the PantriesModule has 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
image
image

Accessing food request form as an admin
image
image

🏕️ (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

Copy link
Collaborator

@sam-schu sam-schu left a 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)

@dburkhart07 dburkhart07 requested a review from sam-schu January 24, 2026 03:48
});
}

async validate(payload) {
Copy link
Collaborator

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 {
Copy link
Collaborator

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) {
Copy link
Collaborator

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';

Copy link
Collaborator

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 {
Copy link
Collaborator

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,
Copy link
Collaborator

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
Copy link
Collaborator

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 () => {
Copy link
Collaborator

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')
Copy link
Collaborator

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)
Copy link
Collaborator

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?

@sam-schu sam-schu removed the on hold label Jan 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants