Skip to content

SSO 與 OAuth 認證

在 Potato 中配置 Google、GitHub、HuggingFace 和通用 OIDC 登入。按郵箱域名或 GitHub 組織限制訪問,並讓 SSO 與本地密碼登入並行執行。

v2.3.0 新增

預設情況下,Potato 使用基於使用者名稱的簡單登入方式,標註者輸入任意使用者名稱即可登入。這對本地開發和小型項目夠用,但生產部署需要正規的認證來阻止未授權訪問、確認標註者身份,並滿足所在機構的要求。

SSO 在一處配置。把 authentication.method 設為 oauth,然後在 authentication.providers 下列出一個或多個提供方。每個提供方在登入頁上都有自己的按鈕。

Potato 內建了三個提供方的端點資訊:google、github 和 huggingface。其他任何提供方名稱都按通用 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 路由:/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 憑據,並把 https://your-domain.com/auth/callback/google 新增為已授權的重定向 URI。本地開發時可以使用 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 許可權範圍,所以你不需要自己列出 scopes。標註者必須是該組織的公開成員,這項檢查才能看到其成員身份。

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 discovery 端點,預設請求 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。

常見提供方的 discovery URL:

提供方Discovery 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。

管理員訪問

管理端點用 API 金鑰認證,而不是走 OAuth 塊:

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 提供方要求重定向 URI 使用 HTTPS。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 下必填每個提供方一個條目,以提供方名稱為鍵
authentication.user_identity_fieldstringemailemail、username、sub 或 name
authentication.allow_local_loginboolfalse在 SSO 按鈕旁顯示密碼錶單
authentication.auto_registerbooltrue首次登入時建立 Potato 賬號
secret_keystring隨機頂層鍵。會話要持久保留就需要一個固定值
session_lifetime_daysint7頂層鍵,會話時長
admin_api_keystring自動生成頂層鍵,用於管理端點

每個提供方的鍵:

鍵類型是否必填說明
client_idstring是來自提供方
client_secretstring是來自提供方
discovery_urlstring通用 OIDC 必填OIDC discovery 端點
display_namestring否登入按鈕文字
allowed_domainstring否限制為單個郵箱域名
allowed_orgstring否限制為單個 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 下至少有一個條目。method 的取值是 oauth,不是某個提供方的名稱。


延伸閱讀

實現細節見源文件。