feat: rewrite auth (#364)

* feat: wip rewrite auth

* feat: restructure folders and fix stories and tests

* feat: remove auth provider and fix tests
This commit is contained in:
Jérémy M
2023-06-23 17:49:50 +02:00
committed by GitHub
parent 1c7980b270
commit c6708b2c1f
54 changed files with 1268 additions and 584 deletions

View File

@ -5,13 +5,12 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
import { AuthService } from './services/auth.service';
import { GoogleAuthController } from './controllers/google-auth.controller';
import { GoogleStrategy } from './strategies/google.auth.strategy';
import { TokenController } from './controllers/token.controller';
import { PrismaService } from 'src/database/prisma.service';
import { UserModule } from '../user/user.module';
import { AuthController } from './controllers/auth.controller';
import { PasswordAuthController } from './controllers/password-auth.controller';
import { VerifyAuthController } from './controllers/verify-auth.controller';
import { TokenService } from './services/token.service';
import { AuthResolver } from './auth.resolver';
const jwtModule = JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
@ -28,18 +27,14 @@ const jwtModule = JwtModule.registerAsync({
@Module({
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
controllers: [
GoogleAuthController,
PasswordAuthController,
TokenController,
AuthController,
],
controllers: [GoogleAuthController, VerifyAuthController],
providers: [
AuthService,
TokenService,
JwtAuthStrategy,
GoogleStrategy,
PrismaService,
AuthResolver,
],
exports: [jwtModule],
})

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthResolver } from './auth.resolver';
import { TokenService } from './services/token.service';
import { AuthService } from './services/auth.service';
describe('AuthResolver', () => {
let resolver: AuthResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthResolver,
{
provide: AuthService,
useValue: {},
},
{
provide: TokenService,
useValue: {},
},
],
}).compile();
resolver = module.get<AuthResolver>(AuthResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,49 @@
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthTokens } from './dto/token.entity';
import { TokenService } from './services/token.service';
import { RefreshTokenInput } from './dto/refresh-token.input';
import { BadRequestException } from '@nestjs/common';
import { Verify } from './dto/verify.entity';
import { VerifyInput } from './dto/verify.input';
import { AuthService } from './services/auth.service';
import { LoginToken } from './dto/login-token.entity';
import { ChallengeInput } from './dto/challenge.input';
@Resolver()
export class AuthResolver {
constructor(
private authService: AuthService,
private tokenService: TokenService,
) {}
@Mutation(() => LoginToken)
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
const user = await this.authService.challenge(challengeInput);
const loginToken = await this.tokenService.generateLoginToken(user.email);
return { loginToken };
}
@Mutation(() => Verify)
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
const email = await this.tokenService.verifyLoginToken(
verifyInput.loginToken,
);
const result = await this.authService.verify(email);
return result;
}
@Mutation(() => AuthTokens)
async renewToken(@Args() args: RefreshTokenInput): Promise<AuthTokens> {
if (!args.refreshToken) {
throw new BadRequestException('Refresh token is mendatory');
}
const tokens = await this.tokenService.generateTokensFromRefreshToken(
args.refreshToken,
);
return { tokens: tokens };
}
}

View File

@ -1,30 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PasswordAuthController } from './password-auth.controller';
import { AuthService } from '../services/auth.service';
import { TokenService } from '../services/token.service';
describe('PasswordAuthController', () => {
let controller: PasswordAuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PasswordAuthController],
providers: [
{
provide: AuthService,
useValue: {},
},
{
provide: TokenService,
useValue: {},
},
],
}).compile();
controller = module.get<PasswordAuthController>(PasswordAuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -1,23 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ChallengeInput } from '../dto/challenge.input';
import { AuthService } from '../services/auth.service';
import { LoginTokenEntity } from '../dto/login-token.entity';
import { TokenService } from '../services/token.service';
@Controller('auth/password')
export class PasswordAuthController {
constructor(
private readonly authService: AuthService,
private readonly tokenService: TokenService,
) {}
@Post()
async challenge(
@Body() challengeInput: ChallengeInput,
): Promise<LoginTokenEntity> {
const user = await this.authService.challenge(challengeInput);
const loginToken = await this.tokenService.generateLoginToken(user.email);
return { loginToken };
}
}

View File

@ -1,21 +0,0 @@
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
import { RefreshTokenInput } from '../dto/refresh-token.input';
import { TokenService } from '../services/token.service';
@Controller('auth/token')
export class TokenController {
constructor(private tokenService: TokenService) {}
@Post()
async generateAccessToken(@Body() body: RefreshTokenInput) {
if (!body.refreshToken) {
throw new BadRequestException('Refresh token is mendatory');
}
const tokens = await this.tokenService.generateTokensFromRefreshToken(
body.refreshToken,
);
return { tokens: tokens };
}
}

View File

@ -1,14 +1,14 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { VerifyAuthController } from './verify-auth.controller';
import { AuthService } from '../services/auth.service';
import { TokenService } from '../services/token.service';
describe('AuthController', () => {
let controller: AuthController;
describe('VerifyAuthController', () => {
let controller: VerifyAuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
controllers: [VerifyAuthController],
providers: [
{
provide: AuthService,
@ -21,7 +21,7 @@ describe('AuthController', () => {
],
}).compile();
controller = module.get<AuthController>(AuthController);
controller = module.get<VerifyAuthController>(VerifyAuthController);
});
it('should be defined', () => {

View File

@ -1,18 +1,18 @@
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
import { VerifyInput } from '../dto/verify.input';
import { VerifyEntity } from '../dto/verify.entity';
import { Verify } from '../dto/verify.entity';
import { TokenService } from '../services/token.service';
@Controller('auth')
export class AuthController {
@Controller('auth/verify')
export class VerifyAuthController {
constructor(
private readonly authService: AuthService,
private readonly tokenService: TokenService,
) {}
@Post('verify')
async verify(@Body() verifyInput: VerifyInput): Promise<VerifyEntity> {
@Post()
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
const email = await this.tokenService.verifyLoginToken(
verifyInput.loginToken,
);

View File

@ -1,10 +1,14 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class ChallengeInput {
@Field(() => String)
@IsNotEmpty()
@IsEmail()
email: string;
@Field(() => String)
@IsNotEmpty()
@IsString()
password: string;

View File

@ -1,5 +1,8 @@
import { TokenEntity } from './token.entity';
import { Field, ObjectType } from '@nestjs/graphql';
import { AuthToken } from './token.entity';
export class LoginTokenEntity {
loginToken: TokenEntity;
@ObjectType()
export class LoginToken {
@Field(() => AuthToken)
loginToken: AuthToken;
}

View File

@ -1,6 +1,9 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class RefreshTokenInput {
@Field(() => String)
@IsNotEmpty()
@IsString()
refreshToken: string;

View File

@ -6,18 +6,23 @@ import {
MinLength,
} from 'class-validator';
import { PASSWORD_REGEX } from '../auth.util';
import { ArgsType, Field } from '@nestjs/graphql';
@ArgsType()
export class RegisterInput {
@Field(() => String)
@IsNotEmpty()
@IsEmail()
email: string;
@Field(() => String)
@IsNotEmpty()
@IsString()
@MinLength(8)
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
password: string;
@Field(() => String)
@IsNotEmpty()
@IsString()
displayName: string;

View File

@ -1,4 +1,25 @@
export class TokenEntity {
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class AuthToken {
@Field(() => String)
token: string;
@Field(() => Date)
expiresAt: Date;
}
@ObjectType()
export class AuthTokenPair {
@Field(() => AuthToken)
accessToken: AuthToken;
@Field(() => AuthToken)
refreshToken: AuthToken;
}
@ObjectType()
export class AuthTokens {
@Field(() => AuthTokenPair)
tokens: AuthTokenPair;
}

View File

@ -1,11 +1,9 @@
import { TokenEntity } from './token.entity';
import { User } from '@prisma/client';
import { Field, ObjectType } from '@nestjs/graphql';
import { AuthTokens } from './token.entity';
import { User } from 'src/core/@generated/user/user.model';
export class VerifyEntity {
user: Omit<User, 'passwordHash'>;
tokens: {
accessToken: TokenEntity;
refreshToken: TokenEntity;
};
@ObjectType()
export class Verify extends AuthTokens {
@Field(() => User)
user: User;
}

View File

@ -1,6 +1,9 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class VerifyInput {
@Field(() => String)
@IsNotEmpty()
@IsString()
loginToken: string;

View File

@ -9,7 +9,7 @@ import { UserService } from 'src/core/user/user.service';
import { assert } from 'src/utils/assert';
import { RegisterInput } from '../dto/register.input';
import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util';
import { VerifyEntity } from '../dto/verify.entity';
import { Verify } from '../dto/verify.entity';
import { TokenService } from './token.service';
export type UserPayload = {
@ -73,17 +73,17 @@ export class AuthService {
return user;
}
async verify(email: string): Promise<VerifyEntity> {
const data = await this.userService.findUnique({
async verify(email: string): Promise<Verify> {
const user = await this.userService.findUnique({
where: {
email,
},
});
assert(data, "This user doesn't exist", NotFoundException);
assert(user, "This user doesn't exist", NotFoundException);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { passwordHash: _, ...user } = data;
// passwordHash is hidden for security reasons
user.passwordHash = '';
const accessToken = await this.tokenService.generateAccessToken(user.id);
const refreshToken = await this.tokenService.generateRefreshToken(user.id);

View File

@ -13,7 +13,7 @@ import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { TokenEntity } from '../dto/token.entity';
import { AuthToken } from '../dto/token.entity';
import { TokenExpiredError } from 'jsonwebtoken';
@Injectable()
@ -24,7 +24,7 @@ export class TokenService {
private readonly prismaService: PrismaService,
) {}
async generateAccessToken(userId: string): Promise<TokenEntity> {
async generateAccessToken(userId: string): Promise<AuthToken> {
const expiresIn = this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN');
assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
@ -55,7 +55,7 @@ export class TokenService {
};
}
async generateRefreshToken(userId: string): Promise<TokenEntity> {
async generateRefreshToken(userId: string): Promise<AuthToken> {
const secret = this.configService.get('REFRESH_TOKEN_SECRET');
const expiresIn = this.configService.get<string>(
'REFRESH_TOKEN_EXPIRES_IN',
@ -86,7 +86,7 @@ export class TokenService {
};
}
async generateLoginToken(email: string): Promise<TokenEntity> {
async generateLoginToken(email: string): Promise<AuthToken> {
const secret = this.configService.get('LOGIN_TOKEN_SECRET');
const expiresIn = this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN');
assert(expiresIn, '', InternalServerErrorException);
@ -163,8 +163,8 @@ export class TokenService {
}
async generateTokensFromRefreshToken(token: string): Promise<{
accessToken: TokenEntity;
refreshToken: TokenEntity;
accessToken: AuthToken;
refreshToken: AuthToken;
}> {
const {
user,

View File

@ -1,14 +1,9 @@
import { Module } from '@nestjs/common';
import { WorkspaceService } from './services/workspace.service';
import { WorkspaceMemberService } from './services/workspace-member.service';
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
@Module({
providers: [
WorkspaceService,
WorkspaceMemberService,
WorkspaceMemberResolver,
],
providers: [WorkspaceService, WorkspaceMemberService],
exports: [WorkspaceService, WorkspaceMemberService],
})
export class WorkspaceModule {}