Auth0のIDトークンをサーバーで検証する
どうも。
Webアプリケーションにおいて、APIを使用してフロントエンドとバックエンド間でデータのやり取りをする場合についてお話しします。
認証されたユーザーがフロントエンドからバックエンドにデータをPOSTする場合、バックエンドのAPIに対してデータをリクエストすることになります。
この時、セキュリティの観点から、認証されたユーザーからのPOSTであるかどうかを必ず検証する必要があります。
よく使われるパターンは、リクエストのヘッダーのAuthorizationにIDトークンを含めて送信することです。
サーバーサイドでは、ヘッダーからIDトークンを取り出し、その有効性を検証する方法があります。
Auth0を使用する場合でも同様の手法が可能ですので、試してみたいと思います。
1.そもそもIDトークンってなに?
一般的にはJSON Webトークン(JWT)と呼ばれる仕組みを使用します。認証が行われると、名前やメールアドレスなどの個人のユーザー情報を取得することができます。IDトークンは、リソースサーバーやサーバーアプリケーションに対するユーザーの認証にも使用できます。
ここでAccessトークンとIDトークンの違いについて考えてみましょう。
TwitterやSpotifyなどの外部のWeb APIを使用する際には、常にAccessトークンを使用して認可が必要です。
今回は、自身のバックエンドアプリケーションの認証にIDトークンを使用したいと思います。
それでは、まずは事前にAuth0のアカウントを取得し、簡単なログイン・ログアウト機能を実装しておきましょう。
の4点です。
今回は、Next.jsを使用して進めていきます。
まず、必要なパッケージをインストールしましょう。
$ yarn add -D @auth0/nextjs-auth0
2.headersにIDトークンを入れる関数
// api/index.ts
const api = async (path: string, method: 'PATCH' | 'POST', token: string | undefined, body: { text: string }) => {
const response = await fetch(path, {
method: method,
headers: {
Authorization: token ?? '',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const res = (await response.json());
return res;
};
Fetch時にトークンとボディを受け取り、PATCHまたはPOSTを行う処理を記述します。
3.IDトークンを取得する
pages/index.tsx
import { withPageAuthRequired, useUser } from '@auth0/nextjs-auth0';
// クライアントで取得する場合
const { user } = useUser();
// サーバー側で取得する場合
export const getServerSideProps: GetServerSidePropsContext = (context) => {
const session = getSession(context.req, context.res);
const user = this.session?.user;
return { props: { token: user.idToken }}
}
ログイン処理を実装し、正しくログインできればデータの取得が可能になるはずです。
次にバックエンドのAPIを構築していきます。
今回はNext.jsのAPI Routesを使用します。
その前に、IDトークン(JWT)を検証するためのパッケージをインストールしましょう。
$ yarn add -D jsonwebtoken
このパッケージでは、IDトークンのペイロード内に含まれる署名を、Auth0が公開している公開鍵の内容と検証します。
4.pemを取得する。
やり方は3つあります。
以上が3つの方法ですが、今回は後者のJSON Web Key Set (JWKS) から pem を生成する方法を試してみたいと思います。
まず、以下のライブラリを事前にインストールしておきましょう。
$ yarn add -D jwk-to-pem
以下に、一連の処理の概要を書いてみます。
// jwk/index.ts
import jwkToPem, { JWK } from 'jwk-to-pem';
//----------------------------------------
// jwksのtype
//----------------------------------------
export type IJwks = {
keys: {
alg: string;
kty: string;
use: string;
n: string;
e: string;
kid: string;
x5t: string;
x5c: [];
}[];
};
//----------------------------------------
// jwksを取得する処理
//----------------------------------------
const jwks = async (path: string) => {
const response = await fetch(path, {
method: 'GET'
});
const jwk = (await response.json()) as IJwks;
const pem = jwkToPem(jwk.keys[0] as JWK); // 配列の1つ目を使用してpemに変換する
return pem;
};
やっていることは簡単です。
jwksの取得からpemへの変換までが完了したため、バックエンドのAPI処理を記述していきます。
5.バックエンドでIDトークンを受け取って検証する
// pages/api/v1/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';
import { jwks } from 'services/jwks';
const Index = async (req: NextApiRequest, res: NextApiResponse) => {
// 公開鍵を生成する
const pem = await jwks('https://[your_account]/.well-known/jwks.json');
const idToken = req.headers['authorization'];
// idTokenがない場合はエラー
if(!idToken) {
res.status(400).json({
datas: {
error: {
message: '認証エラーです'
}
}
});
}
// idTokenから署名で認証をチェック
try {
jwt.verify(String(req.headers['authorization']), pem, { algorithms: ['RS256'] }, function (_err, decodedToken) {
if (_err) throw { _err };
// DBにアクセスする処理を書いたり...
});
} catch (e) {
return res.status(400).json({
datas: {
error: {
message: 'ログインセッションが切れました。再度ログインしてください。'
}
}
});
}
export default Index;
証明書の内容を検証し、検証に成功した場合はdecodedTokenに認証情報が格納されます。
もしIDトークンの有効期限が切れた場合など、_errにエラー情報が格納されるため、再度ログインさせるなどの処理を実装しましょう。
認証情報が取得できれば、認証が正常に行われたことになりますので、APIサーバーやDBなどへのアクセスを行い、処理を実行しましょう。
IDトークンの有効期限はデフォルトで36000秒(10時間)です。
案外簡単に実装できたかと思います。
IDトークンの検証自体は初めて行う場合、全体像を把握するのが少し難しいかもしれませんが、実際に手を動かしてみるとそこまで難しくないですね。
この記事が参考になれば幸いです。それでは。