add working authentication
This commit is contained in:
parent
f7fab04e54
commit
7c30574bf2
@ -20,14 +20,23 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.1.2",
|
||||
"@nestjs/common": "^6.0.0",
|
||||
"@nestjs/core": "^6.0.0",
|
||||
"@nestjs/common": "^6.5.2",
|
||||
"@nestjs/core": "^6.5.2",
|
||||
"@nestjs/jwt": "^6.1.1",
|
||||
"@nestjs/mongoose": "^6.1.2",
|
||||
"@nestjs/platform-express": "^6.0.0",
|
||||
"@nestjs/passport": "^6.1.0",
|
||||
"@nestjs/platform-express": "^6.5.2",
|
||||
"@nestjs/swagger": "^3.1.0",
|
||||
"@nestjs/terminus": "^6.5.0",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/mongoose": "^5.5.11",
|
||||
"bcrypt": "^3.0.6",
|
||||
"class-transformer": "^0.2.3",
|
||||
"class-validator": "^0.9.1",
|
||||
"mongoose": "^5.6.7",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rimraf": "^2.6.2",
|
||||
"rxjs": "^6.3.3",
|
||||
|
||||
@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -6,7 +6,13 @@ export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
getIndex(): object {
|
||||
return {
|
||||
message: 'hello humanoid',
|
||||
_links: {
|
||||
doc: '/doc',
|
||||
health: '/health'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,17 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import {UsersModule} from "./users/users.module"
|
||||
import {FormsModule} from "./forms/forms.module"
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forRoot('mongodb://localhost/nest'),
|
||||
MongooseModule.forRoot('mongodb://localhost/ohmyform'),
|
||||
TerminusModule.forRootAsync({
|
||||
useClass: TerminusOptionsService,
|
||||
}),
|
||||
UsersModule,
|
||||
FormsModule,
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
31
api/src/auth/auth.module.ts
Normal file
31
api/src/auth/auth.module.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { PasswordStrategy } from "./strategies/password.strategy"
|
||||
import { PasswordService } from "./services/password.service"
|
||||
import { AuthController } from "./controllers/auth.controller"
|
||||
import { jwtConstants } from "./constants"
|
||||
import { JwtModule } from "@nestjs/jwt"
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy"
|
||||
import { JwtRefreshStrategy } from "./strategies/jwt.refresh.strategy"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: jwtConstants.secret,
|
||||
signOptions: { expiresIn: '12h' },
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
PasswordService,
|
||||
PasswordStrategy,
|
||||
JwtStrategy,
|
||||
JwtRefreshStrategy,
|
||||
]
|
||||
})
|
||||
export class AuthModule {}
|
||||
3
api/src/auth/constants.ts
Normal file
3
api/src/auth/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const jwtConstants = {
|
||||
secret: 'secretKey',
|
||||
};
|
||||
20
api/src/auth/controllers/auth.controller.ts
Normal file
20
api/src/auth/controllers/auth.controller.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import {AuthService} from "../services/auth.service"
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@UseGuards(AuthGuard('password'))
|
||||
@Post('login')
|
||||
async login(@Request() req) {
|
||||
return this.authService.createToken(req.user);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt.refresh'))
|
||||
@Post('refresh')
|
||||
async refresh(@Request() req) {
|
||||
return this.authService.createToken(req.user);
|
||||
}
|
||||
}
|
||||
4
api/src/auth/interfaces/auth.jwt.interface.ts
Normal file
4
api/src/auth/interfaces/auth.jwt.interface.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface AuthJwt {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
8
api/src/auth/interfaces/auth.user.interface.ts
Normal file
8
api/src/auth/interfaces/auth.user.interface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
roles: [string];
|
||||
created: Date;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
18
api/src/auth/services/auth.service.spec.ts
Normal file
18
api/src/auth/services/auth.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
70
api/src/auth/services/auth.service.ts
Normal file
70
api/src/auth/services/auth.service.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
import { PasswordService } from "./password.service"
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthUser } from "../interfaces/auth.user.interface"
|
||||
import { User } from "../../users/interfaces/user.interface"
|
||||
import {AuthJwt} from "../interfaces/auth.jwt.interface"
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly jwtService: JwtService
|
||||
) {}
|
||||
|
||||
async verifyForLogin(identifier: string, pass: string): Promise<AuthUser | null> {
|
||||
try {
|
||||
const user = await this.usersService.findOneByIdentifier(identifier);
|
||||
|
||||
if (await this.passwordService.verify(pass, user.passwordHash, user.salt)) {
|
||||
return AuthService.setupAuthUser(user)
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async validate(identifier: string): Promise<AuthUser | null> {
|
||||
try {
|
||||
return AuthService.setupAuthUser(
|
||||
await this.usersService.findOneByIdentifier(identifier)
|
||||
)
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static setupAuthUser(user:User):AuthUser {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
roles: user.roles,
|
||||
created: user.created,
|
||||
lastUpdated: user.lastUpdated
|
||||
};
|
||||
}
|
||||
|
||||
async createToken(user: AuthUser): Promise<AuthJwt> {
|
||||
const payload = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
roles: user.roles,
|
||||
};
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
// TODO add refresh token invalidation uppon usage! They should only work once
|
||||
refresh_token: this.jwtService.sign(
|
||||
{
|
||||
...payload,
|
||||
refresh: true
|
||||
},
|
||||
{
|
||||
expiresIn: '30days',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
30
api/src/auth/services/password.service.ts
Normal file
30
api/src/auth/services/password.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import * as crypto from 'crypto'
|
||||
import * as bcrypt from 'bcrypt'
|
||||
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
async verify (password: string, hash: string, salt?: string): Promise<boolean> {
|
||||
console.log('try to verify password', password)
|
||||
if (password[0] === '$') {
|
||||
return await bcrypt.compare(password, hash)
|
||||
}
|
||||
|
||||
//Generate salt if it doesn't exist yet
|
||||
if(!salt){
|
||||
salt = crypto.randomBytes(64).toString('base64');
|
||||
}
|
||||
|
||||
return hash === crypto.pbkdf2Sync(
|
||||
password,
|
||||
new Buffer(salt, 'base64'),
|
||||
10000,
|
||||
128,
|
||||
'SHA1'
|
||||
).toString('base64');
|
||||
}
|
||||
|
||||
hash (password): Promise<String> {
|
||||
return bcrypt.hash(password, 4)
|
||||
}
|
||||
}
|
||||
26
api/src/auth/strategies/jwt.refresh.strategy.ts
Normal file
26
api/src/auth/strategies/jwt.refresh.strategy.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { jwtConstants } from '../constants';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import {AuthUser} from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt.refresh') {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any): Promise<AuthUser | null> {
|
||||
if (!payload.refresh) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO add refresh token invalidation uppon usage! They should only work once
|
||||
return this.authService.validate(payload.username);
|
||||
}
|
||||
}
|
||||
21
api/src/auth/strategies/jwt.strategy.ts
Normal file
21
api/src/auth/strategies/jwt.strategy.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { jwtConstants } from '../constants';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import {AuthUser} from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any): Promise<AuthUser | null> {
|
||||
return this.authService.validate(payload.username);
|
||||
}
|
||||
}
|
||||
16
api/src/auth/strategies/password.strategy.ts
Normal file
16
api/src/auth/strategies/password.strategy.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import {AuthUser} from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class PasswordStrategy extends PassportStrategy(Strategy, 'password') {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
validate(username: string, password: string): Promise<AuthUser | null> {
|
||||
return this.authService.verifyForLogin(username, password);
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,20 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// app.enableCors({ origin: '*' });
|
||||
// app.getHttpAdapter().options('*', cors());
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({
|
||||
disableErrorMessages: false,
|
||||
transform: true,
|
||||
}));
|
||||
|
||||
const options = new DocumentBuilder()
|
||||
.setTitle('OhMyForm')
|
||||
.setDescription('API documentation')
|
||||
|
||||
19
api/src/users/interfaces/user.interface.ts
Normal file
19
api/src/users/interfaces/user.interface.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
export interface User extends Document{
|
||||
readonly firstName: string;
|
||||
readonly lastName: string;
|
||||
readonly email: string;
|
||||
readonly username: string;
|
||||
readonly passwordHash: string;
|
||||
readonly salt: string;
|
||||
readonly provider: string;
|
||||
readonly roles: [string];
|
||||
readonly language: string;
|
||||
readonly created: Date;
|
||||
readonly lastUpdated: Date;
|
||||
readonly resetPasswordToken: string;
|
||||
readonly resetPasswordExpires: Date;
|
||||
readonly token: string;
|
||||
readonly apiKey: string;
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { UsersService } from './users.service';
|
||||
import {UserSchema} from "./schemas/user.schema"
|
||||
|
||||
@Module({
|
||||
providers: [],
|
||||
exports: [],
|
||||
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
18
api/src/users/users.service.spec.ts
Normal file
18
api/src/users/users.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
28
api/src/users/users.service.ts
Normal file
28
api/src/users/users.service.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Model } from 'mongoose';
|
||||
import {Injectable, NotFoundException} from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {User} from "./interfaces/user.interface"
|
||||
|
||||
export type User = any;
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@InjectModel('User') private readonly userModel: Model<User>) {}
|
||||
|
||||
async findOneByIdentifier(identifier: string): Promise<User> {
|
||||
const results = await this.userModel.find().or([
|
||||
{
|
||||
username: identifier
|
||||
},
|
||||
{
|
||||
email: identifier
|
||||
}
|
||||
]).exec();
|
||||
|
||||
if (results.length !== 1) {
|
||||
throw new NotFoundException()
|
||||
}
|
||||
|
||||
return results[0]
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user