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,不是某个提供方的名称。
延伸阅读
实现细节见源文档。