Firebase Authentication×Keycloak連携を実装してみた

  •  
 
トビウオ2019年11月28日 - 18:04 に投稿

概要

以前の記事で、Googleが提供するユーザー認証機構である「Firebase Authentication」について概ね理解できました。

ただ、Firebase Authenticationには「カスタム認証」という概念があり、他の任意の認証システムをFirebase認証と連携させることができます。

……というわけでやってみた記録です。スライドにもまとめていたりします。

今回やりたいスキーマについて整理

Firebase Authenticationには「カスタム認証」という仕組みがあります。かいつまんで言えば、

  • ユーザーを一意に識別するUIDを用意する
    • UIDは、文字列であればどういったフォーマットでも構わない
  • Firebase Admin SDKを利用することで、UIDからカスタムトークンを作成する
    • カスタムトークンはJWT形式で、ちゃんと署名もされているものである
  • カスタムトークンをFirebase SDKでFirebaseのサーバーに送信し、認証情報を受け取る
    • 認証情報は普通にJSON形式で、ユーザー名など様々な情報が取れる

といった仕組みです。

カスタム認証の仕組み

ここで注意したいのが、UIDの仕様が通常のFirebase認証と異なっていることです。

  • 通常のFirebase認証では、「Firebase側でユーザーを管理するためのUID」がランダムな文字列となります
  • カスタム認証を用いたFirebase認証では、「Firebase側でユーザーを管理するためのUID」が上記の「ユーザーを一意に識別するUID」となります

なので、カスタム認証を用いる場合はそれだけにするのが無難な気がします(※併用・アカウント統合も可能ではある)。

実際にコーディングしてみた

1. Keycloakで普通に認証するルーチンを書く

TypeScript+keycloak.jsで書いた例です。

import Keycloak from 'keycloak-js';

const initOptions = {
  url: 'http://example.com/auth/',
  realm: 'sample',
  clientId: 'sample-auth'
};

// 初期化
const keycloak = Keycloak(initOptions);
keycloak.init({onLoad: 'login-required'})
  .success((authenticated) => {
    if (authenticated) {
      alert('状態:認証済みです');
    } else {
      alert('状態:まだ認証されていません');
    }
  })
  .error(() => alert('エラー:初期化に失敗しました'));

// 認証成功時の処理を登録
keycloak.onAuthSuccess = () => {
  alert(`IDトークン:${keycloak.idToken}`);
  keycloak.loadUserProfile().success((data) => {
    alert(`ユーザー名:${data.username}`);
  });
};

// 認証処理
keycloak.login();

ポイントとしては、認証成功時の処理をコールバックとして登録することと、ユーザー名を取り出すには一段Promiseが挟まることです。

2. カスタムトークンを作成するため、uid生成に必要な情報を検証サーバーに渡す

これは、ユーザー名をそのまま渡すのではなく、IDトークンを送信することにします。

3. 検証サーバー側で、受け取ったIDトークンの検証を実施する

受信したIDトークンは、Open ID Connectの規格上、JWT形式になっています。
JWTは、ヘッダー・ペイロード・署名をそれぞれBase64形式でエンコードしたものを繋げた文字列です。
ヘッダーには署名アルゴリズムなどの情報、ペイロードには送りたいデータの実体(OIDCでは認証情報など)がJSON形式で収められています。

ただ、ヘッダー・ペイロード・署名を攻撃者が勝手に作成することができるため、正しいIDトークンかの検証は必須となります。
幸い、検証作業の難易度を下げるためのライブラリが提供されているため、今回はそちらを使用します。

# jwtをインポートするためにpyjwt、RSAAlgorithmをインポートするためにcryptographyを
# pipからそれぞれインストールする必要がある
import jwt
from jwt.algorithms import RSAAlgorithm

# ヘッダーをデコードし、必要な情報を手に入れる
# algは署名アルゴリズム、kidはキーID
header = jwt.get_unverified_header(id_token)
print(header)
alg = header['alg']
kid = header['kid']

# 公開鍵を入手する。データさえ取れればいいのでrequestsじゃなくてurllib3などでもOK
import requests
KEYCLOAK_PATH = 'http://example.com/auth/'
KEYCLOAK_REALM = 'sample'
cert_url = f'{KEYCLOAK_PATH}realms/{KEYCLOAK_REALM}/protocol/openid-connect/certs'
data: Dict[str, any] = requests.get(cert_url).json()

# キーIDに対応する公開鍵を取得する
jwk = [x for x in data['keys'] if x['kid'] == kid][0]
public_key = RSAAlgorithm.from_jwk(json.dumps(jwk))

# ペイロードをデコードする。ただし上記のように検証処理も実施する
KEYCLOAK_CLIENT_ID = 'sample-auth'
claims = jwt.decode(id_token, public_key,
                    issuer=f'{KEYCLOAK_PATH}realms/{KEYCLOAK_REALM}',
                    audience=KEYCLOAK_CLIENT_ID,
                    algorithms=[alg])

少々手間は掛かりますが、これによりIDトークンが正当なものであるかを証明できました。
なお、 「claims = jwt.decode(id_token, verify=False)」とすると、検証せずペイロード部分を取得できますが推奨されません。

4. 検証したIDトークンからユーザー情報を取得し、uidを作成する

サンプルアプリケーションなので単純に、「claims["preferred_username"] をユーザー名として、それをそのままuid」とする方向で。

5. uidからカスタムトークンを作成する

Python向けのFirebase Admin SDKの初期化方法は複数ありますが、今回は「設定ファイル(JSON形式)の位置を環境変数に書き込んでから初期化コマンドを叩く」形式とします。

// firebase-adminsdk.json
{
  "type": "service_account",
  "project_id": "hogehoge",
  "private_key_id": "aaabbbccc",
  "private_key": "-----BEGIN PRIVATE KEY-----\n(中略)\n-----END PRIVATE KEY-----\n",
  "client_email": "fugafuga@hogehoge.iam.gserviceaccount.com",
  "client_id": "dddeeefff",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fugafuga%40hogehoge.iam.gserviceaccount.com"
}
import firebase_admin
import firebase_admin.auth

# Firebase Admin APIを初期化
config_path = os.path.join(os.path.dirname(__file__), 'firebase-adminsdk.json')
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = config_path
firebase_admin.initialize_app()

# カスタムトークンを生成
custom_token = firebase_admin.auth.create_custom_token(uid)

6. カスタムトークンを使ってfirebase認証する

カスタムトークンもJWT形式で、これまた検証可能ではありますが、クライアントサイドだと難しいので検証は省略します。

import firebase, { auth } from 'firebase';

// Firebaseの設定(実運用では別ファイルに切り分けなど行う)
const firebaseConfig = {
  apiKey: "piyopiyo",
  authDomain: "hogehoge.firebaseapp.com",
  databaseURL: "https://hogehoge.firebaseio.com",
  projectId: "hogehoge",
  storageBucket: "hogehoge.appspot.com",
  messagingSenderId: "hogefugapiyo",
};

// Firebase APIを初期化
firebase.initializeApp(firebaseConfig);

// カスタムトークンを利用して認証開始
firebase.auth().signInWithCustomToken(customToken)
  .then((result) => {
    const user = result.user;
    const uid = user.uid;
  })
  .catch((error) => console.error(error));

……ここまでの処理で、Keycloakと認証〜firebase認証によるユーザー情報取得まで行うことができました。

参考資料

コメントを追加

プレーンテキスト

  • HTMLタグは利用できません。
  • 行と段落は自動的に折り返されます。
  • ウェブページのアドレスとメールアドレスは自動的にリンクに変換されます。
CAPTCHA
この質問はあなたが人間の訪問者であるかどうかをテストし、自動化されたスパム送信を防ぐためのものです。