Help us understand the problem. What is going on with this article?

Auth0でSPA+APIの認証・認可

はじめに

Auth0を使って以下のことをやってみました。

  • Single Page Applicationに認証機能を組み込み
  • 認証されたユーザーにBackend APIに対する認可を与える
  • Backend APIはユーザーが認可されているかを検証

ログイン時にJWT形式のアクセストークンを取得し、Backend APIで検証するという手順になります。

アプリの準備

SPAの準備

0から始めるので、とりあえずサンプルアプリそのままでやってみます。

  1. Auth0にログインしてDashBoardを開く
  2. DashBoard > Applications > CREATE APPLICATIONボタンを押してSingle Page Applicationを登録
  3. DashBoard > Applications > 2.で登録したSPAを選択 > Quick Startの中から使用するライブラリを選択(今回自分はライブラリを使用しない素のjavascriptを選んだので、以降はその前提での作業手順になります。)
  4. 3.で選択したもののQuick Startが表示されるので、その画面内のDOWNLOAD SAMPLEボタンを押下してサンプルアプリをダウンロードします。
  5. サンプルアプリをダウンロードする時に表示された説明に従って、DashBoard > Applications > SettingsAllowed Callback URLs「Allowed Web Origins」Allowed Logout URLsを設定する。
  6. ダウンロードしたサンプルアプリをnpm install後にnpm startで立ち上げる。

先にApplicationを登録してからサンプルアプリをダウンロードすると、client_id等の諸々の設定を済んだ状態のアプリがダウンロードできるので、上記手順だけであっという間にアプリを立ち上げることができます。

※余談
SPAなので認証フローはimplicitなのかなと思っていましたが、
Auth0の新しいSPA用のSDKのソースを見た感じ、PKCEでやっているようです。

Backend APIの準備

こちらも同様にサンプルアプリをそのまま使います。

  1. Auth0にログインしてDashBoardを開く
  2. DashBoard > APIs > CREATE APIボタンを押してAPIを登録
  3. APIはDashBoardからサンプルアプリをダウンロードできないので、Auth0のドキュメントのQuick Startを開き、任意の言語、フレームワークを選択。(自分は Node(Express)APIを選択。以降の説明はこれを前提にします。)
  4. 3.で選択したもののQuick Startが表示されるので、その画面内のDOWNLOAD SAMPLEボタンを押下。
  5. 表示されたDownload sample画面でAPIを選択できるので、2.で登録したAPIを選択し、サンプルアプリをダウンロードする。
  6. ダウンロードしたサンプルアプリをnpm install後にnpm startで立ち上げる。

これだけで、以下の3つのエンドポイントを持ったAPIサーバーを立ち上げることができます。

server.js
app.get('/api/public', function(req, res) {
  res.json({
    message:
      "Hello from a public endpoint! You don't need to be authenticated to see this."
  });
});
server.js
app.get('/api/private', checkJwt, function(req, res) {
  res.json({
    message:
      'Hello from a private endpoint! You need to be authenticated to see this.'
  });
});
server.js
const checkScopes = jwtAuthz(['read:messages']);

app.get('/api/private-scoped', checkJwt, checkScopes, function(req, res) {
  res.json({
    message:
      'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.'
  });
});
  • /api/public : 認可不要の誰でもリクエスト可能なAPI。
  • /api/private : スコープなしのアクセストークンを含むリクエストで利用可能なAPI。checkJwtミドルウェアでJWT形式のアクセストークンの検証を実施。
  • /api/private-scoped : スコープあり(サンプルアプリの場合:read:messages)のアクセストークンを含むリクエストで利用可能なAPI。checkJwtでのアクセストークンの検証に加え、checkScopesでscopeの検証も実施。

JWT形式のアクセストークンについては後述。

Backend APIのDashBoard設定

サンプルアプリに定義されているスコープread:messagesをDashBoardからAPIのPermissionsに設定します。

  1. DashBoard > APIs > 今回作成したAPIを選択 > Permissionsを選択
  2. Permissionにread:messages、Descriptionに適当な説明を設定し、ADDボタンを押下

DashBoard > APIs > 今回作成したAPIを選択 > Machine to Machine Applicationsで、Backend APIと連携するアプリの紐付けをする必要があるかと思いましたが、説明に「SPAやNative Appは追加の設定は不要」的なことが書いてあるのに加え、SPAはアプリの一覧にも出てきませんでした。どうやら設定は不要のようです。

ユーザーの作成とロール付与

SPAとBackend APIを利用するユーザーを作成します。

  1. DashBoard > Users & Roles > Users > CREATE USERボタンを押下し、ユーザーを作成

次に、ロールの作成とユーザーに対してロールの付与をします。
これをやらないと/api/private-scopedに対する認可(scope)を要求することができません。

  1. DashBoard > Users & Roles > Roles > CRETE ROLEボタンを押下し、適当なNameとDescriptionを入力して新しいロールを作成。
  2. DashBoard > Users & Roles > Roles > 登録したロールを選択 > Permissions > ADD PERMISSIONSボタンを押下し、先に登録したPermission read:messagesを選択する。
  3. DashBoard > Users & Roles > Roles > 登録したロールを選択 > Users > ADD USERSボタンを押下し、ロールを付与するユーザーを選択する。(Userの詳細ページからロールを付与することも可能です。)

Backend APIを利用できるようにSPAを修正

SPAからAuth0にBackend APIに対する認可を要求

SPAのサンプルアプリそのまんまだと当然Backend APIに対する認可を取得できていないので、ログイン時にAuth0に認可を要求するようにSPAを修正します。

public/js/app.js
const configureClient = async () => {
  const response = await fetchAuthConfig();
  const config = await response.json();

  auth0 = await createAuth0Client({
    domain: config.domain,
    client_id: config.clientId,
    audience: "xxxxxxxxxxxx",   // Backend APIのIdentifierを設定
    scope: "read:messages"      // APIに定義したPermissionを設定
  });
};

通常、Auth0が発行するアクセストークンの形式は無意味文字列ですが
ログイン時のパラメータにaudienceを設定をすると、JWT形式のアクセストークンが取得できます。

JWT形式のアクセストークンが取得できているかを確認するためのコードも仕込んでみます。

public/js/app.js
window.onload = async () => {
  await configureClient();

  // 省略

  if (shouldParseResult) {
    console.log("> Parsing redirect");
    try {
      const result = await auth0.handleRedirectCallback();

      if (result.appState && result.appState.targetUrl) {
        showContentFromUrl(result.appState.targetUrl);
      }

      console.log("Logged in!");
      const accessToken = await auth0.getTokenSilently(); // アクセストークンを取得
      console.log(accessToken);                           // アクセストークンを表示
    } catch (err) {
      console.log("Error parsing redirect:", err);
    }

    window.history.replaceState({}, document.title, "/");
  }

  updateUI();
};

ログイン時にconsoleにアクセストークンが表示されるようになります。

eyJ0eBX2K-cZ2F ... em0dB84Zo5fiEKq6_fQ

これをjwt.ioでデコードして、アクセストークンのペイロードをみてみると、audiencescopeに要求した内容が含まれていることがわかります。

アクセストークンのペイロード
{
  "iss": "https://{ドメイン名}/",
  "sub": "auth0|5d ... 7f",
  "aud": [
    "xxxxxxxxxxxx",
    "https://{ドメイン名}/userinfo"
  ],
  "iat": 1569000000,
  "exp": 1569000000,
  "azp": "8R ... lD",
  "scope": "openid profile email read:messages"
}

APIリクエストボタンを設置

SPA上に3つのAPIを呼び出すボタンを置きます。
動作確認がしたいだけなのでココは本当にテキトーです。

index.html
<div>
  <button class="btn btn-primary" id="publicApiRequestBtn" onclick="publicApiRequest()">
    Public API Request
  </button>
  <button class="btn btn-primary" id="privateApiRequestBtn" onclick="privateApiRequest()">
    Private API Request
  </button>
  <button class="btn btn-primary" id="privateScopedApiRequestBtn" onclick="privateScopedApiRequest()">
    Private-Scoped API Request
  </button>
</div>
public/js/app.js
// /api/publicへのリクエスト
const publicApiRequest = async () => {
  const myHeaders = new Headers();
  const accessToken = await auth0.getTokenSilently();
  myHeaders.set("Authorization", "Bearer " + accessToken);
  const response = await fetch("http://{ホスト名}/api/public", {
    method: "GET",
    headers: myHeaders
  })
    .then((res) => {
      return res.json();
    })
    .catch((err) => {
      console.log(err);
    });
  alert(JSON.stringify(response));
};

// /api/privateへのリクエスト
const privateApiRequest = async () => {
  const myHeaders = new Headers();
  const accessToken = await auth0.getTokenSilently();
  myHeaders.set("Authorization", "Bearer " + accessToken);
  const response = await fetch("http://{ホスト名}/api/private", {
    method: "GET",
    headers: myHeaders
  })
    .then((res) => {
      return res.json();
    })
    .catch((err) => {
      console.log(err);
    });
  alert(JSON.stringify(response));
};

// /api/private-scopeへのリクエスト
const privateScopedApiRequest = async () => {
  const myHeaders = new Headers();
  const accessToken = await auth0.getTokenSilently();
  myHeaders.set("Authorization", "Bearer " + accessToken);
  const response = await fetch("http://{ホスト名}/api/private-scoped", {
    method: "GET",
    headers: myHeaders
  })
    .then((res) => {
      return res.json();
    })
    .catch((err) => {
      console.log(err);
    });
  alert(JSON.stringify(response));
};

APIリクエストを実行

ここまでの設定がうまくできていれば、SPAに配置したボタンを押すとそれぞれのAPIのレスポンスがアラート表示されるはずです。

/api/publicのレスポンス
{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}
/api/privateのレスポンス
{"message":"Hello from a private endpoint! You need to be authenticated to see this."}
/api/private-scopeのレスポンス
{"message":"Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this."}

APIリクエスト時にCORSエラーが出る場合は、Backend API側のCORSオプションを見直してください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away