Skip to content

Commit

Permalink
initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
hams7504 committed Nov 3, 2024
1 parent 2cd8231 commit 3973cdd
Show file tree
Hide file tree
Showing 18 changed files with 1,452 additions and 9 deletions.
90 changes: 90 additions & 0 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
BadRequestException,
Body,
Controller,
Post
} from '@nestjs/common';

import { SignInDto } from '../dtos/sign-in.dto';
import { SignUpDto } from '../dtos/sign-up.dto';
import { AuthService } from './auth.service';
import { UserService } from '../user/user.service';
import { VerifyUserDto } from '../dtos/verify-user.dto';
import { DeleteUserDto } from '../dtos/delete-user.dto';
import { User } from '../user/user.entity';
import { SignInResponseDto } from '../dtos/sign-in-response.dto';
import { RefreshTokenDto } from '../dtos/refresh-token.dto';
//import { AuthGuard } from '@nestjs/passport';
import { ConfirmPasswordDto } from '../dtos/confirm-password.dto';
import { ForgotPasswordDto } from '../dtos/forgot-password.dto';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('Auth')
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private userService: UserService,
) {}

@Post('/signup')
async createUser(@Body() signUpDto: SignUpDto): Promise<User> {
// By default, creates a standard user
try {
await this.authService.signup(signUpDto);
} catch (e) {
throw new BadRequestException(e.message);
}

const user = await this.userService.create(
signUpDto.email,
signUpDto.firstName,
signUpDto.lastName,
);

return user;
}

// TODO deprecated if verification code is replaced by link
@Post('/verify')
verifyUser(@Body() body: VerifyUserDto): void {
try {
this.authService.verifyUser(body.email, body.verificationCode);
} catch (e) {
throw new BadRequestException(e.message);
}
}

@Post('/signin')
signin(@Body() signInDto: SignInDto): Promise<SignInResponseDto> {
return this.authService.signin(signInDto);
}

@Post('/refresh')
refresh(@Body() refreshDto: RefreshTokenDto): Promise<SignInResponseDto> {
return this.authService.refreshToken(refreshDto);
}

@Post('/forgotPassword')
forgotPassword(@Body() body: ForgotPasswordDto): Promise<void> {
return this.authService.forgotPassword(body.email);
}

@Post('/confirmPassword')
confirmPassword(@Body() body: ConfirmPasswordDto): Promise<void> {
return this.authService.confirmForgotPassword(body);
}

@Post('/delete')
async delete(@Body() body: DeleteUserDto): Promise<void> {
const user = await this.userService.getUser(body.userId);

try {
await this.authService.deleteUser(user.email);
} catch (e) {
throw new BadRequestException(e.message);
}

this.userService.remove(user.userID);
}
}
19 changes: 19 additions & 0 deletions apps/backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserService } from '../user/user.service';
import { User } from '../user/user.entity';
import { JwtStrategy } from './jwt.strategy';

@Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, UserService, JwtStrategy],
})
export class AuthModule {}
179 changes: 179 additions & 0 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Injectable } from '@nestjs/common';
import {
AdminDeleteUserCommand,
AdminInitiateAuthCommand,
AttributeType,
CognitoIdentityProviderClient,
ConfirmForgotPasswordCommand,
ConfirmSignUpCommand,
ForgotPasswordCommand,
ListUsersCommand,
SignUpCommand,
} from '@aws-sdk/client-cognito-identity-provider';

import CognitoAuthConfig from './aws-exports';
import { SignUpDto } from '../dtos/sign-up.dto';
import { SignInDto } from '../dtos/sign-in.dto';
import { SignInResponseDto } from '../dtos/sign-in-response.dto';
import { createHmac } from 'crypto';
import { RefreshTokenDto } from '../dtos/refresh-token.dto';
import { Status } from '../users/types';
import { ConfirmPasswordDto } from '../dtos/confirm-password.dto';

@Injectable()
export class AuthService {
private readonly providerClient: CognitoIdentityProviderClient;
private readonly clientSecret: string;

constructor() {
this.providerClient = new CognitoIdentityProviderClient({
region: CognitoAuthConfig.region,
credentials: {
accessKeyId: process.env.NX_AWS_ACCESS_KEY,
secretAccessKey: process.env.NX_AWS_SECRET_ACCESS_KEY,
},
});

this.clientSecret = process.env.COGNITO_CLIENT_SECRET;
}

// Computes secret hash to authenticate this backend to Cognito
// Hash key is the Cognito client secret, message is username + client ID
// Username value depends on the command
// (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);
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,
status: Status = Status.STANDARD,
): Promise<boolean> {
// Needs error handling
const signUpCommand = new SignUpCommand({
ClientId: CognitoAuthConfig.clientId,
SecretHash: this.calculateHash(email),
Username: email,
Password: password,
UserAttributes: [
{
Name: 'name',
Value: `${firstName} ${lastName}`,
},
// Optional: add a custom Cognito attribute called "role" that also stores the user's status/role
// If you choose to do so, you'll have to first add this custom attribute in your user pool
{
Name: 'custom:role',
Value: status,
},
],
});

const response = await this.providerClient.send(signUpCommand);
return response.UserConfirmed;
}

async verifyUser(email: string, verificationCode: string): Promise<void> {
const confirmCommand = new ConfirmSignUpCommand({
ClientId: CognitoAuthConfig.clientId,
SecretHash: this.calculateHash(email),
Username: email,
ConfirmationCode: verificationCode,
});

await this.providerClient.send(confirmCommand);
}

async signin({ email, password }: SignInDto): Promise<SignInResponseDto> {
const signInCommand = new AdminInitiateAuthCommand({
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
ClientId: CognitoAuthConfig.clientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
USERNAME: email,
PASSWORD: password,
SECRET_HASH: this.calculateHash(email),
},
});

const response = await this.providerClient.send(signInCommand);

return {
accessToken: response.AuthenticationResult.AccessToken,
refreshToken: response.AuthenticationResult.RefreshToken,
idToken: response.AuthenticationResult.IdToken,
};
}

// Refresh token hash uses a user's sub (unique ID), not their username (typically their email)
async refreshToken({
refreshToken,
userSub,
}: RefreshTokenDto): Promise<SignInResponseDto> {
const refreshCommand = new AdminInitiateAuthCommand({
AuthFlow: 'REFRESH_TOKEN_AUTH',
ClientId: CognitoAuthConfig.clientId,
UserPoolId: CognitoAuthConfig.userPoolId,
AuthParameters: {
REFRESH_TOKEN: refreshToken,
SECRET_HASH: this.calculateHash(userSub),
},
});

const response = await this.providerClient.send(refreshCommand);

return {
accessToken: response.AuthenticationResult.AccessToken,
refreshToken: refreshToken,
idToken: response.AuthenticationResult.IdToken,
};
}

async forgotPassword(email: string) {
const forgotCommand = new ForgotPasswordCommand({
ClientId: CognitoAuthConfig.clientId,
Username: email,
SecretHash: this.calculateHash(email),
});

await this.providerClient.send(forgotCommand);
}

async confirmForgotPassword({
email,
confirmationCode,
newPassword,
}: ConfirmPasswordDto) {
const confirmComamnd = new ConfirmForgotPasswordCommand({
ClientId: CognitoAuthConfig.clientId,
SecretHash: this.calculateHash(email),
Username: email,
ConfirmationCode: confirmationCode,
Password: newPassword,
});

await this.providerClient.send(confirmComamnd);
}

async deleteUser(email: string): Promise<void> {
const adminDeleteUserCommand = new AdminDeleteUserCommand({
Username: email,
UserPoolId: CognitoAuthConfig.userPoolId,
});

await this.providerClient.send(adminDeleteUserCommand);
}
}
7 changes: 7 additions & 0 deletions apps/backend/src/auth/aws-exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const CognitoAuthConfig = {
userPoolId: process.env.AWS_COGNITO_USER_POOL_ID,
clientId: process.env.AWS_COGNITO_CLIENT_ID,
region: process.env.AWS_REGION,
};

export default CognitoAuthConfig;
31 changes: 31 additions & 0 deletions apps/backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';

import CognitoAuthConfig from './aws-exports';

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

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

async validate(payload) {
return { idUser: payload.sub, email: payload.email };
}
}
12 changes: 12 additions & 0 deletions apps/backend/src/dtos/confirm-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsEmail, IsString } from 'class-validator';

export class ConfirmPasswordDto {
@IsEmail()
email: string;

@IsString()
newPassword: string;

@IsString()
confirmationCode: string;
}
6 changes: 6 additions & 0 deletions apps/backend/src/dtos/delete-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsPositive } from 'class-validator';

export class DeleteUserDto {
@IsPositive()
userId: number;
}
6 changes: 6 additions & 0 deletions apps/backend/src/dtos/forgot-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsEmail } from 'class-validator';

export class ForgotPasswordDto {
@IsEmail()
email: string;
}
9 changes: 9 additions & 0 deletions apps/backend/src/dtos/refresh-token.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';

export class RefreshTokenDto {
@IsString()
refreshToken: string;

@IsString()
userSub: string;
}
19 changes: 19 additions & 0 deletions apps/backend/src/dtos/sign-in-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class SignInResponseDto {
/**
* The JWT access token to be passed in API requests
* @example eyJ...
*/
accessToken: string;

/**
* The JWT refresh token to maintain user sessions by requesting new access tokens
* @example eyJ...
*/
refreshToken: string;

/**
* The JWT ID token that carries the user's information
* @example eyJ...
*/
idToken: string;
}
Loading

0 comments on commit 3973cdd

Please sign in to comment.