prototype authentication

main
flavien 2022-07-15 15:50:28 +02:00
parent 8676facb68
commit 360ba3df58
19 changed files with 1112 additions and 90 deletions

4
.env.sample Normal file
View File

@ -0,0 +1,4 @@
COGNITO_USER_POOL_ID = <generated by terraform>
COGNITO_CLIENT_ID = <generated by terraform>
COGNITO_REGION = <generated by terraform>
COGNITO_AUTHORITY = <generated by terraform>

814
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,18 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@hapi/joi": "^17.1.1",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.1.0",
"@nestjs/core": "^8.0.0",
"@nestjs/passport": "^8.2.2",
"@nestjs/platform-express": "^8.0.0",
"amazon-cognito-identity-js": "^5.2.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"jwks-rsa": "^2.1.4",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"

View File

@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,10 +1,19 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import * as Joi from '@hapi/joi';
import { AuthenticationModule } from './authentication/authentication.module';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
COGNITO_USER_POOL_ID: Joi.string().required(),
COGNITO_CLIENT_ID: Joi.string().required(),
COGNITO_REGION: Joi.string().required(),
COGNITO_AUTHORITY: Joi.string().required(),
}),
}),
AuthenticationModule
]
})
export class AppModule {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthenticationController } from './authentication.controller';
describe('AuthenticationController', () => {
let controller: AuthenticationController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthenticationController],
}).compile();
controller = module.get<AuthenticationController>(AuthenticationController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,64 @@
import { Body, Controller, Get, HttpException, HttpStatus, Post, Query, Req, UseGuards } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import RegisterDto from './dto/register.dto';
import { JwtGuard } from './guard/jwt.guard';
import { LoginGuard } from './guard/login.guard';
import RequestWithSession from './interface/requestWithSession.interface';
@Controller('authentication')
export class AuthenticationController {
constructor(private readonly authenticationService: AuthenticationService) {}
@UseGuards(LoginGuard)
@Post('login')
async login(@Req() { session }: RequestWithSession) {
const idToken = session.getIdToken();
const refreshToken = session.getRefreshToken();
//send when usage found
//const accessToken = session.getAccessToken();
return {
idToken: idToken.getJwtToken(),
idTokenPayload: idToken.decodePayload(),
refreshToken: refreshToken.getToken(),
};
}
@Post('register')
async register(@Body() registrationData: RegisterDto) {
try {
await this.authenticationService.register(registrationData);
} catch (err) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: err.message
}, HttpStatus.BAD_REQUEST);
}
return 'account created';
}
@Get('confirm-email')
async confirm(@Query('code') code: string, @Query('email') email: string) {
try {
await this.authenticationService.confirm(email, code);
} catch (err) {
throw new HttpException({
status: HttpStatus.UNAUTHORIZED,
error: err.message
}, HttpStatus.UNAUTHORIZED);
}
return 'email confirmed';
}
//refresh
//password-reset
//email-change
//resend-code
//create user table with dynamoose
//index on user name
@UseGuards(JwtGuard)
@Get()
async test(@Req() { session }: RequestWithSession) {
return session
}
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthenticationService } from './authentication.service';
import { AuthenticationController } from './authentication.controller';
import { PassportModule } from '@nestjs/passport';
import { LoginStrategy } from './strategy/login.strategy';
import { JwtStrategy } from './strategy/jwt.strategy';
@Module({
imports: [
PassportModule.register({
property: 'session'
}),
ConfigModule
],
providers: [AuthenticationService, LoginStrategy, JwtStrategy],
controllers: [AuthenticationController]
})
export class AuthenticationModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthenticationService } from './authentication.service';
describe('AuthenticationService', () => {
let service: AuthenticationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthenticationService],
}).compile();
service = module.get<AuthenticationService>(AuthenticationService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,86 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
AuthenticationDetails,
CognitoUser,
CognitoUserAttribute,
CognitoUserPool,
CognitoUserSession,
} from 'amazon-cognito-identity-js';
import RegisterDto from './dto/register.dto';
@Injectable()
export class AuthenticationService {
private readonly userPool: CognitoUserPool;
constructor(
configService: ConfigService
) {
this.userPool = new CognitoUserPool({
UserPoolId: configService.get('COGNITO_USER_POOL_ID'),
ClientId: configService.get('COGNITO_CLIENT_ID'),
});
}
getCognitoUser(Username: string) {
return new CognitoUser({
Username,
Pool: this.userPool
});
}
login(name: string, password: string) {
const newUser = this.getCognitoUser(name);
const authenticationDetails = new AuthenticationDetails({
Username: name,
Password: password
});
return new Promise<CognitoUserSession>((resolve, reject) => {
newUser.authenticateUser(authenticationDetails, {
onSuccess: result => resolve(result),
onFailure: err => reject(err)
});
});
}
private createAttributes(registrationData: RegisterDto) {
const { name } = registrationData;
return [
new CognitoUserAttribute({ Name: 'name', Value: name })
];
}
register(registrationData: RegisterDto) {
const { email, password } = registrationData;
return new Promise<CognitoUser>((resolve, reject) => {
this.userPool.signUp(
email,
password,
this.createAttributes(registrationData),
null,
(error, result) => {
if (!result) {
reject(error);
} else {
resolve(result.user);
}
}
);
});
}
confirm(email: string, code: string) {
const userToConfirm = this.getCognitoUser(email);
return new Promise((resolve, reject) => {
userToConfirm.confirmRegistration(code, true, (err, result) => {
if (err) {
reject(err);
}
resolve(result);
});
});
}
}

View File

@ -0,0 +1,24 @@
import {
IsEmail,
IsString,
IsNotEmpty,
Length,
} from 'class-validator';
export class RegisterDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
@Length(2, 128)
name: string;
@IsNotEmpty()
@IsString()
@Length(8, 128)
password: string;
}
export default RegisterDto;

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LoginGuard extends AuthGuard('local') {}

View File

@ -0,0 +1,8 @@
import { Request } from 'express';
import { CognitoUserSession } from 'amazon-cognito-identity-js';
interface RequestWithSession extends Request {
session: CognitoUserSession;
}
export default RequestWithSession;

View File

@ -0,0 +1,31 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { passportJwtSecret } from 'jwks-rsa';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
configService: ConfigService,
) {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${configService.get('COGNITO_AUTHORITY')}/.well-known/jwks.json`,
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: configService.get('COGNITO_CLIENT_ID'),
issuer: configService.get('COGNITO_AUTHORITY'),
algorithms: ['RS256'],
});
}
public async validate(payload: any) {
return payload;
}
}

View File

@ -0,0 +1,24 @@
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { Strategy } from 'passport-local';
import { PassportStrategy } from "@nestjs/passport";
import { CognitoUserSession } from "amazon-cognito-identity-js";
import { AuthenticationService } from "../authentication.service";
@Injectable()
export class LoginStrategy extends PassportStrategy(Strategy) {
constructor(private authenticationService: AuthenticationService) {
super({
usernameField: 'email'
});
}
async validate(email: string, password: string): Promise<CognitoUserSession> {
try {
return this.authenticationService.login(email, password);
} catch (err) {
throw new HttpException({
status: HttpStatus.UNAUTHORIZED,
error: err.message
}, HttpStatus.UNAUTHORIZED);
}
}
}

View File

@ -1,8 +1,18 @@
import { NestFactory } from '@nestjs/core';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
})
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();