forked from Code-4-Community/scaffolding
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,452 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.