Skip to content

SSOとOAuth認証

PotatoでGoogle、GitHub、HuggingFace、汎用OIDCによるログインを設定します。メールドメインやGitHub組織でアクセスを制限し、SSOとローカルのパスワードログインを併用できます。

v2.3.0の新機能

デフォルトでは、Potatoはシンプルなユーザー名ベースのログインを使用し、アノテーターは任意のユーザー名を入力してサインインします。これはローカル開発や小規模プロジェクトには十分ですが、本番環境のデプロイメントでは、不正アクセスの防止、アノテーターの身元確認、組織の要件への準拠のために適切な認証が必要です。

SSOの設定は1か所にまとまっています。authentication.methodをoauthに設定し、authentication.providersの下に1つ以上のプロバイダーを列挙します。各プロバイダーには、ログインページ上に専用のボタンが表示されます。

Potatoには、google、github、huggingfaceの3つのプロバイダーについてエンドポイントのメタデータが組み込まれています。それ以外のプロバイダー名は汎用OIDCとして扱われ、discovery_urlが必要です。

クイックスタート

最小構成のGoogle設定:

yaml
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
  user_identity_field: "email"
 
secret_key: ${POTATO_SECRET_KEY}

シークレットはファイルではなく環境変数で設定します:

bash
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET="your-client-secret"
export POTATO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"

secret_keyはトップレベルのキーであり、authenticationブロックには含まれません。これを設定するか、POTATO_SECRET_KEYを設定してください。どちらもない場合、Potatoはランダムなキーを生成するため、再起動のたびにすべてのセッションが無効になります。また、これを設定せずにOAuthを構成するとPotatoは警告をログに出力します。

PotatoはOAuth用に2つのルートを提供します。/auth/login/<provider>がフローを開始し、/auth/callback/<provider>がレスポンスを受け取ります。コールバックはプロバイダーに登録するURLなので、Googleの場合はhttps://your-domain.com/auth/callback/googleです。redirect_uriという設定キーはなく、ルートはプロバイダー名から決まります。

Google

Google Cloud Consoleで種類が"Web application"のOAuth 2.0認証情報を作成し、承認済みリダイレクトURIとしてhttps://your-domain.com/auth/callback/googleを追加します。ローカル開発ではhttp://localhost:8000/auth/callback/googleも受け付けられます。

yaml
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
      allowed_domain: "umich.edu"

allowed_domainは単一のドメイン文字列を取ります。メールアドレスがそのドメインに属するアカウントだけがサインインできます。

GitHub

GitHub Settings > Developer settings > OAuth AppsでOAuthアプリを登録し、コールバックURLをhttps://your-domain.com/auth/callback/githubに設定します。

yaml
authentication:
  method: "oauth"
  providers:
    github:
      client_id: ${GITHUB_CLIENT_ID}
      client_secret: ${GITHUB_CLIENT_SECRET}
      allowed_org: "my-research-lab"

allowed_orgは単一の組織名を取ります。これを設定するとPotatoはread:orgスコープを自動的に要求するため、スコープを自分で列挙する必要はありません。チェックが所属を確認できるよう、アノテーターは組織のメンバーシップを公開にしておく必要があります。

HuggingFace

huggingface.co/settings/applicationsでOAuthアプリケーションを作成します。

yaml
authentication:
  method: "oauth"
  providers:
    huggingface:
      client_id: ${HF_CLIENT_ID}
      client_secret: ${HF_CLIENT_SECRET}
  user_identity_field: "username"

HuggingFaceプロバイダーはHuggingFaceのOIDCディスカバリーエンドポイントに接続し、デフォルトでopenid profile emailを要求します。

汎用OIDC

google、github、huggingface以外のプロバイダー名は汎用OIDCとして扱われ、discovery_urlが必要です。ログインボタンの表記がわかりやすくなるよう、display_nameを指定してください。

yaml
authentication:
  method: "oauth"
  providers:
    oidc:
      display_name: "University SSO"
      discovery_url: "https://sso.university.edu/.well-known/openid-configuration"
      client_id: ${OIDC_CLIENT_ID}
      client_secret: ${OIDC_CLIENT_SECRET}
      scopes:
        - "openid"
        - "email"
        - "profile"

プロバイダーに登録するコールバックURLはプロバイダーキーに従うため、oidcという名前のブロックではhttps://your-domain.com/auth/callback/oidcを使います。

主なプロバイダーのディスカバリーURL:

プロバイダーディスカバリーURL
Oktahttps://your-org.okta.com/.well-known/openid-configuration
Azure ADhttps://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration
Auth0https://your-domain.auth0.com/.well-known/openid-configuration
Keycloakhttps://keycloak.example.com/realms/{realm}/.well-known/openid-configuration

scopesのデフォルトは["openid", "email", "profile"]です。

複数のプロバイダー

providersには必要なだけエントリを追加でき、それぞれに専用のログインボタンが表示されます。

yaml
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
    github:
      client_id: ${GITHUB_CLIENT_ID}
      client_secret: ${GITHUB_CLIENT_SECRET}
    oidc:
      display_name: "University SSO"
      discovery_url: "https://sso.university.edu/.well-known/openid-configuration"
      client_id: ${OIDC_CLIENT_ID}
      client_secret: ${OIDC_CLIENT_SECRET}

user_identity_field: "email"を指定すると、Googleでサインインし、後に同じアドレスで大学のプロバイダーからサインインした人は同じPotatoユーザーとなり、同じ割り当てに戻ります。

アイデンティティと自動登録

user_identity_fieldは、プロバイダーのどのクレームをPotatoのユーザー名にするかを決めます。email、username、sub、nameのいずれかを受け付け、デフォルトはemailです。アノテーターの出力ディレクトリはユーザー名にちなんで命名されるため、この値は重要です。研究の途中で変更すると、アノテーターがそれまでの作業から切り離されます。

auto_registerは、初回のログイン成功時にPotatoアカウントを作成するかどうかを制御します。デフォルトはtrueです。

yaml
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
      allowed_domain: "umich.edu"
  user_identity_field: "email"
  auto_register: true

アクセス制御は、設定ファイル内のアドレスリストではなくプロバイダーを通じて行います。サインインできるユーザーを制限するには、Googleではallowed_domain、GitHubではallowed_org、あるいはOIDCプロバイダー側のグループやアプリ割り当ての機能を使います。

混合モード

allow_local_loginを有効にすると、SSOボタンの下にユーザー名とパスワードのフォームが追加されます。デフォルトはfalseなので、有効にしない限りSSOがローカルログインに置き換わります。

yaml
require_password: true
 
user_config:
  allow_all_users: true
 
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
  allow_local_login: true

移行時にはしばらく両方を併用します。アノテーターをプロバイダーへ移行させ、全員がプロバイダー経由でサインインしたらallow_local_loginを削除します。混合モードは、組織内のアノテーターと、あなたの組織にアカウントを持たない外部のアノテーターを組み合わせる研究にも使えます。

混合モードのローカルアカウントは、他のパスワードアカウントと同じソルト付きPBKDF2ハッシュを使います。リセット手順についてはパスワード管理を参照してください。

セッション

セッションの有効期間はトップレベルのキーで、単位は日です:

yaml
session_lifetime_days: 7

再起動後もセッションが維持されるのは、secret_keyが固定されている場合です。

管理者アクセス

管理用エンドポイントは、OAuthブロックではなくAPIキーで認証します:

yaml
admin_api_key: ${POTATO_ADMIN_API_KEY}

Potatoはadmin_api_keyをまず設定ファイルから、次に環境変数POTATO_ADMIN_API_KEYから読み取ります。どちらも設定されていない場合はキーを生成し、タスクディレクトリ内のadmin_api_key.txtに書き込みます。呼び出し側はX-API-Keyヘッダーでこのキーを送ります。

HTTPS

本番環境では、OAuthプロバイダーはHTTPSのリダイレクトURIを要求します。localhostは例外なので、HTTPでの開発は証明書なしで動作します。

リバースプロキシの背後では元のスキームを転送してください。転送しないと、Potatoが組み立てるコールバックURLがhttpになり、プロバイダーに拒否されます:

nginx
location / {
    proxy_pass http://localhost:8000;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
}

設定リファレンス

キー型デフォルト説明
authentication.methodstringin_memoryin_memory、database、clerk、oauthのいずれか
authentication.providersdictoauthでは必須プロバイダーごとに1エントリ、プロバイダー名をキーとする
authentication.user_identity_fieldstringemailemail、username、sub、nameのいずれか
authentication.allow_local_loginboolfalseSSOボタンと並べてパスワードフォームを表示する
authentication.auto_registerbooltrue初回ログイン時にPotatoアカウントを作成する
secret_keystringランダムトップレベル。セッションを永続化するには固定値が必要
session_lifetime_daysint7トップレベルのセッション期間
admin_api_keystring自動生成管理用エンドポイントのためのトップレベルのキー

プロバイダーごとのキー:

キー型必須説明
client_idstringはいプロバイダーから取得
client_secretstringはいプロバイダーから取得
discovery_urlstring汎用OIDCでは必須OIDCディスカバリーエンドポイント
display_namestringいいえログインボタンのテキスト
allowed_domainstringいいえ1つのメールドメインに制限する
allowed_orgstringいいえ1つのGitHub組織に制限する
scopeslistいいえデフォルトは["openid", "email", "profile"]

完全な例

yaml
annotation_task_name: "Annotation Project"
task_dir: "."
 
authentication:
  method: "oauth"
  providers:
    google:
      client_id: ${GOOGLE_CLIENT_ID}
      client_secret: ${GOOGLE_CLIENT_SECRET}
      allowed_domain: "umich.edu"
  user_identity_field: "email"
  auto_register: true
 
secret_key: ${POTATO_SECRET_KEY}
session_lifetime_days: 7
admin_api_key: ${POTATO_ADMIN_API_KEY}
 
data_files:
  - "data/instances.jsonl"
 
item_properties:
  id_key: id
  text_key: text
 
annotation_schemes:
  - annotation_type: radio
    name: sentiment
    description: "What is the sentiment of this text?"
    labels: [Positive, Neutral, Negative]
 
output_annotation_dir: "output/"
export_annotation_format: "jsonl"

トラブルシューティング

プロバイダーがリダイレクトURIを拒否する。 プロバイダーに登録したURIは/auth/callback/<provider>と完全に一致する必要があります。スキームや末尾のパスも照合の対象になるため、X-Forwarded-Protoを転送しないプロキシではhttpのコールバックが生成され、プロバイダーに拒否されます。

再起動後に全員がログアウトされる。 secret_keyが設定されていないため、Potatoが起動時に新しいキーを生成しています。

GitHub組織による制限でメンバーが拒否される。 そのメンバーの組織への所属が非公開になっています。本人が公開にするか、allowed_orgを外して別の方法でアクセスを制御してください。

検証が"providers is required"で失敗する。 method: "oauth"にはprovidersの下に少なくとも1つのエントリが必要です。methodの値はoauthであり、プロバイダー名ではありません。


参考資料

実装の詳細については、ソースドキュメントを参照してください。