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 配置:
authentication:
method: "oauth"
providers:
google:
client_id: ${GOOGLE_CLIENT_ID}
client_secret: ${GOOGLE_CLIENT_SECRET}
user_identity_field: "email"
secret_key: ${POTATO_SECRET_KEY}金鑰請放在環境變數裡,不要寫進配置檔案:
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 Cloud Console 中建立類型為 “Web application” 的 OAuth 2.0 憑據,並把 https://your-domain.com/auth/callback/google 新增為已授權的重定向 URI。本地開發時可以使用 http://localhost:8000/auth/callback/google。
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。
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 應用。
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,登入按鈕上的文字才易讀。
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 |
|---|---|
| Okta | https://your-org.okta.com/.well-known/openid-configuration |
| Azure AD | https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration |
| Auth0 | https://your-domain.auth0.com/.well-known/openid-configuration |
| Keycloak | https://keycloak.example.com/realms/{realm}/.well-known/openid-configuration |
scopes 預設為 ["openid", "email", "profile"]。
多個提供方
providers 可以容納任意數量的條目,每一個都會渲染出自己的登入按鈕。
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。
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 會取代本地登入。
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 雜湊。重置流程見密碼管理。
會話
會話時長是一個頂層鍵,單位為天:
session_lifetime_days: 7會話能在重啟後保留下來,靠的是一個固定不變的 secret_key。
管理員訪問
管理端點用 API 金鑰認證,而不是走 OAuth 塊:
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,提供方會拒絕它:
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.method | string | in_memory | in_memory、database、clerk 或 oauth |
authentication.providers | dict | oauth 下必填 | 每個提供方一個條目,以提供方名稱為鍵 |
authentication.user_identity_field | string | email | email、username、sub 或 name |
authentication.allow_local_login | bool | false | 在 SSO 按鈕旁顯示密碼錶單 |
authentication.auto_register | bool | true | 首次登入時建立 Potato 賬號 |
secret_key | string | 隨機 | 頂層鍵。會話要持久保留就需要一個固定值 |
session_lifetime_days | int | 7 | 頂層鍵,會話時長 |
admin_api_key | string | 自動生成 | 頂層鍵,用於管理端點 |
每個提供方的鍵:
| 鍵 | 類型 | 是否必填 | 說明 |
|---|---|---|---|
client_id | string | 是 | 來自提供方 |
client_secret | string | 是 | 來自提供方 |
discovery_url | string | 通用 OIDC 必填 | OIDC discovery 端點 |
display_name | string | 否 | 登入按鈕文字 |
allowed_domain | string | 否 | 限制為單個郵箱域名 |
allowed_org | string | 否 | 限制為單個 GitHub 組織 |
scopes | list | 否 | 預設為 ["openid", "email", "profile"] |
完整示例
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,不是某個提供方的名稱。
延伸閱讀
實現細節見源文件。