凄い久しぶりに書く気がする。最近忙殺されすぎだな。少しずつ再開しましょう。。。


独自アプリにSAML認証付けたくなった。IdPはGoogle。

SAML認証は結構実装の情報少な目な感じがする。

認証フロー的なのはあまりちゃんと見てないのですが、

とりあえず動くようになったので流れを記載しておこう。

基本的なところは下記の先駆者さんの参考ページを見た方が確実。


まず、Pythonのバックエンド側。

自前で全部実装するのはかなり大変そうなので、コア部分はOSSを利用したい。

候補は下記2つ。

今回はpysaml2を利用。コア部分だけ使うならこっちの方がやりやすそうだったので。

公式に記載の通り、xmlsec1が必要みたいなので、事前にインストールしておく。

apt-get install xmlsec1
pip install pysaml2

そしたら、まず下記。

from saml2 import (
    BINDING_HTTP_POST,
    BINDING_HTTP_REDIRECT,
    entity,
)
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config

class SamlUtil:
    @classmethod
    def saml_client_for(cls):
        settings = {
            'entityid': 'https://*****/****',
            # 'metadata': {
            #     'remote': [
            #         {'url': 'https://*****/****'},
            #     ],
            # },
            "metadata": {
                "local": [
                    "/opt/app/samle-meta.xml",
                ],
            },
            'service': {
                'sp': {
                    'endpoints': {
                        'assertion_consumer_service': [
                            ("[call-back-url]", BINDING_HTTP_REDIRECT),
                            ("[call-back-url]", BINDING_HTTP_POST),
                        ]
                    },
                    "name_id_policy_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
                    'allow_unsolicited': True,
                    'authn_requests_signed': False,
                    'want_assertions_signed': False,
                    'want_response_signed': False,

                }
            }
        }

        spConfig = Saml2Config()
        spConfig.load(settings)
        spConfig.allow_unknown_attributes = True
        saml_client = Saml2Client(config=spConfig)
        return saml_client
        
    @classmethod
    def saml_request(cls):
        saml_client = cls.saml_client_for()
        req_id, info = saml_client.prepare_for_authenticate()
        return (req_id, info)

settingsの中身がまず肝。

GoogleでのSAML認証の場合というか、だいたいどこも同じだと思うけど、

IdP側へのアプリの登録時に以下の情報を入れる。

EntityIdとコールバックのURL。

EntityIdは大体アプリのURLが多いんじゃないでしょうか。

コールバックのURLは適宜自分のアプリに応じて。

entityidとendpointsの内容は上記で指定しているはずの内容。

自分はemailのみ欲しいので、name_id_policy_formatでemailを指定。

この辺の細かい設定内容は公式のこの辺参照

もう一つの肝が`metadata`。

ネット上の良くあるサンプルだと、IdP側のメタデータ取得用のURLをremoteで記載している場合が多い。

GoogleでのSAML認証の場合、やり方は色々あると思うのですが、メタデータのXMLを予めDLしておける。

DLした該当ファイルのローカルでのパス指定でも同様に指定可能。その場合は上述のサンプルの記載の感じ。

※いちを記載しておくと、こういうファイルはGitHubとかに上げたらダメ。

で、ここまでpysaml2でリクエストURLを作成できる。

上述のコード上だと`req_id, info = saml_client.prepare_for_authenticate()`の箇所。

infoが以下のようなdictなので、これをフロントに返してリダイレクトなりさせる。

{
  "headers": [
    [
      "Location",
      "https://*****?idpid=******&SAMLRequest=[文字列の羅列]"
    ]
  ],
  "data": [],
  "status": 303,
  "url": "https://[認証URL]",
  "method": "GET"
}

headersのLocationのURLがクライアントがブラウザでつなぐ先のURL。

SAMLRequestのパラメータがSAMLリクエストのxmlをURLエンコードしたもの。

この文字列の内容を見たい場合はこのツールサイトあたりでSAMLデコードすると何かいてるか見れる。

で、クライアントでLocationのURLにアクセスすると、今回の場合はGoogle側で認証確認して、

コールバック先のURLにPOSTでリクエストが戻ってくる。

POSTではBODYにSAMLResponseの名前でレスポンスのXMLをURLエンコードした文字列が入ってくる。

これのパースはpysaml2で下記の様にしてパースできる。

saml_response = req.data.get("SAMLResponse")  # この値の取得は適宜いい感じに。。。
saml_client = cls.saml_client_for()
authn_response = saml_client.parse_authn_request_response(saml_response, entity.BINDING_HTTP_POST)
authn_response.parse_assertion()
user_info = authn_response.get_subject()

# 例えばメタデータでemail指定してる場合はuser_info.textにその値が入ってる。
user_email = user_info.text

具体的に生のXMLがどういう内容かは前述のツールサイトでデコードすれば見れる。

あと、request時のIDに一意なIDがセットされるので、検証しておいた方がよいかな。

ここまでくれば、後は独自アプリ側の認証情報をどう作るか次第。


フロントはNUXT3なのですが、NUXT3のみでも上手い事やれば完結出来そうな感じがした。

NUXT3はserver側でのリクエストのみで処理させることも出来る。

公式:https://nuxt.com/docs/guide/directory-structure/server

コールバックのURLを例えば、sso/saml/callbackとかにした場合、

`server/routes/sso/saml/callback.ts`のようにファイル作って、下記の様に記述すれば、

SAMLResponseをPOSTで受け取って処理できる。

※参考に載せたURLのISSUEで記載されているのは少し古いやり方なので、今のNUXT3では動作しません。

import { defineEventHandler, readBody } from "h3";

export default defineEventHandler(async (event) => {
  if (event.node.req.method === "POST") {
    const backUrl = "http://サーバ側からバックエンドのAPI叩く場合のURLとか";
    const data = await readBody(event); // POSTのデータを読み込み
    const samlData = await $fetch(backUrl, {
        method: "POST",
        body: {saml_response: data.SAMLResponse}
    });
    await sendRedirect(event, [リダイレクトさせたいURLとか], 302)
  }
});

この辺のやり方が分かってればNUXT3のみで完結も出来そう。

ただ、XMLのパースとかURLエンコードとか結構大変そうかな。

もっと簡単に出来る方法はないんだろうか。