Skip to content

Commit

Permalink
feat: add refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
fasenderos committed Aug 24, 2023
1 parent 646df2b commit 51332f6
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 84 deletions.
2 changes: 1 addition & 1 deletion packages/api-gateway/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SERVER_ADDRESS="127.0.0.1"
SERVER_PORT="3001"

JWT_SECRET_ACCESS_TOKEN="change-me-jwt-secret"
COOKIE_SECRET="change-me-cookie-secret"
JWT_SECRET_REFRESH_TOKEN="change-me-jwt-refresh-secret"

POSTGRES_HOST="127.0.0.1"
POSTGRES_PORT="5432"
Expand Down
27 changes: 16 additions & 11 deletions packages/api-gateway/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,37 @@ import {
} from '@nestjs/common';
import { SignUpDto } from './dto/signup.dto';
import { SignInDto } from './dto/signin.dto';
import { AuthService } from './auth.service';
import { RealIP } from 'nestjs-real-ip';
import { AuthService, ISignInResponse } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { JwtRefreshGuard } from './guards/jwt-refresh.guard';

@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}

@Post('sign-up')
async signUp(@Body(ValidationPipe) dto: SignUpDto): Promise<void> {
signUp(@Body(ValidationPipe) dto: SignUpDto): Promise<void> {
const { email, password } = dto;
return await this.auth.signUp(email, password);
return this.auth.signUp(email, password);
}

@Post('sign-in')
async signIn(
@RealIP() userIp: string,
@Body(ValidationPipe) dto: SignInDto,
): Promise<any> {
signIn(@Body(ValidationPipe) dto: SignInDto): Promise<ISignInResponse> {
const { email, password } = dto;
return await this.auth.signIn(email, password, userIp);
return this.auth.signIn(email, password);
}

@UseGuards(JwtAuthGuard)
@Get('logout')
async logout(@Headers('Authorization') auth: string): Promise<any> {
return await this.auth.logout(auth);
logout(@Headers('Authorization') auth: string): Promise<void> {
return this.auth.logout(auth);
}

@UseGuards(JwtRefreshGuard)
@Get('refresh-token')
refreshToken(
@Headers('Authorization') auth: string,
): Promise<ISignInResponse> {
return this.auth.refreshToken(auth);
}
}
25 changes: 9 additions & 16 deletions packages/api-gateway/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AppConfig } from '../../typings/common';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
import { SessionService } from './session.service';
Expand All @@ -11,27 +9,22 @@ import { AuthService } from './auth.service';
import { Session } from './entities/session.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtStrategy } from './strategies/jwt-strategy';
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';

@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => ({
secret: configService.get<AppConfig['auth']['secretAccessToken']>(
'auth.secretAccessToken',
) as string,
signOptions: {
expiresIn: configService.get<AppConfig['auth']['expAccessToken']>(
'auth.expAccessToken',
),
},
}),
inject: [ConfigService],
}),
JwtModule.register({}),
TypeOrmModule.forFeature([Session]),
UserModule,
],
providers: [AuthService, JwtStrategy, SessionService, TokenService],
providers: [
AuthService,
JwtStrategy,
JwtRefreshStrategy,
SessionService,
TokenService,
],
controllers: [AuthController],
})
export class AuthModule {}
44 changes: 27 additions & 17 deletions packages/api-gateway/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ export class AuthService {
await this.user.createUser({ email, password });
}

async signIn(
email: string,
password: string,
userIp: string,
): Promise<ISignInResponse> {
async signIn(email: string, password: string): Promise<ISignInResponse> {
const user = await this.user.getUserByEmail(email);
if (user == null)
throw new UnauthorizedException(
Expand All @@ -39,27 +35,41 @@ export class AuthService {
);

const now = Date.now();
const session = await this.session.createSession({
userId: user.id,
userIp,
now,
});
const accessToken = await this.token.generateAccessToken(
const session = await this.session.createSession(user.id, now);
const { accessToken, refreshToken } = await this.createTokens(
user.id,
session.id,
now,
);
const refreshToken = await this.token.generateRefreshToken(
user.id,
session.id,
now,
);

return { accessToken, refreshToken };
}

async logout(auth: string): Promise<void> {
const jwt = this.token.decode(auth);
await this.session.deleteById(jwt.jti, false);
}

async refreshToken(auth: string): Promise<ISignInResponse> {
const jwt = this.token.decode(auth);
const now = Date.now();
const { accessToken, refreshToken } = await this.createTokens(
jwt.sub,
jwt.jti,
now,
);
await this.session.refreshSession(jwt.jti, now);
return { accessToken, refreshToken };
}

private async createTokens(
userId: string,
sessionId: string,
now: number,
): Promise<ISignInResponse> {
const tokens = await Promise.all([
this.token.generateAccessToken(userId, sessionId, now),
this.token.generateRefreshToken(userId, sessionId, now),
]);
return { accessToken: tokens[0], refreshToken: tokens[1] };
}
}
3 changes: 0 additions & 3 deletions packages/api-gateway/src/auth/entities/session.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,5 @@ export class Session {
userId!: string;

@Column()
userIp!: string;

@Column({ type: 'timestamp' })
expires!: Date;
}
5 changes: 5 additions & 0 deletions packages/api-gateway/src/auth/guards/jwt-refresh.guard.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 JwtRefreshGuard extends AuthGuard('jwt-refresh') {}
19 changes: 7 additions & 12 deletions packages/api-gateway/src/auth/session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import { ConfigService } from '@nestjs/config';
import timestring from 'timestring';
import { BaseService } from '../base/base.service';

interface ISessionCreate {
userId: string;
userIp: string;
now: number;
}

@Injectable()
export class SessionService extends BaseService<Session> {
EXP_MS_REFRESH: number;
Expand All @@ -28,13 +22,14 @@ export class SessionService extends BaseService<Session> {
this.EXP_MS_REFRESH = timestring(expRefreshToken, 'ms');
}

async createSession({
userId,
userIp,
now,
}: ISessionCreate): Promise<Session> {
async createSession(userId: string, now: number): Promise<Session> {
const expires = new Date(now + this.EXP_MS_REFRESH);
const session = await this.session.insert({ userId, userIp, expires });
const session = await this.session.insert({ userId, expires });
return session.raw[0];
}

async refreshSession(sessionId: string, now: number): Promise<void> {
const expires = new Date(now + this.EXP_MS_REFRESH);
await this.session.update({ id: sessionId }, { expires });
}
}
38 changes: 38 additions & 0 deletions packages/api-gateway/src/auth/strategies/jwt-refresh.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { User } from '../../user/user.entity';
import { IJwtPayload } from '../token.service';
import { UserService } from '../../user/user.service';
import { SessionService } from '../session.service';

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
Strategy,
'jwt-refresh',
) {
constructor(
readonly config: ConfigService,
private readonly session: SessionService,
private readonly user: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get<string>('auth.secretRefreshToken'),
});
}

async validate(payload: IJwtPayload): Promise<User> {
// Check that the session exist. On logout the session is destroyed but the token may still be valid
const session = await this.session.findById(payload.jti);
if (session === null || session.expires.getTime() < Date.now())
throw new UnauthorizedException();

// Get user from DB and add the result on req.user
const user = await this.user.findById(payload.sub);
if (user === null) throw new UnauthorizedException();
return user;
}
}
3 changes: 2 additions & 1 deletion packages/api-gateway/src/auth/strategies/jwt-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
async validate(payload: IJwtPayload): Promise<User> {
// Check that the session exist. On logout the session is destroyed but the token may still be valid
const session = await this.session.findById(payload.jti);
if (session === null) throw new UnauthorizedException();
if (session === null || session.expires.getTime() < Date.now())
throw new UnauthorizedException();

// Get user from DB and add the result on req.user
const user = await this.user.findById(payload.sub);
Expand Down
10 changes: 10 additions & 0 deletions packages/api-gateway/src/auth/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface IJwtPayload {
@Injectable()
export class TokenService {
APP_NAME: string;
ACCESS_SECRET: string;
REFRESH_SECRET: string;
EXP_ACCESS: string;
EXP_REFRESH: string;

Expand All @@ -22,6 +24,12 @@ export class TokenService {
private readonly config: ConfigService,
) {
this.APP_NAME = this.config.get<string>('app.name') as string;
this.ACCESS_SECRET = this.config.get<string>(
'auth.secretAccessToken',
) as string;
this.REFRESH_SECRET = this.config.get<string>(
'auth.secretRefreshToken',
) as string;
this.EXP_ACCESS = this.config.get<string>('auth.expAccessToken') as string;
this.EXP_REFRESH = this.config.get<string>(
'auth.expRefreshToken',
Expand All @@ -41,6 +49,7 @@ export class TokenService {
iat: now,
};
const accessToken = await this.jwt.signAsync(payload, {
secret: this.ACCESS_SECRET,
expiresIn: this.EXP_ACCESS,
});
return accessToken;
Expand All @@ -59,6 +68,7 @@ export class TokenService {
iat: now,
};
const accessToken = await this.jwt.signAsync(payload, {
secret: this.REFRESH_SECRET,
expiresIn: this.EXP_REFRESH,
});
return accessToken;
Expand Down
4 changes: 2 additions & 2 deletions packages/api-gateway/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ export default (): AppConfig => ({
},
auth: {
expAccessToken: '15m',
expRefreshToken: '30d',
expRefreshToken: '7d',
expVerifyMail: '8m',
expResetPassword: '8m',
secretAccessToken: ensureValues('JWT_SECRET_ACCESS_TOKEN'),
cookieSecret: ensureValues('COOKIE_SECRET'),
secretRefreshToken: ensureValues('JWT_SECRET_REFRESH_TOKEN'),
},
db: {
host: ensureValues('POSTGRES_HOST'),
Expand Down
42 changes: 21 additions & 21 deletions packages/api-gateway/typings/common/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
export type EmptyObject = {
[K in any]: never
}
[K in any]: never;
};

export interface AppConfig {
app: {
name: string
version: string
}
name: string;
version: string;
};
auth: {
expAccessToken: string
expRefreshToken: string
expVerifyMail: string
expResetPassword: string
secretAccessToken: string
cookieSecret: string
}
expAccessToken: string;
expRefreshToken: string;
expVerifyMail: string;
expResetPassword: string;
secretAccessToken: string;
secretRefreshToken: string;
};
db: {
host: string
port: number
username: string
password: string
database: string
}
host: string;
port: number;
username: string;
password: string;
database: string;
};
server: {
address: string
port: number
}
address: string;
port: number;
};
}

0 comments on commit 51332f6

Please sign in to comment.