凄い久しぶりに書く気がする。最近忙殺されすぎだな。少しずつ再開しましょう。。。
独自アプリにSAML認証付けたくなった。IdPはGoogle。
SAML認証は結構実装の情報少な目な感じがする。
認証フロー的なのはあまりちゃんと見てないのですが、
とりあえず動くようになったので流れを記載しておこう。
基本的なところは下記の先駆者さんの参考ページを見た方が確実。
- https://thinkami.hatenablog.com/entry/2024/02/12/230830
- https://tech.kanmu.co.jp/entry/2023/02/20/105042
まず、Pythonのバックエンド側。
自前で全部実装するのはかなり大変そうなので、コア部分はOSSを利用したい。
候補は下記2つ。
- python3-saml https://github.com/SAML-Toolkits/python3-saml
- pysaml2 https://github.com/IdentityPython/pysaml2
今回は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エンコードとか結構大変そうかな。
もっと簡単に出来る方法はないんだろうか。