포시코딩

[NestJS] SAML Assertion에 값 추가 (passport-saml, strategy) 본문

Node.js

[NestJS] SAML Assertion에 값 추가 (passport-saml, strategy)

포시 2024. 2. 5. 18:41
728x90

Guard

import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { SamlStrategy } from './saml.strategy';

@Injectable()
export class SamlAuthGuard extends AuthGuard('saml') {
  constructor(private readonly samlStrategy: SamlStrategy) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    request.query.RelayState = request.headers.referer;
    return super.canActivate(context);
  }

  handleRequest(err, user) {
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

 

Strategy

import { ForbiddenException, Headers, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from '@node-saml/passport-saml';
import fs from 'fs';
import { AuthService } from './auth.service';

@Injectable()
export class SamlStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      issuer: '*****',
      callbackUrl: '*****',
      cert: fs.readFileSync(`${process.cwd()}/*****.cer`, 'utf8'),
      entryPoint: '*****',
    });
  }

  async authenticate(request: any, options?: any): Promise<any> {
    this._saml.options.additionalParams.RelayState = request.query.RelayState;
    return super.authenticate(request, options);
  }

  async validate(profile: Profile) {
    const user = this.authService.validateTempAccount(profile.nameID);
    if (!user) {
      throw new UnauthorizedException('등록되지 않은 이메일입니다.');
    }
    return user;
  }
}

 

Controller

@Post('test')
@UseGuards(SamlAuthGuard)
async test(@Request() req, @Res() res) {
  const relayState = req.body?.RelayState;
  const accessToken = await this.authService.createAccessToken(
    req.user.email,
  );

  res.redirect(`${relayState}/callback?auth=${accessToken}`);
}

 

 

MS의 SSO를 구현하던 중

클라이언트 서버에서 로그인 요청 시 MS의 로그인 페이지를 다녀온 후 redirect로 백엔드 서버로 돌아오고, 

이후 다시 클라이언트 서버로 redirect 시켜야 하는 상황에서 맨 처음 요청했던 클라이언트 서버의 referer 값을 갖고 있지 못하는 문제를 겪고 있었다. 

 

login api에 대해 guard로 가기 전에 middleware를 거치게 하여 referer 값을 DB에 저장해서 

클라이언트 서버로 redirect 시키는 시점에서 가져옴과 동시에 DB에서 삭제시키는 방법으로도 진행해봤는데

잘 작동했으나, 여러 클라이언트에서 동시에 로그인 요청 시 발생하는 문제와 DB 커넥션이 이루어져야 한다는 부분이 걸려 계속 리서치를 진행했다. 

 

결국 Strategy에서 authenticate를 통해 SAML Assertion에 값을 추가하는 방법을 발견하여 위와 같이 문제를 해결할 수 있었다. 

 

동료 팀원이 'relayState' 라는 키워드를 통해 찾아낸 방법인데

정작 relayState를 쓰는 방법이 아니라 passport를 완전히 파악하지 못했던게 문제이지 않았나 싶다. 

728x90