跳到主内容

OAuth 2.0

Casdoor会颁发访问令牌以对客户端进行身份验证。本页面介绍如何通过API获取令牌、验证令牌并使用它。或者,使用Casdoor SDK来处理该流程。

支持的授权类型:

授权类型RFC用例
授权码RFC 6749 §4.1默认;带有后端的Web/移动应用。默认启用。
t隐式RFC 6749 §4.2仅前端应用,无后端。
资源所有者密码RFC 6749 §4.3无前端重定向的应用程序;用户凭据直接发送。
客户端凭证RFC 6749 §4.4服务间调用,无需用户参与。
刷新令牌RFC 6749 §6无需重新认证即可续订访问令牌。
设备授权RFC 8628输入受限或无浏览器的设备
令牌交换RFC 8693用现有令牌交换具有不同范围或受众的令牌。
JWT持有者RFC 7523使用签名的JWT断言而非客户端密钥进行服务身份验证。
Verification CodeCasdoor extensionNative apps that sign users in (and up) with a code sent by SMS or email.

在应用程序编辑页面上启用非默认授权类型。

OAuth授权类型

授权码许可​

将用户重定向至:

https://<CASDOOR_HOST>/login/oauth/authorize?
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=openid&
state=STATE

作用域​

作用域描述
openid(默认)sub、iss、aud
profilename、displayName、avatar
email电子邮件地址
address地址(OIDC 对象在JWT 标准中;请参阅OIDC 地址声明)
phone电话号码

:::信息 授权 URL 中的请求范围多个 scope 之间用 %20 分隔:

https://<CASDOOR_HOST>/login/oauth/authorize?
client_id=...&
scope=openid%20email

请参阅OIDC 规范以了解详情。 :::

用户登录后,Casdoor 将重定向至:

https://REDIRECT_URI?code=CODE&state=STATE

使用 POST 将代码兑换为令牌:

https://<CASDOOR_HOST>/api/login/oauth/access_token

请求体:

{
"grant_type": "authorization_code",
"client_id": ClientId,
"client_secret": ClientSecret,
"code": Code,
}

示例响应:

{
"access_token": "eyJhb...",
"id_token": "eyJhb...",
"refresh_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid"
}
备注

Casdoor支持PKCE(代码交换证明密钥),以增强安全性。要启用PKCE,请在请求授权码时添加两个参数:

&code_challenge_method=S256&code_challenge=YOUR_CHALLENGE

代码挑战应为随机生成的代码验证器的Base64-URL编码SHA-256哈希值(长度为43至128个字符)。请求令牌时,带上原始的 code_verifier 参数。启用 PKCE 后,client_secret 变为可选,但如果传了,就必须正确。

对于在Casdoor中配置的OAuth提供商(如Twitter以及启用了PKCE的自定义提供商),Casdoor会为每个认证流程自动生成唯一的代码验证器,因此您无需手动实现PKCE。

将令牌绑定到特定服务​

当您的应用程序需要调用多个后端服务时,您可能希望使用明确绑定到特定服务的令牌。这可防止出现安全问题,即本应用于某项服务的令牌意外地被用于另一项服务。

Casdoor 支持 RFC 8707 资源指示符,允许您在请求授权时指定预期的服务。加上 resource 参数,值为标识你的服务的绝对 URI:

https://<CASDOOR_HOST>/login/oauth/authorize?
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=openid&
state=STATE&
resource=https://api.example.com

用授权码换取令牌时,带上同样的 resource 参数:

{
"grant_type": "authorization_code",
"client_id": ClientId,
"client_secret": ClientSecret,
"code": Code,
"resource": "https://api.example.com"
}

得到的访问令牌中,aud(受众)声明会是你的资源 URI,而不是客户端 ID。您的后端服务随后可以通过检查受众声明来验证令牌是否专门为其颁发。资源必须在授权请求和令牌请求之间完全匹配。

在基于浏览器的登录流程中,resource 参数会一直保留。如果用户需要完成交互式登录(例如输入密码、MFA 或 WebAuthn),该参数将贯穿整个重定向链,并在颁发授权码时一并包含。

provider_hint参数​

要跳过Casdoor登录页面,直接将用户发送到特定的OAuth提供商,请在授权URL中添加provider_hint=<provider-name>:

https://<CASDOOR_HOST>/login/oauth/authorize?
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=openid&
state=STATE&
provider_hint=github

Casdoor 提供一个轻量级的重定向页面(无需加载完整的 React 应用),可立即将用户引导至该提供商的 OAuth 流程。这可缩短在设备资源受限或网络连接较慢时的重定向时间。

带有 OAuth 的注册流程​

当用户通过 OAuth 授权流程注册时,他们会自动被重定向到您的应用程序的回调 URL,并附带授权代码,这与登录流程完全相同。此前,用户在创建账户后必须手动点击进入中间页面。现在注册流程和登录一样顺畅:注册完成后,Casdoor 会立即生成授权码并跳转到你的 redirect_uri。

<b您的应用程序无需任何更改即可支持此功能。在 OAuth 授权过程中,如果用户选择注册新账号,授权参数(client_id、response_type、redirect_uri 等)会自动带入注册流程。

隐式授权​

对于没有后端的应用程序,请使用隐式授权. 在应用程序中启用它,然后将用户重定向到:

:::警告

隐式授权的令牌接口需要有效的 username 和 password。纯 OAuth 用户(只通过第三方提供商创建、没有设置本地密码的账号)不能使用这个流程,会收到 invalid_grant。对社交登录用户使用授权码流程。

:::

https://<CASDOOR_HOST>/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=token&scope=openid&state=STATE

当您的用户通过 Casdoor 身份验证后,他会被 Casdoor 重定到:

https://REDIRECT_URI/#access_token=ACCESS_TOKEN

Casdoor也支持id_token作为response_type,这是OpenID的一个特性。

设备授权​

对于输入受限或无浏览器的设备,请使用设备授权. 在应用上启用它,从 OIDC 发现文档中获取 device_authorization_endpoint 并请求,然后展示 verification_uri(例如二维码或文字),让用户完成登录。

Casdoor 在 verification_uri 提供内置的设备登录页,用户在这里输入用户码并登录,无需自己做界面。设备登录也可以作为一种登录方式,在应用的 Signin methods 表中启用。

Meanwhile, the device polls the token endpoint with its device_code until the user has signed in, as described in RFC 8628, section 3.4.

Scanning the website's QR code with your app​

The same flow lets users who are signed in to your native app sign in to your website by scanning a QR code, the way many apps do. Add Device login to the application's Signin methods with the rule Login page, and enable Device Code in Grant types. The login page then shows a QR code next to the form and finishes the sign-in by itself once it is approved.

The QR code holds the verification_uri, for example https://<CASDOOR_HOST>/login/oauth/device/ra91hy?cancelToken=.... A phone camera opens it in the browser, where the user approves after signing in. Your app can instead approve it directly with the access token it already has: take the user code from the path (ra91hy), show the user what is being signed in to, and send a POST request to https://<CASDOOR_HOST>/api/login with the header Authorization: Bearer <ACCESS_TOKEN>:

{
"application": ApplicationName,
"organization": OrganizationName,
"type": "device",
"userCode": "ra91hy"
}

To reject the sign-in, send userCode and the cancelToken from the same URL to https://<CASDOOR_HOST>/api/cancel-device-auth as query parameters.

多副本部署

待处理的设备授权请求(设备码和用户码之间的映射)默认保存在内存中。在多副本 / 水平扩展的部署中,设备的轮询请求和浏览器的确认可能落到不同副本上,内存存储会导致设备流程时好时坏。

要让设备流程跨副本工作,在 conf/app.conf 中配置 redisEndpoint。设置后,Casdoor 会自动用 Redis 存储设备授权数据,所有副本共享同一份状态。不需要额外的配置项,复用共享会话所用的同一个 redisEndpoint 值即可(格式:host:port[,db[,password]])。如果启动时连不上 Redis,Casdoor 会记录一条警告并退回内存存储。见 配置。

使用资源拥有者的密码凭据授权​

如果您的应用程序没有前端来重定向用户到Casdoor,那么您可能需要这个功能。

在应用程序上启用密码凭据授权,然后向以下地址发送 POST 请求:

https://<CASDOOR_HOST>/api/login/oauth/access_token
{
"grant_type": "password",
"client_id": ClientId,
"client_secret": ClientSecret,
"username": Username,
"password": Password,
}

示例响应:

{
"access_token": "eyJhb...",
"id_token": "eyJhb...",
"refresh_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid"
}

Verification Code Grant​

Native apps that sign users in with a phone number or email and a one-time code, without opening a browser, use this grant. It is a Casdoor extension grant (RFC 6749 section 4.5). An address that has no account yet is signed up in the same step when the application allows it.

Enable Verification Code in the application's Grant types and add an SMS and/or email provider to the application. Then:

  1. Send the code with a POST request to https://<CASDOOR_HOST>/api/send-verification-code (form data):

    FieldValue
    applicationIdadmin/<APPLICATION_NAME>
    typephone or email
    destThe phone number or email
    countryCodeThe region of a phone number, e.g. CN or US. Not needed for an E.164 number like +8613800000000
    methodlogin
    captchaTypenone, or the captcha type and captchaToken when the application's captcha provider asks for one
  2. Exchange the code for tokens with a POST request to https://<CASDOOR_HOST>/api/login/oauth/access_token:

    {
    "grant_type": "urn:casdoor:params:oauth:grant-type:verification-code",
    "client_id": ClientId,
    "username": "+8613800000000",
    "code": "123456",
    "scope": "openid profile"
    }

    username is the phone number or email the code was sent to. For a phone number in the national format, also send country_code. No client secret is needed, so the grant works from a public client. The response is the same as for the other grants, with a refresh_token to keep the user signed in.

Signing up new users: when no user has the phone number or email, the grant creates the user, the same as the signup page would, if all of these hold:

  • Enable signup is on for the application, and Disable self signup is off.
  • The application's Signup items require nothing but what the code proves: an item other than ID, Username, Display name, Password, Confirm password, Agreement, Signup button and Providers can't be required, and Email (or Phone) can only be required when signing up with an email (or a phone number). The default signup items require both, so make the other one optional.
  • For a phone number, its region is in the organization's Supported country codes.

Otherwise /api/send-verification-code answers that the user does not exist, the same as before, and only existing users can sign in. Wrong codes count towards the application's Failed signin limit. Users who have MFA enabled are refused, use the authorization code flow for them.

使用客户端凭据授权​

当应用程序没有前端时,请使用客户端凭据授权。

在应用程序上启用客户端凭证授权,并发送 POST 请求至https://<CASDOOR_HOST>/api/login/oauth/access_token:

{
"grant_type": "client_credentials",
"client_id": ClientId,
"client_secret": ClientSecret,
}

示例响应:

{
"access_token": "eyJhb...",
"id_token": "eyJhb...",
"refresh_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid"
}

必须指出,以这种方式获得的AccessToken 不同于前三个,因为它与应用程序相对应,而不是与用户相对应。

刷新令牌​

要刷新访问令牌,使用上面拿到的 refreshToken。

在应用程序中设置刷新令牌的过期时间(默认为0小时),然后向https://<CASDOOR_HOST>发送POST请求/api/login/oauth/refresh_token

{
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
"scope": SCOPE,
"client_id": ClientId,
"client_secret": ClientSecret,
}

Example response:

{
"access_token": "eyJhb...",
"id_token": "eyJhb...",
"refresh_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid"
}

Rotation: by default every refresh returns a new refresh token and revokes the one that was used, as recommended for public clients by RFC 9700. Store the new one after each refresh. Presenting a used refresh token again fails with invalid_grant.

When several processes share one refresh token, e.g. a CLI that runs parallel jobs with the token from an environment variable, the first refresh revokes it for all the others. For such clients turn on Disable refresh token rotation in the application's OIDC/OAuth settings. A refresh then returns the same refresh token, which keeps its original expiry, and the access tokens of earlier refreshes stay valid until they expire. Signing out still revokes the refresh token for everyone. This is less secure, a leaked refresh token can be used until it expires, so keep Refresh token expire short and prefer DPoP for public clients.

Renaming a user doesn't break the user's refresh tokens.

令牌交换授权​

令牌交换(RFC 8693)可让您用现有令牌兑换具有不同特性的新令牌——这在以下情况下尤为有用:当一项服务需要代表用户调用另一项服务,或为特定下游服务缩小令牌的作用范围时。

要交换令牌,请向以下地址发送POST请求https://<CASDOOR_HOST>/api/login/oauth/access_token:

{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"client_id": ClientId,
"client_secret": ClientSecret,
"subject_token": SubjectToken,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"scope": "openid email"
}

subject_token 是要交换的令牌,通常是你已经持有的访问令牌或 JWT。如果想缩小新令牌的权限,可以指定一个 scope,取原令牌 scope 的子集。当你省略scope时,新令牌将继承与主体令牌相同的范围。

Casdoor 支持三种令牌类型用于subject_token_type:

  • urn:ietf:params:oauth:token-type:access_token (default)
  • urn:ietf:params:oauth:token-type:jwt
  • urn:ietf:params:oauth:token-type:id_token

该响应会返回一个与您的主体令牌关联的全新令牌:

{
"access_token": "eyJhb...",
"id_token": "eyJhb...",
"refresh_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid email"
}

例如,API网关可能会在将请求转发给下游微服务之前,用范围更广的访问令牌换取范围更窄的访问令牌。这种模式称为范围向下作用域,可确保每个服务仅获得其所需的权限,而不会继承原始令牌的全部访问权限。

**受众绑定:**Casdoor 用签发 subject_token 的应用(由 azp 声明标识)的证书来校验它,而不是用请求方客户端的证书。如果请求方客户端不是令牌的签发方,令牌的 aud 声明必须包含请求方客户端的 ID,否则交换会被拒绝并返回 invalid_grant。这可防止某个应用程序在未获得明确受众授权的情况下交换另一个应用程序的令牌(RFC 8693 §2.1)。

JWT Bearer 授权​

JWT 持有者(RFC 7523)允许客户端通过呈现经过签名的 JWT 断言来获取访问令牌,而非使用客户端密钥。这对于服务间调用非常有用,其中客户端持有私钥,并希望在不共享长期密钥的情况下进行身份验证。

在应用上启用 JWT Bearer,然后在 Security 选项卡的 Client cert 字段上传客户端证书。 Casdoor 使用该证书中的公钥来验证 JWT 断言。

向https://<CASDOOR_HOST>/api/login/oauth/access_token:

{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": "<signed-JWT>",
"client_id": "CLIENT_ID"
}

JWT 断言(client_assertion) 必须使用客户端的私钥进行签名,并包含:

声明描述
iss签发者:应用的 client_id
sub主体:应用的 client_id
aud受众 — Casdoor 令牌端点 URL
exp过期时间(Unix 时间戳)

Casdoor 会使用应用程序的客户端证书中的公钥对签名进行验证,检查标准 JWT 声明;如果验证通过,则返回与该应用程序关联的访问令牌(行为与客户端凭据授权相同)。

示例响应:

{
"access_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 10080,
"scope": "openid"
}

DPoP(绑定发送方的令牌)​

Casdoor 支持 DPoP(Demonstrating Proof of Possession,RFC 9449),它把访问令牌绑定到客户端持有的密钥上,令牌即使泄露,没有对应的私钥也无法使用。

要申请绑定 DPoP 的令牌,在令牌请求中带上 DPoP 请求头,值为 DPoP 证明 JWT:

POST /api/login/oauth/access_token
DPoP: <DPoP proof JWT>

提供了有效的证明时,Casdoor 会把签发的令牌绑定到证明中的公钥(其 JWK 指纹 jkt),并返回 token_type 为 DPoP 而不是 Bearer:

{
"access_token": "eyJhb...",
"token_type": "DPoP",
"expires_in": 10080,
"scope": "openid"
}

刷新令牌请求同样接受 DPoP 请求头。无效的证明会被拒绝,返回 invalid_dpop_proof 错误。

支持的证明签名算法列在 OpenID Connect 发现文档(/.well-known/openid-configuration)的 dpop_signing_alg_values_supported 中:

{
"dpop_signing_alg_values_supported": ["RS256", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"]
}

令牌自省 的响应也会通过 cnf.jkt 声明暴露密钥绑定(RFC 9449 §8)。

如何验证访问令牌​

Casdoor 目前支持 令牌自省 端点。此端点受基本身份验证保护(ClientId:ClientSecret)。

POST /api/login/oauth/introspect HTTP/1.1
Host: CASDOOR_HOST
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=

token=ACCESS_TOKEN&token_type_hint=access_token

示例响应:

{
"active": true,
"client_id": "c58c...",
"username": "admin",
"token_type": "Bearer",
"exp": 1647138242,
"iat": 1646533442,
"nbf": 1646533442,
"sub": "7a6b4a8a-b731-48da-bc44-36ae27338817",
"aud": [
"c58c..."
],
"iss": "http://localhost:8000"
}

如何使用AccessToken​

使用访问令牌调用需要身份验证的Casdoor API。

例如,有两种不同的方式来请求/api/userinfo。

类型1:查询参数

https://CASDOOR_HOST/api/userinfo?accessToken=your_access_token

类型2:HTTP Bearer 令牌

https://CASDOOR_HOST/api/userinfo,并带上请求头:"Authorization: Bearer your_access_token"

Casdoor将解析access_token,并根据scope返回相应的用户信息。响应的格式相同:

{
"sub": "7a6b4a8a-b731-48da-bc44-36ae27338817",
"iss": "http://localhost:8000",
"aud": "c58c..."
}

如果您需要更多用户信息,请在步骤授权码授权中获取AccessToken时添加作用域.

访问 OAuth 提供商令牌​

用户通过 OAuth 提供商(GitHub、Google 等)登录后,可以用提供商的访问令牌代表用户调用第三方 API;该令牌保存在用户的 originalToken 字段中。

可以通过 /api/get-account 接口获取该令牌:

{
"status": "ok",
"data": {
"name": "user123",
"originalToken": "ya29.a0AfH6SMBx...",
...
}

只有用户查询自己的账号,或请求者是管理员时,originalToken 才可见。对于其他请求,出于隐私考虑,它会被屏蔽。

这使您的应用程序能够使用提供商的访问令牌与第三方API(例如GitHub API、Google Drive API)进行交互,而无需额外的OAuth流程。

userinfo 和 get-account APIs之间的差异​

  • /api/userinfo:此API作为OIDC协议的一部分返回用户信息。它提供有限的信息,包括仅在OIDC标准中定义的基本信息。有关 Casdoor 支持的可用范围列表,请参阅范围部分。

  • /api/get-account:此API用于检索当前登录账户的用户对象。这是一个 Casdoor 特有的 API,可让您获取 Casdoor 中用户的所有信息,包括适用情况下的 OAuth 提供商的访问令牌。