Skip to content

Commit

Permalink
Refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
PhamAnhHoang committed Dec 16, 2023
1 parent 9cf3870 commit e2bb037
Show file tree
Hide file tree
Showing 21 changed files with 296 additions and 177 deletions.
19 changes: 17 additions & 2 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@ import { JWTAuthGuard } from './guards/jwt-auth.guard'
import { LocalAuthGuard } from './guards/local-auth.guard'
import { SessionAuthGuard } from './guards/session-auth.guard'
import { TokenInterceptor } from './interceptors/token.interceptor'
import { ConfirmCodeService } from '../user/confirm-code.service'
import {
PasswordResetPerformInterface,
PasswordResetRequestInterface,
Pure,
RefreshTokenResponseInterface,
StatusType
} from '@isomera/interfaces'
import { UserService } from '../user/user.service'
import { JwtRefreshTokenGuard } from './guards/jwt-refresh-token'

@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly confirmCodeService: ConfirmCodeService
private readonly userService: UserService
) {}

@Post('register')
Expand Down Expand Up @@ -97,4 +99,17 @@ export class AuthController {
const result = await this.authService.setNewPassword(body)
return { status: result ? StatusType.OK : StatusType.FAIL }
}

@UseGuards(JwtRefreshTokenGuard)
@Post('/refresh-token')
@HttpCode(HttpStatus.OK)
async refreshToken(
@AuthUser() user: Pure<UserEntity>
): Promise<RefreshTokenResponseInterface> {
const accessToken = this.authService.generateAccessToken(user.email);
return {
access_token: accessToken,
status: StatusType.OK
};
}
}
3 changes: 2 additions & 1 deletion apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LocalStrategy } from './strategies/local.strategy'
import { MailerModule } from '../mailer/mailer.module'
import { ConfirmCodeModule } from '../user/confirm-code.module'
import { OrganizationModule } from '../organization/organization.module'
import { JwtRefreshTokenStrategy } from './strategies/jwt-refresh-token.strategy'

@Module({
imports: [
Expand All @@ -31,6 +32,6 @@ import { OrganizationModule } from '../organization/organization.module'
OrganizationModule
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, SessionSerializer]
providers: [AuthService, LocalStrategy, JwtStrategy, SessionSerializer, JwtRefreshTokenStrategy]
})
export class AuthModule {}
2 changes: 1 addition & 1 deletion apps/api/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('AuthService', () => {
const user = createMock<UserEntity>({ email: '[email protected]' })

mockedJwtService.sign.mockReturnValueOnce('j.w.t')
const {refresh_token, access_token} = service.signToken(user)
const { refresh_token, access_token } = service.signToken(user)

expect(access_token).toEqual(expect.any(String))
expect(refresh_token).toEqual(expect.any(String))
Expand Down
78 changes: 60 additions & 18 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
Expand All @@ -20,6 +21,7 @@ import { Pure } from '@isomera/interfaces'
import { generateRandomStringUtil } from '@isomera/utils'
import { OrganizationService } from '../organization/organization.service'
import { ConfigService } from '@nestjs/config'
import * as bcrypt from 'bcrypt'

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -62,7 +64,7 @@ export class AuthService {
}
}

async login(email: string, password: string): Promise<UserEntity> {
async login(email: string, password: string): Promise<Partial<UserEntity> & { refresh_token: string, access_token: string}> {
let user: UserEntity

try {
Expand All @@ -80,7 +82,11 @@ export class AuthService {
}
delete user.password

return user
const {refresh_token, access_token} = this.signToken(user);

await this.storeRefreshToken(user, refresh_token);

return {...user, refresh_token, access_token}
}

async verifyPayload(payload: JwtPayload): Promise<UserEntity> {
Expand All @@ -98,35 +104,35 @@ export class AuthService {
return user
}

signToken(user: UserEntity): {refresh_token: string, access_token: string} {
signToken(user: UserEntity): { refresh_token: string; access_token: string } {
return {
refresh_token: this.generateRefreshToken(user.email),
access_token: this.generateAccessToken(user.email)
}
}

private generateAccessToken(email: string): string {
public generateAccessToken(email: string): string {
const payload = {
sub: email
}

return this.jwtService.sign(payload, {
expiresIn: `${this.configService.get<string>(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
)}s`,
});
'JWT_ACCESS_TOKEN_EXPIRATION_TIME'
)}s`
})
}

private generateRefreshToken(email: string): string {
public generateRefreshToken(email: string): string {
const payload = {
sub: email
}

return this.jwtService.sign(payload, {
expiresIn: `${this.configService.get<string>(
'JWT_REFRESH_TOKEN_EXPIRATION_TIME',
)}s`,
});
'JWT_REFRESH_TOKEN_EXPIRATION_TIME'
)}s`
})
}

async sendGreetings(user: UserEntity) {
Expand Down Expand Up @@ -177,16 +183,52 @@ export class AuthService {

/**
* After verify user, create personal organization for this user and send email
* @param code
* @param email
* @param code
* @param email
*/
public async verifyCode({code, email}: Pure<ConfirmationCodeDto>): Promise<UserEntity> {
const user = await this.confirmCode.verifyCode(code, email);
public async verifyCode({
code,
email
}: Pure<ConfirmationCodeDto>): Promise<UserEntity> {
const user = await this.confirmCode.verifyCode(code, email)

await this.organizationService.createDefaultOrganization(user.id);
await this.organizationService.createDefaultOrganization(user.id)

await this.sendGreetings(user)

await this.sendGreetings(user);

return user
}

async getUserIfRefreshTokenMatched(
email: string,
refreshToken: string,
): Promise<UserEntity> {
const user = await this.userService.findOne({ where: { email } })
if (!user) {
throw new UnauthorizedException();
}
await this.verifyPlainContentWithHashedContent(
refreshToken,
user.refreshToken,
);
return user;
}

private async verifyPlainContentWithHashedContent(
plainText: string,
hashedText: string,
) {
const is_matching = await bcrypt.compare(plainText, hashedText);
if (!is_matching) {
throw new BadRequestException();
}
}

async storeRefreshToken(user: UserEntity, token: string): Promise<void> {
const salt = await bcrypt.genSalt()
const hashed_token = await bcrypt.hash(token, salt);
await this.userService.storeRefreshToken(user, hashed_token);
}


}
5 changes: 5 additions & 0 deletions apps/api/src/auth/guards/jwt-refresh-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtRefreshTokenGuard extends AuthGuard('jwt-refresh-token') {}
14 changes: 6 additions & 8 deletions apps/api/src/auth/interceptors/token.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ describe('TokenInterceptor', () => {

lastValueFrom(interceptor.intercept(context, next))

jest
.spyOn(mockedAuthService, 'signToken')
.mockImplementationOnce(() => {
return {
refresh_token: 'refresh_token',
access_token: 'jwt'
}
})
jest.spyOn(mockedAuthService, 'signToken').mockImplementationOnce(() => {
return {
refresh_token: 'refresh_token',
access_token: 'jwt'
}
})
jest.spyOn(res, 'getHeader').mockReturnValue('Bearer j.w.t')

expect(res.getHeader('Authorization')).toBe('Bearer j.w.t')
Expand Down
15 changes: 8 additions & 7 deletions apps/api/src/auth/interceptors/token.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@ export class TokenInterceptor implements NestInterceptor {

intercept(
context: ExecutionContext,
next: CallHandler<UserEntity>
): Observable<Partial<UserEntity> & {access_token: string, refresh_token: string}> {
next: CallHandler<Partial<UserEntity> & { access_token: string; refresh_token: string }>
): Observable<
Partial<UserEntity> & { access_token: string; refresh_token: string }
> {
return next.handle().pipe(
map(user => {
map(data => {
const response = context.switchToHttp().getResponse<Response>()
const {refresh_token, access_token} = this.authService.signToken(user)

response.setHeader('Authorization', `Bearer ${access_token}`)
response.cookie('token', access_token, {
response.setHeader('Authorization', `Bearer ${data.access_token}`)
response.cookie('token', data.access_token, {
httpOnly: true,
signed: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
})

return {...user, access_token, refresh_token}
return data
})
)
}
Expand Down
30 changes: 30 additions & 0 deletions apps/api/src/auth/strategies/jwt-refresh-token.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Request } from 'express';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../auth.service';
import { JwtPayload } from '@isomera/interfaces';

@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh-token',
) {
constructor(
private readonly authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.APP_SECRET,
passReqToCallback: true,
});
}

async validate(request: Request, payload: JwtPayload) {
return await this.authService.getUserIfRefreshTokenMatched(
payload.sub,
request.headers.authorization.split('Bearer ')[1],
);
}
}
2 changes: 1 addition & 1 deletion apps/api/src/auth/strategies/local.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
})
}

validate(email: string, password: string): Promise<UserEntity> {
validate(email: string, password: string): Promise<Partial<UserEntity> & { refresh_token: string, access_token: string}> {
return this.authService.login(email, password)
}
}
4 changes: 2 additions & 2 deletions apps/api/src/entities/organization.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
UpdateDateColumn
} from 'typeorm'
import { OrganizationInterface } from '@isomera/interfaces'

Expand All @@ -21,5 +21,5 @@ export class OrganizationEntity implements OrganizationInterface {
@UpdateDateColumn()
updatedAt: Date

static DEFAULT_ORGANIZATION_NAME = 'Isomera personal user'
static DEFAULT_ORGANIZATION_NAME = 'Isomera personal user'
}
2 changes: 1 addition & 1 deletion apps/api/src/entities/user-organization.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
PrimaryGeneratedColumn
} from 'typeorm'
import { UserOrganizationInterace } from '@isomera/interfaces'

Expand Down
Loading

0 comments on commit e2bb037

Please sign in to comment.