prototype authentication
parent
8676facb68
commit
360ba3df58
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtGuard extends AuthGuard('jwt') {}
|
|
@ -0,0 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class LoginGuard extends AuthGuard('local') {}
|
|
@ -0,0 +1,8 @@
|
|||
import { Request } from 'express';
|
||||
import { CognitoUserSession } from 'amazon-cognito-identity-js';
|
||||
|
||||
interface RequestWithSession extends Request {
|
||||
session: CognitoUserSession;
|
||||
}
|
||||
|
||||
export default RequestWithSession;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
12
src/main.ts
12
src/main.ts
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue