Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
34edd92
Initial JWT Auth for backend
dburkhart07 Mar 21, 2025
2011b66
Added guards for jwt auth
dburkhart07 Mar 27, 2025
bb180d4
Updated auth
dburkhart07 Mar 29, 2025
1a95f15
Tried fixing JWT Strategy
dburkhart07 Mar 29, 2025
8587514
Updated auth
dburkhart07 Mar 30, 2025
8f251c8
Finished general authentication for both frontend and backend pages
dburkhart07 Mar 31, 2025
89677e3
Final commit for this branch
dburkhart07 Apr 1, 2025
267b6eb
Revisions made with Sam!!!
dburkhart07 Aug 10, 2025
44f537d
Resolved merge conflicts
dburkhart07 Jan 19, 2026
a5a7852
Resolved merge conflicts
dburkhart07 Jan 19, 2026
20a26ba
Another main merge
dburkhart07 Jan 19, 2026
88f8d3c
Fixed all errors with modules
dburkhart07 Jan 19, 2026
5421fe6
prettier
dburkhart07 Jan 19, 2026
6aea768
Fixed module importing
dburkhart07 Jan 19, 2026
caa33e9
prettier
dburkhart07 Jan 19, 2026
390b380
Added back in donation migration
dburkhart07 Jan 19, 2026
f1bad91
Full implementation of backend role-based auth
dburkhart07 Jan 19, 2026
f7621f5
prettier
dburkhart07 Jan 19, 2026
1331bbb
[SSF 17] - environment variables updates (#44)
dburkhart07 Jan 21, 2026
d69b3c8
Fixed user flow to use a cognito id hardcoded into the database
dburkhart07 Jan 23, 2026
fe466d5
prettier
dburkhart07 Jan 23, 2026
5f9f97b
Resolved merge conflicts
dburkhart07 Jan 25, 2026
3267bb5
Added requested changes
dburkhart07 Jan 26, 2026
34f86e0
Added in decorator and guard for a bypass gaurd
dburkhart07 Jan 26, 2026
99c018a
Final commit
dburkhart07 Jan 26, 2026
86e7364
Merged main
dburkhart07 Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@ DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=securing-safe-food
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=PLACEHOLDER_PASSWORD
DATABASE_PASSWORD=PLACEHOLDER_PASSWORD

AWS_ACCESS_KEY_ID = PLACEHOLDER_AWS_ACCESS_KEY
AWS_SECRET_ACCESS_KEY = PLACEHOLDER_AWS_SECRET_KEY
AWS_REGION = PLACEHOLDER_AWS_REGION
COGNITO_CLIENT_SECRET = PLACEHOLDER_COGNITO_CLIENT_SECRET

AWS_BUCKET_NAME = 'confirm-delivery-photos'
41 changes: 40 additions & 1 deletion apps/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,43 @@ You can check that your database connection details are correct by running `nx s
"LOG 🚀 Application is running on: http://localhost:3000/api"
```

Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal.
Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal.

# AWS Setup

We have a few environment variables that we utilize to access several AWS services throughout the application. Below is a list of each of them and how to access each after logging in to AWS

1. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`:
- Click on your username in the top right corner, and navigate to Security Credentials
- Scroll down to access keys, and create a new key
- Select "Local code" as the purpose for the key, and add an optional description
- Replace both the public and secret keys in the .env file to those values. Note that the secret key will not be accessible after you leave this page
- Click done

2. `AWS_REGION`:
This can be found next to your profile name when you login to the main page. Some accounts may be different, but we generally use us-east-1 or us-east-2.
This is the region that you find on the right side after clicking on the location dropdown, usually saying "United States (*some region*)".
For example, if we want to use Ohio as the region, we would put `AWS_REGION="us-east2"`

3. `AWS_BUCKET_NAME`:
This one is already given to you. As of right now, we only use one bucket, confirm-delivery-photos to store photos in a public S3 Bucket. This may be subject to change as we use S3 more in the project.

4. `COGNITO_CLIENT_SECRET`:
This is used to help authenticate you with AWS Cognito and allow you to properly sign in using proper credential. To find this:
- Navigate to AWS Cognito
- Make sure you are on "United States (N. Virginia) as your region
- Go into User pools and click on the one that says "ssf" (NOTE: You can also validate the User pool id in the `auth/aws_exports.ts` file)
- Go to App Clients, and click on 'ssf client w secret'
- There, you can validate the information in `auth/aws_exports.ts` (the `userPoolClientId`), as well as copy the client secret into your env file

5. Creating a new user within AWS Cognito
There are 2 ways you can create a new user in AWS Cognito. The simplest, is through loading the up, going to the landing page, and creating a new account there. If you choose to do it alternatively through the console, follow these steps:
- Navigate to AWS Cognito
- Make sure you are on "United States (N. Virginia) as your region
- Go into User pools and click on the one that says "ssf"
- Go to Users
- If you do not already see your email there, create a new User, setting an email in password (this will be what you login with on the frontend)
- Click 'Create User'
- Load up the app, and go to the landing page
- Verify you are able to login with these new credentials you created

12 changes: 7 additions & 5 deletions apps/backend/src/allocations/allocations.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Allocation } from './allocations.entity';
import { AllocationsController } from './allocations.controller';
import { AllocationsService } from './allocations.service';
import { AuthService } from '../auth/auth.service';
import { JwtStrategy } from '../auth/jwt.strategy';
import { AuthModule } from '../auth/auth.module';

@Module({
imports: [TypeOrmModule.forFeature([Allocation])],
imports: [
TypeOrmModule.forFeature([Allocation]),
forwardRef(() => AuthModule),
],
controllers: [AllocationsController],
providers: [AllocationsService, AuthService, JwtStrategy],
providers: [AllocationsService],
exports: [AllocationsService],
})
export class AllocationModule {}
15 changes: 14 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { ManufacturerModule } from './foodManufacturers/manufacturer.module';
import { DonationModule } from './donations/donations.module';
import { DonationItemsModule } from './donationItems/donationItems.module';
import { AllocationModule } from './allocations/allocations.module';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './auth/roles.guard';
import { JwtAuthGuard } from './auth/jwt-auth.guard';

@Module({
imports: [
Expand All @@ -40,6 +43,16 @@ import { AllocationModule } from './allocations/allocations.module';
AllocationModule,
],
controllers: [AppController],
providers: [AppService],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: RolesGuard,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
9 changes: 6 additions & 3 deletions apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';

@Module({
imports: [UsersModule, PassportModule.register({ defaultStrategy: 'jwt' })],
imports: [
forwardRef(() => UsersModule),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtStrategy],
})
export class AuthModule {}
30 changes: 9 additions & 21 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import {
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
AttributeType,
CognitoIdentityProviderClient,
ConfirmForgotPasswordCommand,
ConfirmSignUpCommand,
Expand All @@ -29,8 +28,8 @@ export class AuthService {
this.providerClient = new CognitoIdentityProviderClient({
region: CognitoAuthConfig.region,
credentials: {
accessKeyId: process.env.NX_AWS_ACCESS_KEY,
secretAccessKey: process.env.NX_AWS_SECRET_ACCESS_KEY,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});

Expand All @@ -43,28 +42,17 @@ export class AuthService {
// (see https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash)
calculateHash(username: string): string {
const hmac = createHmac('sha256', this.clientSecret);
hmac.update(username + CognitoAuthConfig.clientId);
hmac.update(username + CognitoAuthConfig.userPoolClientId);
return hmac.digest('base64');
}

async getUser(userSub: string): Promise<AttributeType[]> {
const listUsersCommand = new ListUsersCommand({
UserPoolId: CognitoAuthConfig.userPoolId,
Filter: `sub = "${userSub}"`,
});

// TODO need error handling
const { Users } = await this.providerClient.send(listUsersCommand);
return Users[0].Attributes;
}

async signup(
{ firstName, lastName, email, password }: SignUpDto,
role: Role = Role.VOLUNTEER,
): Promise<boolean> {
// Needs error handling
const signUpCommand = new SignUpCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
SecretHash: this.calculateHash(email),
Username: email,
Password: password,
Expand All @@ -88,7 +76,7 @@ export class AuthService {

async verifyUser(email: string, verificationCode: string): Promise<void> {
const confirmCommand = new ConfirmSignUpCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
SecretHash: this.calculateHash(email),
Username: email,
ConfirmationCode: verificationCode,
Expand All @@ -100,7 +88,7 @@ export class AuthService {
async signin({ email, password }: SignInDto): Promise<SignInResponseDto> {
const signInCommand = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
USERNAME: email,
Expand All @@ -125,7 +113,7 @@ export class AuthService {
}: RefreshTokenDto): Promise<SignInResponseDto> {
const refreshCommand = new AdminInitiateAuthCommand({
AuthFlow: 'REFRESH_TOKEN_AUTH',
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
Expand All @@ -144,7 +132,7 @@ export class AuthService {

async forgotPassword(email: string) {
const forgotCommand = new ForgotPasswordCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
Username: email,
SecretHash: this.calculateHash(email),
});
Expand All @@ -158,7 +146,7 @@ export class AuthService {
newPassword,
}: ConfirmPasswordDto) {
const confirmComamnd = new ConfirmForgotPasswordCommand({
ClientId: CognitoAuthConfig.clientId,
ClientId: CognitoAuthConfig.userPoolClientId,
SecretHash: this.calculateHash(email),
Username: email,
ConfirmationCode: confirmationCode,
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/auth/aws-exports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const CognitoAuthConfig = {
userPoolId: 'us-east-1_oshVQXLX6',
clientId: '42bfm2o2pmk57mpm5399s0e9no',
userPoolClientId: '1kehn2mr64h94mire6os55bib7',
userPoolId: 'us-east-1_StSYXMibq',
region: 'us-east-1',
};

Expand Down
25 changes: 25 additions & 0 deletions apps/backend/src/auth/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';

// Extension onto AuthGuard to add public route handling
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}

canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);

if (isPublic) {
return true;
}

return super.canActivate(context);
}
}
9 changes: 9 additions & 0 deletions apps/backend/src/auth/jwt-payload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface CognitoJwtPayload {
sub: string;
email?: string;
username?: string;
aud?: string;
iss?: string;
exp?: number;
iat?: number;
}
18 changes: 12 additions & 6 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,36 @@ import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { UsersService } from '../users/users.service';
import CognitoAuthConfig from './aws-exports';
import { CognitoJwtPayload } from './jwt-payload.interface';
import { User } from '../users/user.entity';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
constructor(private usersService: UsersService) {
const cognitoAuthority = `https://cognito-idp.${CognitoAuthConfig.region}.amazonaws.com/${CognitoAuthConfig.userPoolId}`;

super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
_audience: CognitoAuthConfig.clientId,
audience: CognitoAuthConfig.userPoolClientId,
issuer: cognitoAuthority,
algorithms: ['RS256'],
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: cognitoAuthority + '/.well-known/jwks.json',
jwksUri: `${cognitoAuthority}/.well-known/jwks.json`,
}),
});
}

async validate(payload) {
return { idUser: payload.sub, email: payload.email };
// This function is natively called when we validate a JWT token
// Afer confirming that our jwt is valid and our payload is signed,
// we use the sub field in the payload to find the user in our database
async validate(payload: CognitoJwtPayload): Promise<User> {
const dbUser = await this.usersService.findUserByCognitoId(payload.sub);
return dbUser;
}
}
4 changes: 4 additions & 0 deletions apps/backend/src/auth/public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
7 changes: 7 additions & 0 deletions apps/backend/src/auth/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from '../users/types';

// Key used to store roles metadata
export const ROLES_KEY = 'roles';
// Custom decorator to set roles metadata on route handlers for proper parsing by RolesGuard
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
37 changes: 37 additions & 0 deletions apps/backend/src/auth/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../users/types';
import { ROLES_KEY } from './roles.decorator';

// Guard to enforce role-based access control on route handlers
// Applies logic to get us our user, and compare it with the required roles
// Interacts with the metadata that we attach in the @Roles() decorator
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

// If this returns false, Nest will deny access to the route handler
// Automatically throwing a Forbidden Exception (403 status code)
canActivate(context: ExecutionContext): boolean {
// Look for the metadata we set with the @Roles() decorator
// Checks in the route handler, then the controller, and makes it undefined if nothing found
// Routes take priority over controllers in terms of overriding
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(), // method-level
context.getClass(), // controller-level
]);

if (!requiredRoles || requiredRoles.length === 0) {
return true;
}

const request = context.switchToHttp().getRequest();
const user = request.user;

if (!user || !user.role) {
return false;
}

return requiredRoles.includes(user.role);
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/config/typeorm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811
import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses';
import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields';
import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData';
import { AddUserPoolId1769189327767 } from '../migrations/1769189327767-AddUserPoolId';

const config = {
type: 'postgres',
Expand Down Expand Up @@ -67,6 +68,7 @@ const config = {
RemoveMultipleVolunteerTypes1764811878152,
RemoveUnusedStatuses1764816885341,
PopulateDummyData1768501812134,
AddUserPoolId1769189327767,
],
};

Expand Down
Loading
Loading