Open38

認証と認可のまとめ

nassynassy

ID トークンに含まれる情報により、ユーザーが主張する通りの人物であることをクライアントで確認できます。 ID トークンは、サードパーティのアプリケーションで理解されることを意図しています。 ID トークンは承認目的では使用すべきではありません。 承認には、アクセス トークンを使用します。

https://docs.microsoft.com/ja-jp/azure/active-directory/develop/id-tokens

nassynassy

Microsoft ID プラットフォームでは、OAuth 2.0 の仕様で説明されているように、OAuth 2.0 の暗黙的な許可フローがサポートされています。

OAuth2 の暗黙的な許可の適切なシナリオ
暗黙的な許可は、サード パーティの Cookie がなくてもアプリケーションに影響を与えないサインイン フローの最初の対話的な部分でしか信頼できません。 つまり、アプリケーションが承認エンドポイントからのトークンとコードを要求するハイブリッド フローの一部としてのみこの許可を使用すべきです。 そうすることで、アプリケーションは更新トークンと交換可能なコードを受け取ることができるため、アプリケーションのログイン セッションは時間が経過しても有効なままになります。

"暗黙的フロー"の説
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-implicit-grant-flow?WT.mc_id=Portal-Microsoft_AAD_RegisteredApps

nassynassy

scope の実装も可能であるっぽい
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/openid-connect

OpenID Connect は、OAuth 2.0 の "認可" プロトコルを "認証" プロトコルとして使用できるように拡張したものです。 この認証プロトコルでは、シングル サインオンを実行できます。 ここでは、クライアントがユーザーの ID を検証したり、ユーザーに関する基本的なプロファイル情報を取得したりできるようにする "ID トークン" の概念が導入されています。

nassynassy

OAuth 2.0 の Scope にアクセスできるリソースやアクセス権の範囲を記載する具体例を教えてください

ChatGPT
OAuth 2.0のScopeは、クライアントアプリケーションがリソースサーバーに対して要求するアクセス権の範囲を示すために使用されます。以下に具体的な例を示します。

プロフィール情報へのアクセス:
Scope: profile
説明: ユーザーの基本的なプロフィール情報(氏名、メールアドレス、プロフィール写真など)にアクセスする権限を示します。

電子メールアドレスへのアクセス:
Scope: email
説明: ユーザーの登録されたメールアドレスにアクセスする権限を示します。

連絡先情報へのアクセス:
Scope: contacts.read
説明: ユーザーの連絡先情報(アドレス帳)への読み取りアクセス権を示します。

カレンダーへのアクセス:
Scope: calendar.read, calendar.write
説明: ユーザーのカレンダー情報への読み取りおよび書き込みアクセス権を示します。

ソーシャルメディアの投稿へのアクセス:
Scope: socialmedia.posts
説明: ユーザーのソーシャルメディアアカウントでの投稿へのアクセス権を示します。

ファイルへのアクセス:
Scope: files.read, files.write
説明: ユーザーのファイルへの読み取りおよび書き込みアクセス権を示します。

これらは一般的な例であり、実際のアプリケーションに応じて独自のScopeを定義することも可能です。リソースサーバーは、クライアントアプリケーションが要求するScopeに基づいて、ユーザーのアクセス権を制御します。

nassynassy

OAuth と OpenID Connect:このプラットフォームでは、OAuth が承認に使用され、OpenID Connect (OIDC) が認証に使用されます。 OpenID Connect は OAuth 2.0 に基づいて構築されているため、これら 2 つの間では用語とフローが似ています。 さらに、(OpenID Connect を使用して) ユーザーを認証することと、(OAuth 2.0 を使用して) ユーザーが所有する保護されたリソースにアクセスするための承認を得ることを、1 回の要求で行うこともできます。

https://learn.microsoft.com/ja-jp/azure/active-directory/develop/authentication-vs-authorization?bc=%2Fazure%2Factive-directory-b2c%2Fbread%2Ftoc.json&toc=%2Fazure%2Factive-directory-b2c%2FTOC.json

nassynassy

AzureAD で RBAC な アクセス制御は難しそうだったので、Auth0で考える。

nassynassy

AzureAD で RBAC な アクセス制御は難しそうだったので、Auth0で考える。

なかなかのお値段だった

nassynassy

これを元に実装する

SPA のサポートには、OAuth 2.0 認証コード フロー (PKCE あり) の方法が推奨されます。

OAuth 2.0 認証コード フロー (PKCE あり) = OAuth 2.0 承認コード フロー

https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/configure-authentication-sample-spa-app
https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/a81fcef3d82523e03828d91bb0ee8d2ab2cc20d8/samples/msal-react-samples/typescript-sample/src
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/authorization-code-flow
https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/enable-authentication-react-spa-app
https://techblog.ap-com.co.jp/entry/2022/10/21/095727
https://qiita.com/Futo_Horio/items/14c3a9516e62ebd392e7
https://speakerdeck.com/takas0522/msal-dot-jsfalsearekore?slide=3

認可まで進んだ
https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/blob/main/3-Authorization-II/2-call-api-b2c/README.md

msal の使い方はこのあたりに記述がある
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md

  • MSAL Python is a token acquisition and caching library, and not a token validation library
nassynassy

注意:
セルフサービス パスワード リセット にチェックを付けておかない、画面でforgotpasswordを押した際にエラーになる。

nassynassy

https://<アプリ名>.b2clogin.com/<アプリ名>.onmicrosoft.com/B2C_1_sign_in/api/CombinedSigninAndSignup/confirme

の中に Authorization Code が入っている。

nassynassy

https://pypi.org/project/azure-ad-verify-token/

例外の取り回しが良くないけれど、なんとか実装できる感じ。

    try:
        return verify_jwt(
            token=token,
            valid_audiences=[az_ad_auth_config.app_id],
            issuer=az_ad_auth_config.ad_issuer,
            jwks_uri=az_ad_auth_config.jwks_uri,
            verify=True,
        )
    except InvalidAuthorizationToken as e:
        logger.error("Token validation error: %s", e, exc_info=True)
        if "ExpiredSignatureError" in str(e):
            raise HTTPException(status_code=401, detail="Token is expired")
nassynassy

実装途中だけど、自前で作るならこんな感じ

import os
import requests
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2AuthorizationCodeBearer
import jwt
from jwt.algorithms import RSAAlgorithm
from pydantic import BaseModel
import logging

logger = logging.getLogger(__name__)

# 環境変数から設定を読み込む
TENANT_ID = os.getenv("AZURE_TENANT_ID")
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
APP_NAME = os.getenv("APP_NAME")

JWKS_URL = f"https://{APP_NAME}.b2clogin.com/{APP_NAME}.onmicrosoft.com/b2c_1_sign_in/discovery/v2.0/keys"
ISSUER = f"https://{APP_NAME}.b2clogin.com/{TENANT_ID}/v2.0/"

oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl=f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize",
    tokenUrl=f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
)

class TokenPayload(BaseModel):
    sub: str
    scp: str

def get_public_key(token):
    header = jwt.get_unverified_header(token)
    kid = header["kid"]
    logger.debug(f"{kid=}")
    response = requests.get(JWKS_URL)
    jwks = response.json()
    for key in jwks["keys"]:
        logger.debug(f"{key['kid']=}")
        if key["kid"] == kid:
            return RSAAlgorithm.from_jwk(key)
    raise HTTPException(status_code=401, detail="Public key not found")

def validate_token(token: str = Depends(oauth2_scheme)) -> TokenPayload:
    public_key = get_public_key(token)
    try:
        payload = jwt.decode(token, public_key, algorithms=["RS256"], audience=CLIENT_ID, issuer=ISSUER)
        return TokenPayload(**payload)
    except Exception as e:
        logger.error("Token validation error: %s", e, exc_info=True)
        raise HTTPException(status_code=401, detail="Invalid token")

def require_scope(required_scope: str):
    def decorator(payload: TokenPayload = Depends(validate_token)):
        if required_scope not in payload.scp.split():
            raise HTTPException(status_code=403, detail="Forbidden")
        return payload
    return decorator

nassynassy

Azure AD B2C OpenID Connect メタデータ ドキュメント を実行して、
ISSUER や JWKS_URI を取得する

nassynassy