Skip to content

Commit

Permalink
feat: 인증 단계에서 라라벨 세션을 공유하도록 구현 (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coalery authored Oct 3, 2024
2 parents 753f3ce + 09b78f1 commit 381f65d
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 22 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ test.sqlite3
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json

.env.production
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@nestjs/cqrs": "^10.2.5",
"@nestjs/platform-express": "^10.0.0",
"nestjs-cls": "^3.5.1",
"php-serialize": "^5.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
Expand Down
40 changes: 27 additions & 13 deletions src/core/auth/AuthGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,53 @@ import { ClsService } from 'nestjs-cls';
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';

import { ITokenVerifier, TokenVerifier } from '@sight/core/auth/ITokenVerifier';

import { Message } from '@sight/constant/message';
import { LaravelAuthnAdapter } from './LaravelAuthnAdapter';
import { IRequester } from './IRequester';
import { InjectRepository } from '@mikro-orm/nestjs';
import { User } from '@sight/app/domain/user/model/User';
import { EntityRepository } from '@mikro-orm/core';
import { UserRole } from './UserRole';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Inject(TokenVerifier)
private readonly tokenVerifier: ITokenVerifier,
private readonly laravelAuthnAdapter: LaravelAuthnAdapter,
private readonly clsService: ClsService,

@InjectRepository(User)
private readonly userRepository: EntityRepository<User>,
) {}

canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req: Request = context.switchToHttp().getRequest();
const authorizationHeader = req.headers['authorization'];
const rawSession = req.cookies['khlug_session'];

if (!authorizationHeader) {
if (!rawSession) {
throw new UnauthorizedException(Message.TOKEN_REQUIRED);
}

const token = authorizationHeader.split(' ')[1];
if (!token) {
throw new UnauthorizedException(Message.TOKEN_REQUIRED);
const requesterUserId =
await this.laravelAuthnAdapter.authenticate(rawSession);
if (!requesterUserId) {
throw new UnauthorizedException();
}

const requester = this.tokenVerifier.verify(token);
req['requester'] = requester;
const user = await this.userRepository.findOne({ id: requesterUserId });
if (!user) {
throw new UnauthorizedException();
}

const requester: IRequester = {
userId: requesterUserId,
role: user.manager ? UserRole.MANAGER : UserRole.USER,
};

req['requester'] = requester;
this.clsService.set('requester', requester);

return true;
Expand Down
7 changes: 0 additions & 7 deletions src/core/auth/ITokenVerifier.ts

This file was deleted.

87 changes: 87 additions & 0 deletions src/core/auth/LaravelAuthnAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import fs from 'fs/promises';
import crypto from 'crypto';
import { unserialize } from 'php-serialize';

import { LaravelSessionConfig } from '../config/LaravelSessionConfig';

type EncryptedSession = {
iv: string;
value: string;
mac: string;
};

type SessionData = {
_token: string;
_previous: {
url: string;
};
_flash: {
old: any;
new: any;
};
[key: `login_${string}`]: number | undefined;
};

@Injectable()
export class LaravelAuthnAdapter {
private readonly config: LaravelSessionConfig;

constructor(configService: ConfigService) {
this.config = configService.getOrThrow<LaravelSessionConfig>('session');
}

async authenticate(rawSession: string): Promise<string | null> {
const session = this.normalize(rawSession);
const decrypted = this.decrypt(session);

const sessionId = unserialize(decrypted);
const serializedSessionData = await this.getSessionData(sessionId);

const sessionData: SessionData = unserialize(serializedSessionData);
const userId = this.getUserId(sessionData);

return userId;
}

private normalize(session: string): EncryptedSession {
const urlDecoded = decodeURIComponent(session);
const base64Decoded = Buffer.from(urlDecoded, 'base64').toString('utf-8');
return JSON.parse(base64Decoded);
}

private decrypt(session: EncryptedSession): string {
const key = this.config.secret;

const iv = Buffer.from(session.iv, 'base64');
const value = Buffer.from(session.value, 'base64');

const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
const decrypted = Buffer.concat([decipher.update(value), decipher.final()]);

const hash = crypto
.createHmac('sha256', key)
.update(session.iv + session.value)
.digest()
.toString('hex');

if (session.mac !== hash) {
throw new Error('Invalid MAC');
}

return decrypted.toString();
}

private async getSessionData(sessionId: string): Promise<string> {
const path = `${this.config.storagePath}/${sessionId}`;
return await fs.readFile(path, { encoding: 'utf8' });
}

private getUserId(sessionData: SessionData): string | null {
const loginKey = Object.keys(sessionData).find((key) =>
key.startsWith('login_'),
);
return loginKey ? String(sessionData[loginKey]) : null;
}
}
2 changes: 1 addition & 1 deletion src/core/auth/UserRole.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const UserRole = {
USER: 'USER',
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
} as const;
export type UserRole = (typeof UserRole)[keyof typeof UserRole];
10 changes: 10 additions & 0 deletions src/core/config/LaravelSessionConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type LaravelSessionConfig = {
secret: string;
storagePath: string;
};

// 레거시 시스템과의 호환성을 위해 Laravel 세션을 사용합니다.
export const config = (): LaravelSessionConfig => ({
secret: process.env.APP_KEY || '',
storagePath: process.env.SESSION_STORAGE_PATH || '',
});
3 changes: 3 additions & 0 deletions src/core/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as db from '@sight/core/config/DatabaseConfig';
import * as session from '@sight/core/config/LaravelSessionConfig';

export const configuration = (): {
database: db.DatabaseConfig;
session: session.LaravelSessionConfig;
} => ({
database: db.config(),
session: session.config(),
});

0 comments on commit 381f65d

Please sign in to comment.