Keycloak を使って SAML 認証する SSO の仕組みを作る

Keycloak という SSO を実現するための OSS がある。RedHat SSO という製品の元になっているモノらしい。

OIDC や SAML 認証によるシングルサインオンを比較的簡単に実装できる。今回は Docker で試してみた。

$ docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.0.7 start-dev

公式ガイドにある Docker コマンドそのまんま。http://localhost:8080 にアクセスすると Keycloak の管理画面が立ち上がるので、SAML 用のクライアントを作ったりする。

んで、コレを利用する Web アプリについてだが、今回は Express.js でバックエンドを作ってみた。以下の例では http://localhost:3000 で起動しているのが、Keycloak とやり取りするバックエンドサーバ。Node.js では「Passport-Saml」というパッケージで SAML 認証に対応させられる。このへんは使用している言語・フレームワークに応じて SAML 用のライブラリを探してほしい。

フロントエンドは正直なんでもいい。ココでは Vite で立ち上げている (http://localhost:5173) 別サーバとの通信が発生するテイで書いている。

const fs = require('node:fs');
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const SamlStrategy = require('passport-saml').Strategy;
const cors = require('cors');
const cookieParser = require('cookie-parser');
const fetch = require('node-fetch');

const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

app.use(cors({
  origin: 'http://localhost:5173', // 許可するオリジン (SPA フロントエンドなどを想定)
  credentials: true                // クッキーや認証ヘッダーを含む
}));
app.use(session({
  secret: 'saml_secret_key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}));
passport.use(new SamlStrategy({
  path: '/login/callback',  // このパスに POST される
  entryPoint: 'http://localhost:8080/realms/myrealm/protocol/saml',  // Keycloak のエントリポイント
  issuer: 'mysaml',  // Keycloak で設定した Client ID
  cert: fs.readFileSync('./keycloak-cert.pem', 'utf-8'),  // Keycloak の証明書
  validateInResponseTo: false,  // リクエストの検証を無効化
  disableRequestedAuthnContext: true  // 圧縮無効化
}, (profile, done) => done(null, profile)));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
app.use(passport.initialize());
app.use(passport.session());

// Keycloak によるログイン画面に遷移させるためのエンドポイント
app.get('/login', passport.authenticate('saml', { failureRedirect: '/login/fail', failureFlash: true }));

// Keycloak のログイン画面から戻ってくるエンドポイント・SPA フロントエンドなどにリダイレクトするイメージ
app.post('/login/callback', passport.authenticate('saml', { failureRedirect: '/login/fail', failureFlash: true }), (_req, res) => { res.redirect('http://localhost:5173'); });

// ログイン失敗時のエンドポイント
app.get('/login/fail', (_req, res) => { res.send('Login Failed'); });

// 本 Express サーバ内にセッション (req.user) があるか否かの確認用エンドポイント
app.get('/', (req, res) => { res.send(req.isAuthenticated() ? `Hello ${req.user.nameID}` : '<a href="/login">Login</a>'); });

// ログアウト用エンドポイント : 本 Express サーバのセッションを破棄するのみ
app.get('/logout', (req, res) => { req.logout(() => { res.redirect('/'); }); });

app.listen(3000, () => { console.log('Server Started'); });

バックエンドサーバと Keycloak とのやり取りは最低限コレで実現できる。Keycloak との橋渡しを行ってログインが出来たら、Express に戻ってきて Express 内にもセッションを持つ形となる。


正直、今時は OIDC 認証の方が、フロントエンドの非同期通信だけで実現できたりするので手軽なのだが、まだまだ XML を利用する SAML 認証が求められる現場も多いので、今回お試ししてみた次第。