NestJS SAML認証 / JWT Cookie連携
Passport-SAMLでSAML認証を実装し、JWTをHttpOnly Cookieに保存してSPAと連携する
NestJS Guard / JWT認証まで終えている前提。
この記事では、企業や学校のシングルサインオン(SSO)でよく使われる SAML認証 をNestJSで実装する。認証後はJWTをHttpOnly Cookieに保存し、SPAからのAPIアクセスに活用するパターンを構築する。
認証フロー
sequenceDiagram
participant B as ブラウザ
participant SP as NestJS (SP)
participant IdP as IdP
B->>SP: GET /auth/saml/login
note over SP: SamlAuthGuard起動<br>SamlStrategyがSAMLリクエスト生成
SP->>B: 302リダイレクト(IdPへ)
B->>IdP: SAMLリクエスト送信
IdP->>B: ログイン画面を表示
B->>IdP: ユーザー名・パスワードを入力
IdP->>B: SAMLアサーション(HTMLフォーム)を返す
B->>SP: POST /auth/saml/callback(SAMLアサーション送信)
note over SP: SamlAuthGuard起動<br>passport-samlが署名・期限を検証<br>SamlStrategy.validate()でreq.userをセット
note over SP: AuthService.createToken()でJWT生成
note over SP: res.cookie()でHttpOnly CookieにJWTをセット
SP->>B: 302リダイレクト(SPAへ)CookieつきSPA->>SP: GET /auth/me(CookieにJWTを含む)
note over SP: JwtCookieAuthGuard起動<br>JwtCookieStrategy がCookieからJWT取得・検証<br>validate()でreq.userをセット
SP->>B: ユーザー情報を返す
NestJSでの責務分担
実装に入る前に、今回登場する主要クラスの責務を整理しておく。
| クラス | 役割 |
|---|---|
| Controller | HTTPリクエストの入口 |
| Guard | 認証処理の起動・認可判定 |
| Strategy | 認証ロジックの実装 |
| Service | JWT生成やユーザー関連処理 |
| Passport | GuardとStrategyを橋渡しする仕組み |
認証処理を初めて読むと、Controllerに認証ロジックが見当たらず戸惑うことがある。
例えば以下のコード。
@Get("saml/login")
@UseGuards(SamlAuthGuard)
samlLogin() {}メソッド本体は空だが、実際には SamlAuthGuard が認証処理を開始する。
つまり、
Controller
↓
Guard
↓
Strategy
↓
req.user
↓
Serviceという流れで処理が進む。
以降の実装では、この責務分担を意識しながら読むと理解しやすい。
この記事でやること
- テスト用SAML IdPをDockerで起動する
passport-samlを使ってSamlStrategyを実装するSamlAuthGuardでSAML認証フローを制御するAuthServiceでJWTを生成する- JWTをHttpOnly Cookieに保存する
JwtCookieStrategy/JwtCookieAuthGuardでCookieからJWTを検証する/auth/meでSPAからユーザー情報を取得できるようにする
ディレクトリ構成
api-sample/
├─ .env
└─ src/
├─ main.ts # cookie-parser / CORS を追加
├─ app.module.ts # AuthModule を追加
└─ auth/
├─ auth.controller.ts # /auth/saml/login, /auth/saml/callback, /auth/me
├─ auth.module.ts
├─ auth.service.ts # JWT生成ロジック
├─ saml.strategy.ts # SAMLアサーション検証 / req.user の形を決める
├─ saml-auth.guard.ts # SAMLリダイレクト / コールバック用 Guard
├─ jwt-cookie.strategy.ts # Cookie から JWT を取り出して検証する Strategy
└─ jwt-cookie.guard.ts # JWT Cookie 検証を使う Guard触るファイル一覧
api-sample/
├─ .env # SAML / JWT / SPA の環境変数を追加
└─ src/
├─ main.ts # 変更: cookieParser / enableCors を追加
├─ app.module.ts # 変更: AuthModule を追加
└─ auth/ # 新規作成
├─ auth.controller.ts
├─ auth.module.ts
├─ auth.service.ts
├─ saml.strategy.ts
├─ saml-auth.guard.ts
├─ jwt-cookie.strategy.ts
└─ jwt-cookie.guard.ts1. パッケージを追加する
pnpmの場合。
pnpm add @nestjs/passport passport passport-saml @nestjs/jwt passport-jwt cookie-parser
pnpm add -D @types/passport-saml @types/passport-jwt @types/cookie-parsernpmの場合。
npm install @nestjs/passport passport passport-saml @nestjs/jwt passport-jwt cookie-parser
npm install -D @types/passport-saml @types/passport-jwt @types/cookie-parser2. 環境変数を追加する
編集するファイルは api-sample/.env。
# JWT
JWT_SECRET="dev-secret-change-me"
JWT_EXPIRES_IN="1h"
# SAML(接続先IdPに合わせて変更する)
SAML_ENTRY_POINT="http://localhost:8080/simplesaml/saml2/idp/SSOService.php"
SAML_ISSUER="urn:my-nestjs-app"
SAML_CALLBACK_URL="http://localhost:3000/auth/saml/callback"
SAML_CERT="MIIBkTCB+wIJAJC..."
# SPA
SPA_URL="http://localhost:5173"SAML_ENTRY_POINT はIdPのSSOエンドポイントURL。SAML_ISSUER はSPのエンティティIDで、IdP側の設定と一致している必要がある。SAML_CERT はIdPの公開鍵証明書(Base64文字列)で、後述の手順で取得する。
3. テスト用IdPをDockerで起動する
実際のSAML認証を動かすにはIdPが必要。学習用にはDocker版のSimpleSAMLphpが手軽に使える。
docker run -d \
--name saml-idp \
-p 8080:8080 \
-e SIMPLESAMLPHP_SP_ENTITY_ID=urn:my-nestjs-app \
-e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:3000/auth/saml/callback \
kristophjunge/test-saml-idp起動後、http://localhost:8080/simplesaml にアクセスして動作を確認できる。
SAML_CERT はIdPのメタデータから取得する。
curl -s http://localhost:8080/simplesaml/saml2/idp/metadata.php \
| grep -oP '(?<=<ds:X509Certificate>)[^<]+'出力された文字列をそのまま .env の SAML_CERT に入れる。
テストユーザーはIdPのデフォルト設定に含まれている。
| ユーザー名 | パスワード |
|---|---|
| user1 | user1pass |
| user2 | user2pass |
4. SamlStrategy を作る
新規作成するファイルは api-sample/src/auth/saml.strategy.ts。
SamlStrategy は「SAMLのアサーションをどう検証し、req.user に何を入れるか」をPassportに教える設定クラス。
super()の中が SAML接続設定(どのIdPへ、どの証明書で検証するか)validate()が アサーション検証後の処理(プロファイルをアプリで使うユーザー型に変換する)
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-saml";
// SPAやJWTに渡すユーザー情報の型
export type SamlUser = {
nameId: string;
email: string;
displayName: string;
};
// SAMLアサーションのプロファイル型(IdPにより属性名が異なる)
type SamlProfile = {
nameID?: string;
email?: string;
displayName?: string;
[key: string]: string | string[] | undefined;
};
@Injectable()
export class SamlStrategy extends PassportStrategy(Strategy, "saml") {
constructor() {
super({
// IdPのSSOエンドポイントURL
entryPoint: process.env.SAML_ENTRY_POINT,
// このSPを識別するエンティティID(IdP側の設定と合わせる)
issuer: process.env.SAML_ISSUER,
// IdPがSAMLアサーションをPOSTするURL
callbackUrl: process.env.SAML_CALLBACK_URL,
// IdPの公開鍵証明書(署名の検証に使う)
cert: process.env.SAML_CERT ?? "",
});
}
// SAMLアサーションの署名と内容が正しいと検証された後に呼ばれる
// 戻り値がそのまま req.user にセットされる
validate(profile: SamlProfile): SamlUser {
return {
// nameIDはIdPがユーザーを一意に識別する値
nameId: profile.nameID ?? "",
// 属性名はIdPにより異なる。SimpleSAMLphpは "email"、Azure ADは長いURIになる
email: (profile.email as string) ?? "",
// 表示名。IdPの設定に合わせて調整する
displayName: (profile.displayName as string) ?? "",
};
}
}profile の属性名はIdPによって異なる。Azure ADなら http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress、SimpleSAMLphpなら email のように、接続するIdPのドキュメントを確認して属性名を調整する。
5. AuthService を作る
新規作成するファイルは api-sample/src/auth/auth.service.ts。
AuthService はSAMLで認証されたユーザー情報を受け取り、JWTを生成して返す役割に徹する。JWT生成の詳細は JwtService に委譲し、AuthService はその入出力の橋渡しをする。
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { SamlUser } from "./saml.strategy";
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
// SAMLで認証されたユーザー情報からJWT文字列を生成して返す
// 呼ばれるタイミング: samlCallback() で req.user が確定した直後
async createToken(user: SamlUser): Promise<string> {
return this.jwtService.signAsync({
sub: user.nameId, // JWT の subject はユーザーの一意識別子
email: user.email,
displayName: user.displayName,
});
}
}JWTに何を詰めるかという知識はServiceに集約する。ControllerにJWT生成ロジックを書くと、ControllerがHTTPの入口とJWT仕様の両方を知ることになり責務が混ざる。
6. SamlAuthGuard を作る
新規作成するファイルは api-sample/src/auth/saml-auth.guard.ts。
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class SamlAuthGuard extends AuthGuard("saml") {}AuthGuard("saml") の "saml" は SamlStrategy の第2引数で指定した戦略名と揃える。
SamlAuthGuard は同じクラスながら、貼り付けるエンドポイントによって動作が異なる。
| 場面 | エンドポイント | Guard の動作 |
|---|---|---|
| ログイン開始 | GET /auth/saml/login | SAMLリクエストを生成してIdPへ302リダイレクト |
| コールバック | POST /auth/saml/callback | SAMLアサーションを検証して req.user をセット |
どちらも同じ SamlAuthGuard を使っているが、Passportがリクエストのコンテキスト(GETかPOSTか・SAMLアサーションが含まれているか)を判断して処理を自動で切り替える。
7. JwtCookieStrategy を作る
新規作成するファイルは api-sample/src/auth/jwt-cookie.strategy.ts。
前の記事の JwtStrategy は Authorization: Bearer ヘッダーからJWTを取り出した。SPA + Cookie連携では、CookieからJWTを取り出す専用のStrategyが必要になる。
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { Request } from "express";
type JwtPayload = {
sub: string;
email: string;
displayName: string;
};
@Injectable()
export class JwtCookieStrategy extends PassportStrategy(Strategy, "jwt-cookie") {
constructor() {
super({
// Cookie の "access_token" キーから JWT を取り出す
jwtFromRequest: ExtractJwt.fromExtractors([
(req: Request) => req?.cookies?.["access_token"] ?? null,
]),
// 期限切れの JWT は拒否する
ignoreExpiration: false,
// AuthService でJWT生成に使った秘密鍵と同じ値で検証する
secretOrKey: process.env.JWT_SECRET ?? "dev-secret-change-me",
});
}
// 署名と期限の検証が通った後に呼ばれる
// 戻り値が req.user にセットされる
validate(payload: JwtPayload) {
return {
userId: payload.sub,
email: payload.email,
displayName: payload.displayName,
};
}
}ExtractJwt.fromExtractors に関数の配列を渡すと、先頭から順に試してJWTが取れた時点で検証に進む。Cookieが見つからない場合は null を返すと認証失敗として扱われる。
8. JwtCookieAuthGuard を作る
新規作成するファイルは api-sample/src/auth/jwt-cookie.guard.ts。
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtCookieAuthGuard extends AuthGuard("jwt-cookie") {}"jwt-cookie" は JwtCookieStrategy の第2引数で指定した戦略名と揃える。
9. AuthController を作る
新規作成するファイルは api-sample/src/auth/auth.controller.ts。
AuthController は認証フローのHTTP入口。3つのエンドポイントを提供する。
| エンドポイント | 役割 |
|---|---|
GET /auth/saml/login | SAMLログイン開始。SamlAuthGuard がIdPへリダイレクト |
POST /auth/saml/callback | IdPからのSAMLアサーションを受信。JWT生成・Cookie保存・SPAへリダイレクト |
GET /auth/me | JWTが有効なときのみユーザー情報を返す |
import {
Controller,
Get,
Post,
Req,
Res,
UseGuards,
} from "@nestjs/common";
import { Request, Response } from "express";
import { AuthService } from "./auth.service";
import { SamlAuthGuard } from "./saml-auth.guard";
import { JwtCookieAuthGuard } from "./jwt-cookie.guard";
import { SamlUser } from "./saml.strategy";
type AuthenticatedRequest = Request & {
user: { userId: string; email: string; displayName: string };
};
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
// SAMLログイン開始
// SamlAuthGuardがSAMLリクエストを生成してIdPへリダイレクトするため
// このメソッド本体には到達しない
@UseGuards(SamlAuthGuard)
@Get("saml/login")
samlLogin(): void {
// Guard がIdPへリダイレクトするのでここには来ない
}
// SAMLコールバック
// IdPが認証後にSAMLアサーションをPOSTしてくる
@UseGuards(SamlAuthGuard)
@Post("saml/callback")
async samlCallback(
@Req() req: Request & { user: SamlUser },
@Res() res: Response,
): Promise<void> {
// この時点で SamlAuthGuard がアサーションを検証済みで req.user に SamlUser が入っている
// JWT生成タイミング: ここで AuthService に委譲する
const token = await this.authService.createToken(req.user);
// Cookie保存タイミング: JWTをHttpOnly CookieにセットしてSPAに渡す
res.cookie("access_token", token, {
httpOnly: true, // JavaScriptからアクセス不可
secure: process.env.NODE_ENV === "production", // 本番ではHTTPS必須
sameSite: "lax", // CSRF対策
maxAge: 60 * 60 * 1000, // 1時間(ミリ秒)
});
// Cookieをセットしたうえで SPA へリダイレクト
res.redirect(process.env.SPA_URL ?? "http://localhost:5173");
}
// SPA向けのユーザー情報取得API
// JwtCookieAuthGuard が Cookie から JWT を取り出して検証する
@UseGuards(JwtCookieAuthGuard)
@Get("me")
me(@Req() req: AuthenticatedRequest) {
// JwtCookieStrategy.validate() の戻り値が req.user に入っている
return req.user;
}
}samlCallback で @Res() を使っているのは、res.cookie() と res.redirect() がExpressの低レベルAPIに依存するため。@Res() を付けると NestJS の自動レスポンス処理が無効になるので、メソッド内で明示的にレスポンスを完了させる必要がある。
10. AuthModule を作る
新規作成するファイルは api-sample/src/auth/auth.module.ts。
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { SamlStrategy } from "./saml.strategy";
import { JwtCookieStrategy } from "./jwt-cookie.strategy";
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET ?? "dev-secret-change-me",
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN ?? "1h" },
}),
],
controllers: [AuthController],
providers: [AuthService, SamlStrategy, JwtCookieStrategy],
})
export class AuthModule {}SamlStrategy と JwtCookieStrategy はどちらも providers に登録する。DIシステムがインスタンス化してPassportに戦略として登録する流れはフレームワーク側が自動でやってくれる。
11. main.ts に cookie-parser と CORS を追加する
編集するファイルは api-sample/src/main.ts。
cookie-parser を追加しないと req.cookies が undefined になりJWT取得に失敗する。CORS設定の credentials: true がないとブラウザがCookieを送受信しない。
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as cookieParser from "cookie-parser";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// req.cookies を使えるようにする
app.use(cookieParser());
// SPA からのクロスオリジンリクエストで Cookie を送受信できるようにする
app.enableCors({
origin: process.env.SPA_URL ?? "http://localhost:5173",
credentials: true,
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();12. AppModule に AuthModule を追加する
編集するファイルは api-sample/src/app.module.ts。
import { Module } from "@nestjs/common";
import { AuthModule } from "./auth/auth.module"; // 追加
// ... 既存のimport
@Module({
imports: [
AuthModule, // 追加
// ... 既存のModule
],
})
export class AppModule {}13. SPAとの連携
SPAからAPIを呼ぶ場合、fetch や axios に credentials: "include" を指定してCookieをリクエストに含める。
// SPA 側(TypeScript / React など)
// ユーザー情報の取得
const res = await fetch("http://localhost:3000/auth/me", {
credentials: "include", // Cookie を Cross-Origin リクエストに含める
});
const user = await res.json();
// → { userId: "...", email: "user1@example.com", displayName: "User One" }// 認証が必要な任意のAPIを叩く場合も同様
const res = await fetch("http://localhost:3000/some-protected-endpoint", {
credentials: "include",
});credentials: "include" は fetch のデフォルトが "same-origin"(同一オリジンのみCookieを送る)のため明示的に指定が必要。axios の場合は withCredentials: true が対応する設定。
14. 動作確認
アプリを起動する。
pnpm start:devブラウザで http://localhost:3000/auth/saml/login にアクセスすると、SimpleSAMLphpのログイン画面へリダイレクトされる。
user1 / user1pass でログインする。
ログイン成功後、http://localhost:5173(SPA_URL)へリダイレクトされる。ブラウザの開発者ツール → Application → Cookies で access_token が保存されていることを確認する。
次のコマンドでブラウザから取得したCookieを使って /auth/me を確認できる。
curl -s http://localhost:3000/auth/me \
--cookie "access_token=<ブラウザのDevToolsからコピーしたJWT>"レスポンス例。
{
"userId": "user1@example.com",
"email": "user1@example.com",
"displayName": "User One"
}これは JwtCookieStrategy.validate() が返した値が req.user に入り、GET /auth/me でそのまま返っているため。
Cookieなしで叩くと401になる。
curl -i http://localhost:3000/auth/meHTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8{
"statusCode": 401,
"message": "Unauthorized"
}コードリーディングのポイント
実務でSAML認証を調査するときは、Controllerから読むよりもアプリケーションの起動点から追う方が全体像を把握しやすい。
main.ts
↓
AppModule
↓
AuthModule
↓
AuthController
↓
SamlAuthGuard
↓
SamlStrategy
↓
req.user
↓
AuthService特に認証系は「Controllerに処理が書かれていない」ことが珍しくない。
そのため、以下の順番で追うと迷いにくい。
- Controller(入口)
- Guard(どの認証戦略を使うか)
- Strategy(認証ロジック本体)
- req.user(認証結果)
- Service(JWT生成やユーザー処理)
- Cookie設定箇所
- 認証済みAPI
実案件でも同じ見方ができるため、学習用サンプルを読む際にも意識してみてほしい。
責務の整理
各クラスの担当をまとめる。
SamlStrategy の役割
super()にSAML接続設定を渡す(どのIdPへ・どの証明書で検証するか)validate()でアサーション検証後のプロファイルを受け取り、アプリで使うSamlUser型に変換するvalidate()の戻り値がreq.userにセットされる
SAMLのXML解析・署名検証・リダイレクトURLの生成などの複雑な処理は passport-saml が担う。StrategyはIdP設定とユーザーマッピングだけを書けばよい。
SamlAuthGuard の役割
GET /auth/saml/loginに付けることで、PassportにSAMLリクエストの生成とIdPへのリダイレクトをさせるPOST /auth/saml/callbackに付けることで、PassportにSAMLアサーションの検証とreq.userのセットをさせる- Guardは「このエンドポイントでこの戦略を使う」という宣言。検証ロジック本体はStrategyに委譲している
AuthService の役割
- SAMLで認証されたユーザー情報(
SamlUser)を受け取り、JWTを生成して返す - JWTのペイロードに何を含めるかという知識をここに集約する
- Controllerからは「JWT文字列を返すService」としてのみ扱う
AuthController の役割
- HTTPリクエストの入口(URL・HTTPメソッド)を定義する
- Guardを宣言してどの認証戦略を適用するかを指定する
samlCallbackではJWT生成・Cookie保存・リダイレクトの流れを指示するだけで、各処理の詳細はServiceやresに任せる
req.user がセットされる流れ
【SAMLログイン時】
POST /auth/saml/callback(SAMLアサーション付き)
→ SamlAuthGuard が起動
→ passport-saml が XML署名・期限を検証
→ SamlStrategy.validate(profile) が呼ばれる
→ validate() の戻り値(SamlUser)が req.user にセットされる
→ AuthController.samlCallback(req) に到達(req.user が使える状態)
【APIアクセス時】
GET /auth/me(Cookie に JWT を含む)
→ JwtCookieAuthGuard が起動
→ JwtCookieStrategy が req.cookies["access_token"] から JWT を取り出す
→ passport-jwt が署名・期限を検証
→ JwtCookieStrategy.validate(payload) が呼ばれる
→ validate() の戻り値({ userId, email, displayName })が req.user にセットされる
→ AuthController.me(req) に到達(req.user が使える状態)JWT生成タイミングとCookie保存タイミング
POST /auth/saml/callback
→ SamlAuthGuard が req.user をセット(SAMLアサーションの検証完了)
→ AuthController.samlCallback() に入る
→ AuthService.createToken(req.user) でJWTを生成 ← JWT生成タイミング
→ res.cookie("access_token", token, { httpOnly: true, ... }) ← Cookie保存タイミング
→ res.redirect(SPA_URL) でSPAへよくあるエラー
Error: SAML certificate is required
.env の SAML_CERT が空になっている。DockerのIdPから証明書を取得して設定する。
curl -s http://localhost:8080/simplesaml/saml2/idp/metadata.php \
| grep -oP '(?<=<ds:X509Certificate>)[^<]+'InvalidSignatureError
SAML_CERT の値が正しくない。BEGIN CERTIFICATE / END CERTIFICATE ヘッダー付きのPEM形式か、ヘッダーなしのBase64文字列かはIdPにより異なる。passport-saml は通常ヘッダーなしのBase64を期待する。
req.cookies is undefined
main.ts に app.use(cookieParser()) が追加されていない。または import * as cookieParser from "cookie-parser" が抜けている。
Cookie がブラウザに保存されない / 送信されない
CORS設定で credentials: true が漏れている可能性がある。SPA側も credentials: "include" が必要。どちらか片方だけでは機能しない。
/auth/saml/callback で404になる
AppModule に AuthModule が追加されているか確認する。また @Post("saml/callback") が @Controller("auth") の中にあることを確認する。
IdP にリダイレクトされない / エンティティIDが一致しない
SAML_ENTRY_POINT / SAML_ISSUER / SAML_CALLBACK_URL がIdP側の設定と一致しているか確認する。Dockerで起動したIdPの場合、SIMPLESAMLPHP_SP_ENTITY_ID と SAML_ISSUER が一致している必要がある。
実装タスク(チェックリスト)
-
passport-saml/cookie-parserなどのパッケージをインストール -
.envにSAML_*/JWT_SECRET/SPA_URLを追加 - DockerでテストIdPを起動し、
SAML_CERTを取得して.envに入れる -
src/auth/saml.strategy.tsを作成(SamlStrategy/SamlUser) -
src/auth/auth.service.tsを作成(AuthService.createToken) -
src/auth/saml-auth.guard.tsを作成(SamlAuthGuard) -
src/auth/jwt-cookie.strategy.tsを作成(JwtCookieStrategy) -
src/auth/jwt-cookie.guard.tsを作成(JwtCookieAuthGuard) -
src/auth/auth.controller.tsを作成(3エンドポイント) -
src/auth/auth.module.tsを作成 -
src/main.tsにcookieParser()とenableCors()を追加 -
src/app.module.tsにAuthModuleを追加 - ブラウザで
GET /auth/saml/login→ SimpleSAMLphpログイン → Cookie保存を確認 -
GET /auth/meでユーザー情報が返ることを確認 -
GET /auth/me(Cookieなし)で401になることを確認