본문으로 건너뛰기

안동민 개발노트

본문 시작

OAuth 2.0과 OIDC 소셜 로그인

Google 권한 코드 흐름을 NestJS와 Passport로 구현하고, OAuth와 OIDC의 경계, 계정 연결, 로컬 세션, 토큰 수명 주기를 안전하게 설계합니다.

지난 절에서는 로컬 자격 증명, JWT 인증, RBAC를 각각 다루었습니다. 소셜 로그인은 이 흐름을 없애는 기능이 아니라, 외부 제공자가 확인한 정체성을 우리 서비스의 계정과 로그인 상태로 연결하는 또 하나의 인증 진입점입니다.

여기서는 브라우저가 Google을 거쳐 돌아오는 권한 코드 흐름을 사용합니다. 브라우저는 코드와 상태 값만 운반하고, 코드를 Google 토큰으로 바꾸는 작업과 사용자 정보 조회는 NestJS 서버가 수행합니다.

브라우저가 NestJS에서 Google 로그인을 시작하면 서버가 state와 PKCE verifier를 보관하고 정확한 콜백 URI로 권한 요청을 보낸다. 돌아온 code와 state를 검증한 서버가 code를 token으로 교환하고 Google UserInfo의 안정적인 subject를 로컬 계정에 연결한 뒤, 앱 token을 URL에 노출하지 않고 HttpOnly 로컬 세션 쿠키를 만든다.

Nest · OAuth Authorization Code

브라우저는 코드를 운반하고, 서버가 신뢰 경계를 닫는다

이 예제는 OAuth 2.0 access token으로 Google UserInfo를 조회하는 흐름입니다. state는 시작 요청을 콜백에 묶고, PKCE S256은 code verifier를 가진 NestJS만 코드를 교환하게 합니다.

Google OAuth 권한 코드 로그인 시퀀스 브라우저가 NestJS 로그인 엔드포인트를 호출한다. NestJS는 일회용 state와 PKCE verifier를 서버 세션에 저장하고 S256 challenge와 정확한 redirect URI를 포함해 Google로 보낸다. Google은 code와 state를 콜백으로 돌려보낸다. NestJS는 state를 검증하고 서버에서 code와 verifier를 token으로 교환한 뒤 UserInfo의 안정적인 subject를 조회한다. 로컬 계정 연결 정책을 적용한 후 고정된 화면으로 리다이렉트하며 HttpOnly 로컬 세션 쿠키를 설정한다. GET /auth/google state + verifier 저장 302 · state + S256 authorize · exact redirect_uri 302 · code + state GET callback POST token · verifier access token Bearer access token stable sub + profile 303 + HttpOnly local session 브라우저 redirect carrier NestJS OAuth confidential client Google 권한 서버 authorize · token Google UserInfo resource server

권한 코드 왕복

  1. 트랜잭션을 만듭니다. 브라우저가 /auth/google을 호출하면 NestJS는 일회용 state와 PKCE verifier를 짧은 서버 세션에 보관합니다.

  2. 정확한 콜백을 지정합니다. NestJS는 S256 challenge와 등록 값과 정확히 같은 redirect_uri를 포함해 Google 권한 endpoint로 리다이렉트합니다.

  3. 브라우저가 code를 운반합니다. 인증과 동의 뒤 Google은 일회용 code와 원래 state를 콜백으로 돌려보냅니다.

  4. 서버가 검증하고 교환합니다. NestJS는 state를 확인한 뒤 서버에서 code, verifier, 같은 redirect_uri로 Google token endpoint를 호출합니다.

  5. provider 정체성을 읽습니다. NestJS는 provider access token으로 UserInfo를 조회하고 안정적인 sub를 얻습니다. Google token은 우리 서비스의 로그인 자격 증명이 아닙니다.

  6. 로컬 경계를 만듭니다. (google, sub) 연결 정책을 적용하고 고정된 완료 화면으로 이동하면서 HttpOnly 로컬 세션 쿠키를 설정합니다. 앱 JWT는 URL이나 localStorage로 보내지 않습니다.

OIDC 변형

openid scope로 ID token을 받는 구현은 요청별 nonce를 저장하고, ID token의 JWKS 서명, 허용 알고리즘, iss, aud, exp, nonce를 모두 검증합니다.

OIDC 내부 연결 키는 검증된 (iss, sub)입니다. 이 OAuth UserInfo 예제가 그 ID token 검증을 대신하지 않습니다.

provider access token은 Google 자원용이고, 로컬 세션은 우리 서비스용입니다. 두 자격 증명의 저장·만료·폐기 정책을 섞지 않습니다.


OAuth 2.0과 OIDC를 구분하기

OAuth 2.0은 클라이언트가 사용자를 대신해 자원 서버에 접근할 권한을 위임받는 프레임워크입니다. access token의 대상은 Google API 같은 자원 서버이며, 그 토큰 자체가 우리 서비스의 로그인 세션은 아닙니다.

**OpenID Connect(OIDC)**는 OAuth 2.0 위에 인증 계층을 추가합니다. openid scope를 요청하고 검증된 ID token의 isssub로 사용자를 식별합니다. 따라서 “Google로 로그인”을 엄밀히 OIDC로 구현하려면 ID token 검증과 nonce 검증까지 포함해야 합니다.

이 절의 passport-google-oauth20 예제는 Google token endpoint에서 access token을 받은 뒤 Google UserInfo endpoint에서 프로필을 읽는 OAuth 2.0 + UserInfo 방식입니다. 이 전략이 OIDC ID token의 서명이나 nonce를 검증한다고 가정하면 안 됩니다.

권한 코드 흐름의 참여자는 다음과 같습니다.

  • 브라우저: NestJS와 Google 사이의 리다이렉트를 따라가며 코드와 state를 운반합니다.
  • NestJS OAuth 클라이언트: 트랜잭션을 시작하고, 콜백을 검증하며, 코드를 서버에서 토큰으로 교환합니다.
  • Google 권한 서버: 사용자를 인증하고 동의를 받은 뒤 일회용 권한 코드를 발급합니다.
  • Google 자원 서버: provider access token을 검증하고 UserInfo 프로필을 반환합니다.

브라우저에 Google client secret을 보내거나, 브라우저가 token endpoint를 직접 호출하게 만들지 않습니다. 서버용 웹 클라이언트의 secret은 서버 설정에만 둡니다.


Google OAuth 클라이언트 등록

Google Cloud Console에서 애플리케이션 유형을 웹 애플리케이션으로 만들고, 승인된 리디렉션 URI에 다음 개발용 콜백을 등록합니다.

http://localhost:3000/auth/google/callback

등록 값과 GOOGLE_CALLBACK_URL은 scheme, host, port, path, 대소문자, 마지막 슬래시까지 같아야 합니다. 운영 환경에서는 HTTPS 주소를 사용하고 wildcard나 열린 리다이렉트를 두지 않습니다. 이 서버 리다이렉트 흐름만 사용한다면 “승인된 자바스크립트 원본”은 필요하지 않습니다. Google의 브라우저 JavaScript SDK를 별도로 사용할 때만 해당 origin을 등록합니다.

필요한 패키지를 설치합니다.

npm install @nestjs/passport passport passport-google-oauth20 express-session
npm install --save-dev @types/passport-google-oauth20 @types/express-session

환경 변수에는 provider 설정과 두 종류의 리다이렉트 목적을 분리해 둡니다.

.env
GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

OAUTH_STATE_SECRET=충분히_길고_무작위인_별도_비밀
FRONTEND_AFTER_LOGIN_URL=http://localhost:5173/login/complete

GOOGLE_CALLBACK_URL은 Google이 돌아오는 OAuth endpoint이고, FRONTEND_AFTER_LOGIN_URL은 로컬 로그인이 끝난 뒤 이동할 고정된 화면입니다. 후자를 요청의 임의 returnTo 값으로 그대로 바꾸지 말고, 허용 목록에서 선택합니다.


state와 PKCE 트랜잭션 저장소 구성

state는 콜백을 로그인 시작 요청에 묶어 CSRF와 로그인 응답 주입을 막습니다. PKCE는 일회용 code_verifier를 가진 클라이언트만 권한 코드를 교환하게 합니다. 현재 passport-oauth2에서 state: truepkce: true를 사용하면 상태 값과 S256 verifier를 req.session에 보관하므로, Passport middleware보다 먼저 짧은 서버 측 트랜잭션 세션을 구성해야 합니다.

src/main.ts
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import session from 'express-session';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = app.get(ConfigService);
  const production = config.get<string>('NODE_ENV') === 'production';

  app.use(
    session({
      name: 'oauth_tx',
      secret: config.getOrThrow<string>('OAUTH_STATE_SECRET'),
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        sameSite: 'lax',
        secure: production,
        maxAge: 10 * 60 * 1000,
      },
    }),
  );

  await app.listen(3000);
}

void bootstrap();

기본 MemoryStore는 개발 확인용입니다. 여러 인스턴스가 실행되는 운영 환경에서는 만료와 원자적 삭제를 지원하는 공유 세션 저장소를 연결하고, HTTPS를 종료하는 프록시 뒤에서는 신뢰할 프록시 설정도 함께 맞춥니다.

oauth_tx 쿠키는 잠깐 유지되는 OAuth 트랜잭션 저장소입니다. 뒤에서 만드는 우리 서비스의 로그인 세션과 목적과 수명이 다릅니다.


Google 전략은 provider 정체성만 정규화한다

전략은 Google endpoint와 통신하고 provider 프로필을 작은 내부 형태로 바꿉니다. 계정 생성·연결과 우리 서비스 세션 발급은 AuthService의 책임으로 남깁니다.

src/auth/strategies/google.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';

export interface GoogleIdentity {
  provider: 'google';
  subject: string;
  email: string | null;
  emailVerified: boolean;
  displayName: string;
  picture: string | null;
}

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(config: ConfigService) {
    super({
      clientID: config.getOrThrow<string>('GOOGLE_CLIENT_ID'),
      clientSecret: config.getOrThrow<string>('GOOGLE_CLIENT_SECRET'),
      callbackURL: config.getOrThrow<string>('GOOGLE_CALLBACK_URL'),
      scope: ['email', 'profile'],
      state: true,
      pkce: true,
    });
  }

  validate(
    _providerAccessToken: string,
    _providerRefreshToken: string,
    profile: Profile,
  ): GoogleIdentity {
    if (profile.provider !== 'google' || !profile.id) {
      throw new UnauthorizedException('Invalid Google profile');
    }

    const verifiedEmail =
      profile.emails?.find((candidate) => candidate.verified === true)?.value ??
      null;

    return {
      provider: 'google',
      subject: profile.id,
      email: verifiedEmail,
      emailVerified: verifiedEmail !== null,
      displayName: profile.displayName ?? '',
      picture: profile.photos?.[0]?.value ?? null,
    };
  }
}

이 전략에서 profile.id는 Google UserInfo의 안정적인 subject 식별자에 해당합니다. 이메일은 바뀔 수 있으므로 기본키로 사용하지 않습니다. 또한 단순 로그인만 필요하므로 provider access token과 refresh token은 req.user에 넣거나 로그에 출력하지 않고 버립니다.

AuthModule에는 전략을 provider로 등록합니다. 여기서 session: false는 Passport가 사용자를 직렬화하는 로그인 세션을 사용하지 않는다는 뜻입니다. 앞서 만든 Express 세션은 OAuth state와 PKCE verifier를 위한 별도 저장소이므로 여전히 필요합니다.

src/auth/auth.module.ts (관련 부분)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { GoogleStrategy } from './strategies/google.strategy';

@Module({
  imports: [
    ConfigModule,
    PassportModule.register({ session: false }),
  ],
  controllers: [AuthController],
  providers: [AuthService, GoogleStrategy],
})
export class AuthModule {}

기존 로컬·JWT 전략과 사용자 저장소 provider가 있다면 같은 배열에 유지합니다.


콜백에서 로컬 계정과 세션을 만든다

같은 AuthGuard('google')가 시작 route에서는 Google로 리다이렉트하고, 콜백 route에서는 state를 검증한 뒤 코드를 서버 측에서 교환하고 UserInfo를 조회합니다. 전략이 반환한 GoogleIdentity가 콜백 요청의 req.user가 됩니다.

src/auth/auth.controller.ts (Google 관련 부분)
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthGuard } from '@nestjs/passport';
import type { Request, Response } from 'express';
import { AuthService } from './auth.service';
import type { GoogleIdentity } from './strategies/google.strategy';

type GoogleCallbackRequest = Request & { user: GoogleIdentity };

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

  @Get('google')
  @UseGuards(AuthGuard('google'))
  googleLogin(): void {
    // Guard가 권한 요청 URL로 리다이렉트합니다.
  }

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleCallback(
    @Req() req: GoogleCallbackRequest,
    @Res() res: Response,
  ): Promise<void> {
    const user = await this.authService.completeGoogleLogin(req.user);
    const session = await this.authService.createBrowserSession(user.id);
    const production = this.config.get<string>('NODE_ENV') === 'production';

    res.cookie('sid', session.token, {
      httpOnly: true,
      sameSite: 'lax',
      secure: production,
      path: '/',
      maxAge: 8 * 60 * 60 * 1000,
    });

    res.redirect(
      303,
      this.config.getOrThrow<string>('FRONTEND_AFTER_LOGIN_URL'),
    );
  }
}

createBrowserSession()은 암호학적으로 무작위인 opaque token을 한 번 반환하고, 저장소에는 그 token의 해시와 사용자 ID, 유휴·최대 만료 시각을 보관하는 메서드라고 가정합니다. 브라우저 JavaScript가 읽을 수 없는 HttpOnly 쿠키로 전달했으므로 프론트엔드는 고정된 완료 화면에서 /auth/me를 호출해 세션 상태를 확인하면 됩니다.

세션 쿠키를 사용하는 상태 변경 요청에는 SameSite 설정만 믿지 말고 신뢰하는 Origin 확인이나 CSRF token을 적용합니다. 프론트엔드와 API origin이 다르면 정확한 CORS origin, credentials 설정, cookie 정책을 함께 맞춥니다.

계정 연결 정책

completeGoogleLogin()은 다음 순서를 DB 트랜잭션으로 구현합니다.

  1. (provider, subject)에 고유 제약을 두고 이미 연결된 로컬 사용자를 찾습니다.
  2. 연결이 있으면 그 사용자를 로그인시킵니다. 정지·삭제 상태 같은 로컬 정책은 여기서 다시 확인합니다.
  3. 연결은 없지만 같은 이메일의 로컬 계정이 있으면 자동 병합하지 않습니다. 사용자가 기존 방식으로 다시 인증한 뒤 명시적으로 연결하도록 요구합니다.
  4. 충돌이 없고 가입 정책을 만족할 때만 로컬 사용자와 provider 연결 레코드를 원자적으로 생성합니다.

Google이 이메일을 검증했다는 사실만으로 기존 로컬 계정의 소유권까지 증명되지는 않습니다. 안정적인 provider subject를 무시하고 이메일로 자동 연결하면 계정 탈취나 잘못된 병합 경계가 생깁니다.


OIDC로 구현할 때 추가되는 검증

OIDC 구현으로 바꾼다면 단순히 openid scope만 추가해서는 충분하지 않습니다. 검증을 직접 조립하기보다 discovery와 JWKS 검증을 지원하는 검증된 OIDC 클라이언트나 전략을 사용하고, 다음 조건을 모두 실패 폐쇄 방식으로 확인합니다.

  • 권한 요청마다 예측 불가능한 nonce를 만들고 서버의 OAuth 트랜잭션에 저장합니다.
  • ID token의 서명을 provider discovery/JWKS로 검증하고 허용한 알고리즘만 받습니다.
  • iss가 기대한 Google issuer인지, aud가 우리 client ID를 포함하는지 확인합니다. audience가 여러 개라면 azp도 확인합니다.
  • exp와 필요한 시간 claim을 확인하고, ID token의 nonce가 저장한 값과 정확히 같은지 확인한 뒤 한 번만 사용합니다.
  • 내부 연결 키는 이메일이 아니라 검증된 (iss, sub)를 사용합니다.

서명 확인 없이 ID token payload를 decode한 결과를 신뢰하거나, OAuth access token을 우리 서비스의 ID token처럼 사용하면 안 됩니다. 자세한 검증 조건은 OpenID Connect CoreGoogle OpenID Connect 문서를 기준으로 삼습니다.


provider token과 로컬 로그인 수명 주기를 분리하기

이 예제는 Google API를 계속 호출하지 않으므로 provider token을 저장하지 않습니다. 캘린더나 Drive 같은 Google API 위임이 실제로 필요할 때만 최소 scope를 요청하고 다음을 별도로 설계합니다.

  • offline access가 필요한 이유를 명확히 하고, Google refresh token이 매번 다시 발급된다고 가정하지 않습니다.
  • provider access/refresh token은 브라우저나 req.user가 아니라 서버의 암호화된 저장소에 scope·만료·provider 계정과 함께 보관합니다.
  • 갱신 응답에 새 refresh token이 없으면 기존 값을 실수로 지우지 않고, invalid_grant 같은 폐기 신호에서는 재동의를 요구합니다.
  • 연결 해제 시 Google grant를 revoke하고 저장된 provider token을 삭제합니다.

우리 서비스의 sid 세션은 Google token과 독립적으로 유휴 만료, 최대 수명, 회전, 로그아웃, 비밀번호·권한 변경 시 폐기를 적용합니다. API가 JWT를 사용한다면 로그인 완료 후 인증된 HTTPS 응답 body나 HttpOnly cookie로 발급하고 access/refresh 정책을 별도로 적용합니다. 앱 JWT를 리다이렉트 query에 넣거나 localStorage로 넘기지 않습니다.

소셜 로그인에서 리다이렉트 트랜잭션, provider 정체성, 로컬 계정 연결, 로컬 로그인 자격 증명, token 수명 주기를 분리하고 각 경계의 필수 검증과 누락 위험을 비교한 표

Nest · Social Login Boundaries

같은 “토큰”도 발급자와 사용처가 다르다

권한 코드, Google token, OIDC ID token, 로컬 세션은 서로 대신할 수 없습니다. 각 값을 발급한 경계에서 검증하고 필요한 다음 자격 증명으로만 전환합니다.

소셜 로그인 경계별 필수 판정과 실패 시 위험
경계 필수 판정과 저장 섞거나 생략했을 때
콜백 등록 값과 정확히 같은 redirect_uri, 일회용·브라우저 세션 결합 state, PKCE S256을 사용합니다. OIDC면 요청별 nonce도 저장합니다. CSRF, 로그인 응답 주입, 권한 코드 가로채기, 열린 리다이렉트로 이어질 수 있습니다.
식별 OAuth UserInfo 연결 키는 (google, sub)입니다. 고정된 Google endpoint에서 프로필을 얻습니다. OIDC는 ID token의 서명·알고리즘·iss·aud·exp·nonce를 검증합니다. 이메일이나 서명 확인 없이 decode한 claim을 신뢰하면 다른 사용자를 잘못 식별할 수 있습니다.
연결 (provider, subject)에 고유 제약을 두고 트랜잭션으로 연결합니다. 같은 이메일의 기존 계정은 재인증과 명시적 동의 뒤에만 연결합니다. 이메일 자동 병합은 계정 탈취나 서로 다른 계정의 영구 오연결을 만들 수 있습니다.
세션 고정된 완료 URL로 이동하고 opaque 세션을 HttpOnly·Secure·SameSite cookie로 전달합니다. 상태 변경에는 별도 CSRF 방어를 적용합니다. 앱 JWT를 query나 localStorage에 넣으면 기록·referrer·브라우저 확장과 XSS를 통해 노출 범위가 커집니다.
폐기 Google API가 필요할 때만 provider token을 암호화 저장하고 scope·만료·grant 폐기를 관리합니다. 로컬 세션은 유휴·최대 만료, 회전, 로그아웃 폐기를 독립 적용합니다. Google refresh token과 앱 refresh token을 같은 것으로 다루면 한쪽의 만료·연결 해제·보안 이벤트가 다른 쪽에 올바르게 반영되지 않습니다.

리다이렉트 트랜잭션

정확한 redirect_uri, 브라우저 세션에 묶인 일회용 state, PKCE S256을 사용합니다. OIDC면 nonce도 같은 트랜잭션에 저장합니다.

생략하면 CSRF, 응답 주입, code 가로채기, 열린 리다이렉트 경계가 생깁니다.

provider 정체성

OAuth UserInfo는 (google, sub)를 사용합니다. OIDC는 ID token 서명과 허용 알고리즘, iss, aud, exp, nonce를 검증합니다.

이메일이나 검증하지 않은 token payload는 기본 식별자가 아닙니다.

로컬 계정 연결

(provider, subject) 고유 제약과 DB 트랜잭션을 사용합니다. 이메일 충돌은 기존 계정 재인증과 명시적 동의 뒤에만 연결합니다.

검증된 이메일이어도 자동 병합 근거로는 부족합니다.

로컬 로그인 자격 증명

고정된 완료 URL과 HttpOnly·Secure·SameSite 로컬 세션 cookie를 사용하고, 상태 변경 요청에는 CSRF 방어를 적용합니다.

앱 JWT를 리다이렉트 query나 localStorage로 전달하지 않습니다.

수명·폐기

Google API가 필요할 때만 provider token을 서버에 암호화 저장하고 scope·만료·grant 폐기를 관리합니다. 로컬 세션은 별도로 만료·회전·로그아웃 폐기합니다.

provider refresh token과 앱 refresh token은 발급자도 사용처도 다릅니다.

안전한 소셜 로그인은 “Google 인증 성공” 한 단계가 아니라, 외부 트랜잭션을 검증된 로컬 사용자와 폐기 가능한 로컬 로그인 상태로 좁혀 가는 연속된 경계입니다.


실패 경로와 테스트

성공 화면만 확인하지 말고 보안 경계를 각각 실패시켜 봅니다.

/auth/google에 접속하고 권한 요청 URL에 예측 불가능한 state와 S256 code_challenge가 있는지 확인합니다. 콜백 URL은 등록한 값과 정확히 같아야 합니다.

콜백의 state를 바꾸거나 같은 콜백을 재사용합니다. 요청은 거부되고 로컬 계정과 세션이 만들어지지 않아야 합니다.

정상 로그인 뒤 주소 표시줄에 앱 token이 없는지, 응답의 sid가 HttpOnly인지, 고정된 완료 URL로만 이동하는지 확인합니다. 서버 로그에도 provider token과 전체 프로필을 남기지 않습니다.

기존 로컬 계정과 같은 이메일이지만 아직 연결되지 않은 Google 계정으로 로그인합니다. 자동 병합하지 않고 기존 계정 재인증 절차로 보내야 합니다.

OIDC 변형에서는 서명, iss, aud, exp, nonce 중 하나라도 맞지 않는 ID token을 모두 거부합니다.

로컬 로그아웃과 Google 연결 해제를 따로 시험합니다. 전자는 우리 세션을 폐기하고, 후자는 필요한 경우 provider grant까지 revoke해야 합니다.

리다이렉트 URI exact matching, PKCE, 응답 주입 방어의 최신 기준은 OAuth 2.0 Security Best Current Practice, Google token 교환과 폐기 동작은 Google 웹 서버 OAuth 흐름을 함께 확인합니다.


이제 4장의 인증 흐름은 하나로 이어집니다. 로컬 로그인과 소셜 로그인은 서로 다른 정체성 증명 경로이고, 둘 다 검증된 로컬 사용자 컨텍스트를 만든 뒤 세션 또는 앱 token 경계로 넘어갑니다. RBAC는 그 다음 요청에서 인증된 사용자에게 어떤 작업을 허용할지 판정합니다.