メインコンテンツまでスキップ

外部APIでのmetatell利用者認証

プラグインから自前のバックエンドAPI(以下、プラグイン用API)を呼び出すとき、そのリクエストが本当にmetatellのログイン利用者から来たものかを、API側で確かめたいことがあります。 このページでは、metatellが発行するトークンを使ってログイン利用者を識別する方法を説明します。

おおまかな流れは次のとおりです。 まずプラグインがSDK経由でトークンを受け取り、それをプラグイン用APIへ送ります。 プラグイン用APIはそのトークンを確かめ、正しければ送信者をmetatellの利用者として信頼します。

このトークンは、特定のプラグイン用API宛てに発行された、短い有効期限を持つものです。 metatellのログインセッションそのものはプラグインへ渡らないため、万一トークンが漏れても影響を受ける範囲を限定できます。

プラグイン側の実装

トークンを取得する

@urth/metatell-sdk/authgetPluginApiToken()でトークンを取得します。 非同期関数なのでawaitで待ちます。

引数には、OAuth 2.0 のクライアントID(例: plugin-foo-api)を渡します。 これは、プラグイン用APIを表すものとしてmetatellのIdPに登録したクライアントのIDで、発行されるトークンの宛先(aud)になります。 登録は弊社側で行うため、プラグイン側でクライアントシークレットを扱う必要はありません。 入手方法は後述します。

import { getPluginApiToken } from "@urth/metatell-sdk/auth";

const token = await getPluginApiToken("plugin-foo-api");

トークンには有効期限があります。 リクエストのたびにgetPluginApiToken()を呼び、毎回新しいトークンを取得してください。

プラグイン用APIへ送る

取得したトークンをAuthorizationヘッダーにBearerとして付けて送信します。

import { getPluginApiToken } from "@urth/metatell-sdk/auth";

type ApiUserProfile = {
id: string;
displayName: string;
};

export async function fetchProfile(): Promise<ApiUserProfile> {
const token = await getPluginApiToken("plugin-foo-api");

const response = await fetch("https://foo.example.com/api/metatell/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error(`Profile API failed: ${response.status}`);
}

return response.json() as Promise<ApiUserProfile>;
}

取得に失敗したときの処理

getPluginApiToken()は、失敗すると型付きのエラーでrejectします。 原因ごとにinstanceofで分岐できます。 いずれも@urth/metatell-sdk/authからエクスポートされています。

エラー意味
InvalidClientIdErrorclientId が空、もしくは文字列でない
NotAuthenticatedErrormetatellにログインしていない
PluginApiClientNotFoundErrorclientId が未登録(許可されていない)
PluginTokenExchangeErrorトークンの取得に失敗した(その他の失敗)
import {
getPluginApiToken,
NotAuthenticatedError,
PluginApiClientNotFoundError,
} from "@urth/metatell-sdk/auth";

try {
const token = await getPluginApiToken("plugin-foo-api");
// token を使ってリクエストする
} catch (e) {
if (e instanceof NotAuthenticatedError) {
// 利用者にサインインを促す
} else if (e instanceof PluginApiClientNotFoundError) {
// clientId の設定ミス
} else {
// PluginTokenExchangeError / InvalidClientIdError
}
}
警告

ブラウザ側でトークンの中身を読み取って得られる情報は、画面表示の用途にだけ使ってください。 署名を確かめていないトークンの中身は書き換えられている恐れがあるため、認証や権限判定の根拠にしてはいけません。 利用者の確認は、かならず次に説明するプラグイン用API側の検証で行います。

トークンの中身

検証を説明する前に、トークンの形式と中身を確認します。

getPluginApiToken()が返すのは、JWT(JSON Web Token)と呼ばれる、署名の付いた文字列です。 metatellの秘密鍵で署名されているため、第三者が中身を書き換えると署名の検証に失敗します。 そのためプラグイン用APIは、署名が正しいことを確かめたうえで、含まれる情報を信頼できます。

JWTの中身は「クレーム」と呼ばれる項目の集まりです。 検証で使うのは次の4つです。

  • iss(発行者):トークンを発行したmetatellを表す識別子(URL形式)。
  • aud(宛先):このトークンの宛先。getPluginApiToken()に渡したOAuthクライアントID(プラグイン用APIを表す)が入る。
  • sub(利用者ID):トークンの持ち主であるmetatell利用者を一意に表すID。
  • exp(有効期限):このトークンが使える期限。

audを確かめることが、なぜ安全につながるのかを補足します。 audには、宛先となる特定のプラグイン用APIのIDだけが入ります。 これを照合すれば、別のAPI向けに発行されたトークンを自分のAPIへ使い回すことを防げます。

プラグイン用API側の検証

検証には、署名を確かめるための公開鍵が必要です。 JWKSエンドポイントは、その公開鍵を配布しているmetatellのURLです。 署名方式は**RS256**(公開鍵暗号を用いる方式)に固定されています。

プラグイン用APIは、受け取ったトークンを信頼する前に次の項目をすべて検証してください。

項目検証内容
署名JWKSエンドポイントの公開鍵で署名を確かめ、RS256 以外の方式は拒否する
発行者(isshttps://id.u-rth.com/realms/<organization_id> と一致する
宛先(aud自分のOAuthクライアントID(例: plugin-foo-api)と一致する
有効期限(exp期限切れのトークンを拒否する
利用者ID(subUUID v4形式(最大36文字)。利用者の一意なキーとして使える

JWKSエンドポイントのURLは次の形式です。

https://id.u-rth.com/realms/<organization_id>/protocol/openid-connect/certs
Note

この機能の利用は申請制です。 弊社までお申し込みいただくと、弊社側でOAuthクライアントを登録し、コードに設定する次の2つの値をお知らせします。

  • 組織ID(<organization_id> に入る値)
  • OAuthクライアントID(トークンの aud に入る値)

subは利用者ごとに一意なため、バックエンドのデータベースで利用者キーとして保存できます。 値はUUID v4形式(最大36文字)なので、36文字まで格納できる列を用意してください。

検証コードの例(Node.js)

jose を使った検証例です。

import { createRemoteJWKSet, jwtVerify } from "jose";

// 組織IDとクライアントIDに合わせて設定する
const ORGANIZATION_ID = "your-organization-id";
const ISSUER = `https://id.u-rth.com/realms/${ORGANIZATION_ID}`;
const AUDIENCE = "plugin-foo-api"; // 宛先となるOAuthクライアントID

const jwks = createRemoteJWKSet(
new URL(`${ISSUER}/protocol/openid-connect/certs`),
);

export async function verifyMetatellToken(authorizationHeader?: string) {
const token = authorizationHeader?.replace(/^Bearer\s+/i, "");
if (!token) {
throw new Error("missing bearer token");
}

const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ["RS256"],
});

// payload.sub が metatell 利用者の一意な ID
return payload;
}

プラグイン用APIのリクエストハンドラーから利用する例です。

export async function handleRequest(request: Request): Promise<Response> {
try {
const claims = await verifyMetatellToken(
request.headers.get("authorization") ?? undefined,
);

return Response.json({ userId: claims.sub });
} catch {
return new Response("unauthorized", { status: 401 });
}
}

セキュリティに関する注意

  • シークレットをプラグインに埋め込まない:プラグインのコードは利用者環境でそのまま実行されるため、バックエンドのAPIキーや認証情報をコードに含めないでください。
  • 通信はHTTPSで行う:トークンを送るリクエストは、かならずHTTPSで送信してください。

関連ドキュメント