📖

NestJSでのGoogle OAuth実装で、stateパラメータを受け取る方法

に公開

NestJS で Google OAuth を実装する際、認証フローの一環として redirect URI に state パラメータを渡したかったのですが、うまく渡せず困ったため、解決方法を共有します。

state パラメータは CSRF 攻撃を防止するために使用され、認証リクエストとレスポンスを関連付ける役割を果たします。(アプリ側の任意の情報を保持するために使用できます。)

環境

  • Node.js: v22.16.0
  • passport: v0.7.0,
  • passport-google-oauth20: v2.0.0,
  • passport-jwt: v4.0.1,

発生した現象

以下のように、Google OAuth の認証エンドポイントに state パラメータを付与してリクエストを送信しましたが、リダイレクト先のエンドポイントで state パラメータが undefined となり、正常に受け取れませんでした。


  //フロントエンドからこのエンドポイントにアクセス
  @Get('/google')
  @UseGuards(AuthGuard('google'))
  async googleAuth(@Req() _req: any): Promise<void> {}

  // Googleからのリダイレクトを受け取るエンドポイント
  @Get('/google/redirect')
  @UseGuards(GoogleOauthGuard)
  async googleAuthRedirect(
    @Req() req: any,
    @Res({ passthrough: true }) res: Response,
  ): Promise<void> {

  console.log('state:', req.query.state); // ここで state が undefined になっていた

    //省略
  }

解決方法

カスタムガードの作成

NestJS 標準のAuthGuard('google')が Google リダイレクト時に リクエスト毎に異なる state パラメータを設定できなかったため、
AuthGuard('google') を拡張し、state を明示的に Google に渡すカスタムガードを作成し、リクエストのクエリパラメータから state を動的に取得して渡すようにしました。

//NOTE: このコード例はAI生成です

// google-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class GoogleAuthGuard extends AuthGuard("google") {
  getAuthenticateOptions(context) {
    const request = context.switchToHttp().getRequest();
    const state = request.query.state;
    return {
      scope: ["email", "profile", "openid"],
      accessType: "offline",
      state, // ← ここでGoogleへ明示的に渡す
    };
  }
}

// auth.controller.tsで上記Guardを使用
@Get('/google/redirect')
@UseGuards(GoogleAuthGuard)
async googleAuthRedirect(
  @Req() req: any,
  @Res({ passthrough: true }) res: Response,
): Promise<void> {
  // req.user に GoogleUser オブジェクトが入っており、
  // その中に state が含まれている
  console.log('state:', req.user.state);

  // 以降の処理
}

GoogleStrategy の設定変更

また、AuthGuard('google')が呼び出す GoogleStrategy のvalidateメソッドで state パラメータを受け取れるようにしました。

元々 GoogleStrategy のコンストラクタでpassReqToCallbackを設定していなかったため HTTP リクエストオブジェクトを受け取れていませんでした。
passReqToCallback: trueを設定したことでvalidateメソッドの第一引数に HTTP リクエストオブジェクトが渡されるようになったため、そこから state パラメータを取得できるようになりました。

// google.strategy.ts
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
  constructor(private readonly configService: ConfigService) {
    super({
      // ... 他のプロパティ
      passReqToCallback: true, // trueに設定するとvalidateメソッドの第一引数にHTTPリクエストオブジェクトを渡す
    });
  }

  async validate(
    req: any,
    accessToken: string,
    refreshToken: string,
    profile: Profile,
    _done: VerifyCallback
  ): Promise<GoogleUser> {
    const state = req.query.state; // ← ここで state を取得

    const user: GoogleUser = {
      id: GoogleId(id),
      email: GoogleEmail(emails[0].value),
      // ... 他のプロパティ
      state: state || null, // ← state を含める
    };

    return user;
  }
}

以上の対応により、Google OAuth のリダイレクト時に state パラメータを正常に受け取れるようになりました。

まとめ

NestJS で Google OAuth を実装する際、state パラメータを正しく渡すためには、AuthGuard を拡張して state を明示的に渡し、GoogleStrategy の validate メソッドで HTTP リクエストオブジェクトを受け取る設定をする必要がありました。
同じ現象で悩んでいる方の参考になれば幸いです。
何か間違いやお気づきの点がございましたら、コメントいただければと思います。

GitHubで編集を提案

Discussion