OAuth 2

使用请求头 Authorization: Bearer <access_token></access_token> 来执行需要身份验证的请求。你可以通过组合以下两个请求来获取一个持有者令牌(bearer token)。

这里有一篇非常棒的 OAuth 入门介绍:OAuth 2.0 和 OpenID Connect(通俗易懂版)

GET /api/method/frappe.integrations.oauth2.authorize

获取用户的授权码,以便代表用户访问 ERPNext。

参数(在查询字符串中):

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • state(字符串)

你的客户端应用程序用来在请求和回调之间维护状态的任意值。授权服务器在将用户代理重定向回客户端时会包含这个值。该参数应用于防止跨站请求伪造。

  • response_type(字符串)

“code”

  • scope(字符串)

应授予你的应用程序的访问范围。

  • redirect_uri(字符串)

应用程序授权后,用户将被重定向到的回调 URI。然后可以从参数中提取授权码。

  • code_challenge_method(字符串)

(可选)可以是 s256 或 plain 中的一个。

  • code_challenge(字符串)

(可选)当 code_challenge_method=s256 时可以是 base64encode(sha256(random_string)),或者当 code_challenge_method=plain 时可以是 random_string。参考 https://tools.ietf.org/html/rfc7636#appendix-A

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.authorize \
 --data-urlencode 'client_id=511cb2ac2d' \
 --data-urlencode 'state=444' \
 # base64encode(sha256('420')) => 21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0
 # --data-urlencode 'code_verifier=21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0' \
 --data-urlencode 'response_type=code'
 --data-urlencode 'scope=openid%20all' \
 --data-urlencode 'redirect_uri=https://app.getpostman.com/oauth2/callback'

返回:

  • HTTP 状态码:200
  • text/html

这将打开授权页面,然后会将你重定向到 redirect_uri。

如果用户点击”允许”,重定向 URI 将被调用,并在查询参数中附带授权码:

https://{redirect uri}?code=plkj2mqDLwaLJAgDBAkyR1W8Co08Ud&state=444

如果用户点击”拒绝”,你将收到一个错误:

https://{redirect uri}?error=access_denied

使用 ID Token 的授权码授予的令牌交换

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

注意:此端点也可用于获取刷新的访问令牌。只需在请求体中发送 refresh_token 即可。

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback

返回:

 {
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
 }

使用 ID Token 的授权码授予的令牌交换(PKCE)

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

  • code_verifier(字符串)

在 Authorization Request 期间与 code_challenge_method 和 code_challenge 一起使用的 random_string。

Content-Type: application/x-www-form-urlencoded

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a&code_verifier=420'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&code_verifier=420

返回:

{
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
}

撤销令牌端点

POST /api/method/frappe.integrations.oauth2.revoke_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token

要撤销的访问令牌。

返回:

始终返回空响应,HTTP 状态码为 200。

 {}

Open ID Connect id_token

ID Token 是一个 JWT。

  • aud 声明包含已注册的 client_id。
  • iss 声明包含 frappe 服务器 URL。
  • sub 声明包含 Frappe 用户的 userid。
  • roles 声明包含用户角色。
  • exp 声明包含过期时间。
  • iat 声明包含签发时间。

示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCJ9.ZEdnrHjLbArahVTN19b4zoRFoBO5a2BakRkR82O1VU8

使用 PyJWT 验证并提取它。

import jwt

id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCIsInJvbGVzIjpbIlN5c3RlbSBNYW5hZ2VyIiwiU2FsZXMgTWFuYWdlciJdLCJub25jZSI6Ijc4OTEyMyIsImlzcyI6Imh0dHBzOi8vZXJwLmV4YW1wbGUuY29tIn0.F8Wbh5dtD1loZPltJLj_sqF9DZNeBvEbo-ITtf3UPqk"

client_id = 'client_id'
client_secret = 'client_secret'

payload = jwt.decode(
 id_token,
 audience=client_id,
 key=client_secret,
 algorithm="HS256",
 options={"verify_exp": False}, # Enabled by default to verify expiration time
)

print(payload)
# Output

{'sub': '1234567890', 'name': 'J. Doe', 'email': '[email protected]', 'iat': 1516239022, 'exp': 1516242022, 'aud': 'client_id', 'roles': ['System Manager', 'Sales Manager'], 'nonce': '789123', 'iss': 'https://erp.example.com'}

OpenID 用户信息端点

请求

GET /api/method/frappe.integrations.oauth2.openid_profile
Header: Authorization: Bearer valid_access_token

响应

{
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "client_id",
 "roles": ["System Manager", "Sales Manager"]
}

内省令牌端点

POST /api/method/frappe.integrations.oauth2.introspect_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token_type_hint

access_token 或 refresh_token,如果未提供则默认为 access_token

  • token

要内省的访问令牌或刷新令牌。取决于 token_type_hint

返回:

{
 "client_id": "511cb2ac2d",
 "trusted_client": 1,
 "active": true,
 "exp": 1619523326,
 "scope": "openid all",
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "511cb2ac2d",
 "roles": ["System Manager", "Sales Manager"]
}

或者

{
 "active": false,
 "_server_messages": "..."
}

进一步阅读

请查看 Guides / Integration / How To Set Up Oauth 了解如何创建新的 OAuth 2 客户端。

  • Content-Type 请求头,
  • Authorization 请求头,
  • OAuth 2 规范,
  • 持有者令牌(Bearer token)。

作者:

  • Raffael Meyer ([email protected])
  • Revant Nandgaonkar ([email protected])