🔥

OAuth 2.0の理解を諦めた人へ:絶対に理解させるガイド

2024/12/29に公開
5

背景

OAuth 2.0 は、現代の Web およびモバイルアプリケーションで認証と認可のために広く採用されているプロトコルです。多くの開発者が最初にこのプロトコルに直面すると、その概念やフローの多様性に圧倒されることがある。特に、セキュリティが重要な役割を果たすアプリケーションでは、OAuth 2.0 の正確な理解と適切な実装が必須となる。

この記事では、特に Auth0 を使用して OAuth 2.0 を実装しているエンジニア向けに、OAuth 2.0 の基本から応用までをわかりやすく解説します。Auth0 は、OAuth 2.0 プロトコルをサポートすることで、開発者がセキュアな認証を簡単に実装できるようにするサービスの一つです。これにより、エンジニアは認証機能のコンプレックスさを抑えつつ、安全なアプリケーション開発に集中できるようになる

本ガイドでは、OAuth 2.0 の主要なコンセプト、利用シナリオ、そして Auth0 と組み合わせた実践的な実装例を通じて、読者が OAuth 2.0 の全体像をしっかりと掴めるように構成しています。OAuth 2.0 の理解が進むことで、より効率的かつ安全に認証システムを設計・実装できるようになるでしょう。

基礎知識

OAuth2.0 とは

OAuth 2.0 は、第三者アプリケーションがユーザーの代わりにサーバー資源にアクセスするための権限を安全に委任するためのシステムです。
(あくまでリソースアクセス許可の権限のみです。「誰が」「いつ」「どこで」「なんのために」といった情報 かつ署名されていると OpenID Connect になります)

https://zenn.dev/mryhryki/articles/2021-01-30-openid-connect

auth0

Auth0 は、開発者が認証と認可機能をアプリケーションに容易に統合できるようにするクラウドベースのサービスです。

https://auth0.com/jp

他の例:

  • cognito(AWS)
  • Firebase Authentication (Google)
  • Okta

ただ、利用者数や機能によって料金がかかることや柔軟で機能追加ができないため、大規模な会社では、自作の認証サーバがある印象です。

https://aws.amazon.com/jp/cognito/

accessToken

AccessToken は、OAuth 2.0 認証プロセスでクライアントがリソースサーバーのリソースにアクセスするために使用されるトークンです. frontend から backend に アクセスするための鍵ぐらいに思ってくれたらいいです。(後で詳しく説明します)

refreshToken

refreshToken は、AccessToken が失効した後に新しい AccessToken を取得するために使用されるトークンです。
(後で詳しく説明します)

解説

流れとしては、

  • accessToken の問題を解決するために refreshToken があり
  • refrehToken の問題を解決するために refresh Rotation がある


なぜ accessToken が必要なのか

1. accessToken で backend にアクセスする

accessToken

2. accessToken が盗まれると...

backend にアクセスするごとに accessToken がインターネット上を通るので盗まれる可能性がある。accessToken が盗まれることで悪役は、backend の api にアクセスが可能になってしまう.
大きな問題点としては、同じ accessToken が使われていることである
accssTokenProblem

なぜ refreshToken が必要なのか

1. refreshToken を使うと

refreshToken を認証サーバに渡すことで新しい accessToken を取得することができる
refresh

2. accessToken を盗まれると...

refreshToken を使うことで、accessToken が更新される。これにより、accessToken が盗まれていも、その accessToken は使えなくなっている可能性が高いため、backend にアクセスすることを防ぐことになる.

refresh2

3. refreshToken が盗まれると...

同じ refreshToken がインターネット上を通ることになるので、refreshToken を盗まれ、有効な accessToken を発行することができてしまう.
refresh3

なぜ refreshToken Rotation が必要なのか

refreshToken Rotation とは

refreshToken を認証サーバに渡すときに、その refreshToken は、無効になり使用できなくなる。そして新しい accessToken と新しい refreshToken を取得して、accessTokenrefreshToken を更新することができる
refresh4

refreshToken が盗まれると...

refreshToken を盗まれていても、すでにその refreshToken が使えなくなっている可能性があるので、有効な accessToken を取得することができない.
無効な refreshTokenaccessToken を取得しようとすると、そのセッション(全ての accessTokenrefreshToken)は使えなり、自動的にログアウト状態になる
refresh5

現状対策できないこと

refreshToken Rotation をしても1 つだけセッションが盗まれてしまう方法があります

ログアウト(セッションを切らない)をしないで放置してしまうこと


  • refreshToken が無効化されずに、refreshTokenaccessToken が発行されるため、リソースにアクセスできるようになる.
    上で説明した無効な refreshToken で accessToken を取得しようとすると、そのセッション(全ての accessToken と refreshToken)は使えなくるという安全装置が作動しなくなるためである

まとめ

oauth2.0 を使う時は、

  • 開発者は、accessTokenrefreshTokenを短くしましょう
  • ユーザは、ログアウトをしっかりしましょう

次回は、
Nextjs(approuter)登場による token の管理のベストプラクティスについて
話そうと思います

GitHubで編集を提案

Discussion

ritouritou

OAuth 2.0の代表的な使い方に ○○ ログインを挙げるのは避けるべきでしょう。

OAuth 2.0 は、第三者アプリケーションがユーザーの代わりにサーバー資源にアクセスするための権限を安全に委任するためのシステムです。

この通り、第3者のアプリケーションがあるユーザーのリソースへアクセスを可能にします。
その結果を用いてユーザー認証に利用するユースケースも実際存在しますがあくまで「OAuth 2.0を用いた認証機能の実装」であり、独自実装が多分に含まれています。

この○○ ログインという用途のために策定された仕様はOpenID ConnectやSAMLです。
この違いは実際のサービスの安全性に大きく関わることですので正しい理解が必要です。

なぎなぎ

ご指摘ありがとうございます。 OpenID ConnectとOAuth 2.0を混同して考えてしまい、誤解を招く表現となってしまいました。OAuth 2.0はリソースへのアクセス許可を委任するための仕組みであり、ユーザー認証を直接的に目的としたものではないという点を改めて認識しました。とても勉強になりました。今後はこの点を明確にして情報提供を行いたいと思います。

マロンマロン

記事ありがとうございます。
一点お伺いしたいことがあり、コメントさせていただきました。
Auth0にアクセストークンが有効かを確認すると図の中であるのですが、こちらどういった機能で達成されましたでしょうか。
お手すきの際にご返信いただけますと幸いです。
よろしくお願いいたします。

なぎなぎ
  1. accessTokenをbackendで渡す際に、backendは毎回auth0でaccessTokenが正しいかを確認します
  2. そこでaccessTokenが無効な場合は、backendからfrontendの方にその趣旨の内容を返し、それを受け取ったfrontendは、refreshTokenを使って、新たなaccessToken取得する流れになります。
    <参考>
    nest(ts)を使った実装
    https://zenn.dev/naginagi124/articles/f28fadec5a661d
    https://zenn.dev/naginagi124/articles/b24493a04d6ec4
    基本的には公開鍵と秘密鍵の関係で確認を行います
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';
import * as dotenv from 'dotenv';
dotenv.config();

@Injectable()
export class AuthGuard implements CanActivate {
  // 公開鍵が取得できるurlを指定 (jwtのkeyを渡さないと取得はできない。ユーザ一人に足して一つの公開鍵があるからkeyを渡す必要がある。)
  private client = jwksClient({
    jwksUri: `${process.env.AUTH0_ISSUER_URL}/.well-known/jwks.json`,
  });
  // canActivateはリクエストが進行する前に特定の条件が満たされているかどうかをチェックするために使用されます。
  canActivate(context: ExecutionContext): Promise<boolean> {
    // contextはリクエストの内容が入っている。
    const ctx = context.getArgs()[2]; // GraphQL context
    const request = ctx.req; // Direct access to GraphQL request object
    const authHeader = request.headers.authorization;
    if (!authHeader) throw new UnauthorizedException('No token provided');
    // bearという先頭の文字があるためそれを取り除く
    const token = authHeader.split(' ')[1];
    return this.validateToken(token).then((decoded) => {
      // Here you can use the user ID from decoded JWT
      const userId = decoded.sub;
      // You can attach the user ID to the request object if needed
      // requestにrequestのuserIdを追加する
      request.user = { userId };
      return true;
    });
  }

  // トークンの検証を行い、tokenの暗号化を解く
  private async validateToken(token: string): Promise<any> {
    // decodeメソッドを使用してトークンをデコード
    // complete: trueを指定すると、デコードされたトークンにヘッダー情報が含まれる
    // payloadはトークンのペイロード部分
    const decoded: any = jwt.decode(token, { complete: true });
    if (!decoded) throw new UnauthorizedException('Invalid token');
    const kid = decoded.header.kid;
    // keyIdを渡して自分に対応する公開鍵を取得
    const key = await this.client.getSigningKey(kid);
    const signingKey = key.getPublicKey();
    try {
      // tokenが改竄されていないかの確認 jwtと公開鍵、受け取りての確認、発行元の確認をしている
      jwt.verify(token, signingKey, {
        algorithms: ['RS256'],
        audience: process.env.AUTH0_AUDIENCE,
        issuer: `${process.env.AUTH0_ISSUER_URL}/`,
      });
      // payload部分開けを返す。
      return decoded.payload; // Return the decoded payload
    } catch (err) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}
マロンマロン

返信ありがとうございます。
実装についてもありがとうございます。
JWKSを使用したアクセストークンの検証で、有効かの確認をしていたんですね。
お答えいただき、ありがとうございました!