Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
75c3f95
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
36b5c88
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
c40096d
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
6a02f87
Messy first attempt. Working for single service validation
dburkhart07 Jan 24, 2026
27ca72c
Working version, precleanup
dburkhart07 Jan 24, 2026
8958137
prettier
dburkhart07 Jan 24, 2026
9a1aee6
Added documentation to make things clearer
dburkhart07 Jan 25, 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 {}
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
12 changes: 7 additions & 5 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } 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 { AuthService } from './auth.service';

@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({
Expand All @@ -26,6 +27,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}

async validate(payload) {
return { idUser: payload.sub, email: payload.email };
const dbUser = await this.usersService.findUserByCognitoId(payload.sub);
return dbUser;
}
}
25 changes: 25 additions & 0 deletions apps/backend/src/auth/ownership.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SetMetadata, Type } from '@nestjs/common';

// Resolver function type to get the owner user ID for a given entity ID
export type OwnerIdResolver = (params: {
entityId: number;
services: ServiceRegistry;
}) => Promise<number | null>;

// Registry of services that can be easily resolved
// Eliminates the issues with circular dependencies
// allowing the lambdas to resolve only the services they need
export interface ServiceRegistry {
get<T>(serviceClass: Type<T>): T;
}

// Configuration for ownership check
export interface OwnershipConfig {
idParam: string;
resolver: OwnerIdResolver;
}

export const OWNERSHIP_CHECK_KEY = 'ownership_check';

export const CheckOwnership = (config: OwnershipConfig) =>
SetMetadata(OWNERSHIP_CHECK_KEY, config);
99 changes: 99 additions & 0 deletions apps/backend/src/auth/ownership.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
Type,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ModuleRef } from '@nestjs/core';
import {
OWNERSHIP_CHECK_KEY,
OwnershipConfig,
ServiceRegistry,
} from './ownership.decorator';

@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(private reflector: Reflector, private moduleRef: ModuleRef) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const config = this.reflector.get<OwnershipConfig>(
OWNERSHIP_CHECK_KEY,
context.getHandler(),
);

if (!config) {
return true;
}

// Process all request information and the logged in user
const req = context.switchToHttp().getRequest();
const user = req.user;

// Admins bypass ownership checks
if (user.role === 'ADMIN') {
return true;
}

if (!user) {
throw new ForbiddenException('Not authenticated');
}

// Get the id from the parameters
const entityId = Number(req.params[config.idParam]);

if (isNaN(entityId)) {
throw new ForbiddenException(`Invalid ${config.idParam}`);
}

// Create a service registry that easily resolves services
const services = this.createServiceRegistry();

try {
// Execute the lambda function to get the owner user ID
const ownerId = await config.resolver({
entityId,
services,
});

if (ownerId === null || ownerId === undefined) {
throw new ForbiddenException('Unable to determine resource ownership');
}

if (ownerId !== user.id) {
throw new ForbiddenException('Access denied');
}

return true;
} catch (error) {
console.error('Error in ownership resolver:', error);
throw new ForbiddenException('Error verifying resource ownership');
}
}

// Use a service registry for easy service resolution and caching
private createServiceRegistry(): ServiceRegistry {
const cache = new Map<Type<unknown>, unknown>();
const moduleRef = this.moduleRef;

return {
get<T>(serviceClass: Type<T>): T {
// Return cached service if already resolved before
if (cache.has(serviceClass)) {
return cache.get(serviceClass) as T;
}

// Resolve and cache the service
try {
const service = moduleRef.get(serviceClass, { strict: false });
cache.set(serviceClass, service);
return service;
} catch (error) {
throw new Error(`Could not resolve service: ${serviceClass.name}`);
}
},
};
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/auth/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from '../users/types';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
Loading
Loading