add working authentication

This commit is contained in:
wodka 2019-07-30 00:46:56 +02:00
parent f7fab04e54
commit 7c30574bf2
20 changed files with 350 additions and 31 deletions

View File

@ -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",

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

@ -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'
}
};
}
}

View File

@ -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],

View 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 {}

View File

@ -0,0 +1,3 @@
export const jwtConstants = {
secret: 'secretKey',
};

View 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);
}
}

View File

@ -0,0 +1,4 @@
export interface AuthJwt {
access_token: string;
refresh_token: string;
}

View File

@ -0,0 +1,8 @@
export interface AuthUser {
id: string;
username: string;
email: string;
roles: [string];
created: Date;
lastUpdated: Date;
}

View 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();
});
});

View 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',
}
),
};
}
}

View 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)
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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')

View 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;
}

View File

@ -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 {}

View 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();
});
});

View 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]
}
}