PKCE 是 Proof Key for Code Exchange 的缩写,PKCE 是一种用于增强授权码模式安全性的方法,它可以防止恶意应用程序通过截获授权码和重定向 URI 来获得访问令牌。PKCE 通过将随机字符串( code_verifier )和其 SHA-256 哈希值( code_challenge )与授权请求一起发送,确保访问令牌只能由具有相应 code_verifier 的应用程序使用,保障用户的安全性。
[ OAuth 2.0 协议扩展] PKCE 扩展协议:为了解决公开客户端的授权安全问题
「面向对象」 public 客户端,其本身没有能力保存密钥信息(恶意攻击者可以通过反编译等手段查看到客户端的密钥 client_secret , 也就可以通过授权码 code 换取 access_token , 到这一步,恶意应用就可以拿着 token 请求资源服务器了)
「原理」 PKCE 协议本身是对 OAuth 2.0 的扩展, 它和之前的授权码流程大体上是一致的, 区别在于在向授权服务器的 authorize endpoint 请求时,需要额外的 code_challenge 和 code_challenge_method 参数;向 token endpoint 请求时, 需要额外的 code_verifier 参数。最后授权服务器会对这三个参数进行对比验证, 通过后颁发令牌。
如果你的应用是一个 SPA 前端应用或移动端 App ,建议使用授权码 + PKCE 模式来完成用户的认证和授权。授权码 + PKCE 模式适合不能安全存储密钥的场景(例如前端浏览器)
我们解释下 code_verifier 和 code_challenge 对于每一个 OAuth/OIDC 请求,客户端会先创建一个代码验证器 code_verifier
code_verifier:在 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 范围内,生成 43-128 位的随机字符串。
code_challenge:则是对 code_verifier 通过 code_challenge_method 例如 sha256 转换得来的。
用大白话讲下就是在认证是用户携带的是加密后的 code_challenge ,在用户认证成功通过 code 获取 Token 时,客户端证明自己的方式则是把 code_verifier 原文发送,认证中心收到获取 Token 请求时通过 code_verifier + code_challenge_method 进行转换,发现最终结果与 code_challenge 匹配则返回 Token ,否则拒绝。
整体上,有以下流程:
1.用户点击登录。 2.在你的应用中,生成 code_verifier 和 code_challenge 。 3.拼接登录链接(包含 code_challenge ) 跳转到 Authing 请求认证。 4.Authing 发现用户没有登录,重定向到认证页面,要求用户完成认证。 5.用户在浏览器完成认证。 6.Authing 服务器通过浏览器通过重定向将授权码( code )发送到你的应用前端。 7.你的应用将授权码 (code) 和 code_verifier 发送到 Authing 请求获取 Token. 8.Authing 校验 code 、code_verifier 和 code_challenge 。 9.校验通过,Authing 则返回 AccessToken 和 IdToken 以及可选的 RefreshToken 。 10.你的应用现在知道了用户的身份,后续使用 AccessToken 换取用户信息,调用资源方的 API 等
需要先在 Authing 创建应用: 配置登录回调和登出回调,配置为你实际项目的地址,我们在这里配置 localhost 用于测试。 若你想匹配多个登录 /登出回调 可以使用 ‘*’ 号进行通配,登录 /登出回调可以是如下格式
在协议配置中,我们勾选 authorization_code 并且使用 code 作为返回类型,如下图所示:PKCE 模式使用的是 code_verifier 来换取 Token ,所以需要配置获取 Token 的方式为 null
GET${host}/oidc/auth 发起登录(拼接你的发起登录地址) POST${host}/oidc/token 获取 TokenGET${host}/oidc/me 获取用户信息 POST${host}/oidc/token/introspection 校验 TokenPOST${host}/oidc/token 刷新 TokenPOST${host}/oidc/revocation 吊销 TokenGET${host}/session/end 登出
GET${host}/oidc/auth
这是基于浏览器的 OIDC 的起点,请求对用户进行身份验证,并会在验证成功后返回授权码到您所指定的 redirect_uri 。
生成 code_challenge 和 code_verifier
在线生成 https://tonyxu-io.github.io/pkce-generator/
离线生成 首先,我们要生成一个 code_challenge 和 code_verifier,以下是使用 Javascript 语言生成 PKCE 所需要的 code_verifier 和 code_challenge 的脚本:
// 生成随机字符串 function generateRandomString(length) { var result = ''; var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; var charactersLength = characters.length; for (var i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } // 生成 code_verifier var codeVerifier = generateRandomString(128); // 对 code_verifier 进行 SHA-256 编码,并将其转换为 base64url 格式的 code_challenge var sha256 = new jsSHA("SHA-256", "TEXT"); sha256.update(codeVerifier); var codeChallenge = btoa(String.fromCharCode.apply(null, new Uint8Array(sha256.getHash("ARRAYBUFFER")))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // 将 code_verifier 和 code_challenge 用对象形式返回 var pkce = { codeVerifier: codeVerifier, codeChallenge: codeChallenge };
以上代码使用 jsSHA 库计算 SHA-256 哈希值,使用 base64url 编码将哈希值转换为 code_challenge 。你可以将以上代码复制到你的 Javascript 代码中,并使用 pkce.codeVerifier 和 pkce.codeChallenge 调用 OAuth 2.0 授权请求。
举例
code_verifier:4aHg5fN1AGdbnBAfVKMf9ZMK4PUOBTwQSKKk9V8wYXOFYDZklMl7dzDUhnQi4sYhzGb6PWCkNQqLP70K1DNOneEDq8iyASepAdGjGBBmCs4BGCDDJNwLrGpnJEfmrI66
code_verifier 的长度为 43 ~ 128 ,我们生成的是 128 位
code_challenge:OhMk95M9qWkKd06--utVtRzQh8Y0Qtqo4cPqqzMJyMw
发起登录地址(浏览器中打开) https://{host}/oidc/auth?scope=openid+profile+offline_access+username+email+phone&redirect_uri=http://localhost:8080/&response_type=code&prompt=consent&nOnce=6e187def-1a19-4067-8875-653f024d5a9f&client_id={client_id}&state=1676881862&code_challenge={code_challenge}&code_challenge_method=S256
参数说明
POST${host}/oidc/token
用户在 Authing 侧完成登录操作后,Authing 会将生成的 code 作为参数回调到 redirect_uri 地址,此时通过 code 换 token 接口即可拿到对应的访问令牌 access_token
请求参数 请求示例
curl --location --request POST 'https://{host}/oidc/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'redirect_uri={发起登录时指定的 redirect_uri}' \ --data-urlencode 'code={/oidc/auth 返回的 code}' \ --data-urlencode 'code_verifier={code_verifier}'
响应示例(成功)
{ "scope": "openid username email phone offline_access profile", "token_type": "Bearer", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9wZW5pZCB1c2VybmFtZSBlbWFpbCBwaG9uZSBvZmZsaW5lX2FjY2VzcyBwcm9maWxlIiwiaWF0IjoxNjc2MzY2OTE0LCJleHAiOjE2Nzc1NzY1MTQsImp0aSI6ImVmVU04enNrbl92LXYzeXZfbDVHRV9fQ2JEY0NNZDhEVDFnYVI0bHRqcHAiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.E3gAYzCQbJmrtM5zl91OPHm2YPnDxzRejw75oVMF1tLqCS0trj6CSBxyxP3Z9t6Eb_oAu1f_3I6XC3KC-l0DTM6q7_R2rnW4LWlik2rDCLuGpG0FqFScLZhwafmrPsVn93yaBQfEEoaLviqKhj3DgOymKqHZzFG3taaz2k_pWsxt4z97DtKjRTiqyMvcSfHsVrjSKELaC-5S_PHPWcQ70iX85IwUb6i5ldZGxYmODCvChNC9p4D4IOT3atvyEHgBTmjA9ZKI-T7hCVHSO91WZY3l1p4iWdi6KdP1oMGTy8WbmUHG9SiWO1Efh_9I5ZpRzVNWXINLv-lZ0d2aZKjg2w", "expires_in": 1209600, "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzYzNjY5MTQsImV4cCI6MTY3NzU3NjUxNCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJub25jZSI6IjhiYjg3MjdhLWU1MGUtNDUzOC05ZmZmLWZhOTFlNWQ0Y2MwYSIsIm5hbWUiOm51bGwsImdpdmVuX25hbWUiOm51bGwsIm1pZGRsZV9uYW1lIjpudWxsLCJmYW1pbHlfbmFtZSI6bnVsbCwibmlja25hbWUiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6bnVsbCwicHJvZmlsZSI6bnVsbCwicGljdHVyZSI6Imh0dHBzOi8vZmlsZXMuYXV0aGluZy5jby9hdXRoaW5nLWNvbnNvbGUvZGVmYXVsdC11c2VyLWF2YXRhci5wbmciLCJ3ZWJzaXRlIjpudWxsLCJiaXJ0aGRhdGUiOm51bGwsImdlbmRlciI6IlUiLCJ6b25laW5mbyI6bnVsbCwibG9jYWxlIjpudWxsLCJ1cGRhdGVkX2F0IjoiMjAyMy0wMi0xNFQwOToyNjoyOC4wNjhaIiwiZW1haWwiOm51bGwsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicGhvbmVfbnVtYmVyIjoiMTg1MTY4Mjk5OTUiLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOnRydWUsInVzZXJuYW1lIjpudWxsfQ.GweoWBCEyHQGP6G9ohbfBMUMALlbZMM9hRAes1De7BM", "refresh_token": "KanvCEmonS_FgCRdFftOCwka2f8Qjj4tcsIfJF-VC1W" }
响应示例(失败)
{ "error": "invalid_grant", "error_description": "授权码无效或已过期" }
GET${host}/oidc/me
获取用户信息 此端点是 OIDC 获取用户端点,可以通过 AccessToken 获取有关当前登录用户的信息。
请求参数 请求示例
curl --location --request GET 'https://{host}/oidc/me?access_token={access_token}'
响应示例(成功)
{ "name": null, "given_name": null, "middle_name": null, "family_name": null, "nickname": null, "preferred_username": null, "profile": null, "picture": "https://files.authing.co/authing-console/default-user-avatar.png", "website": null, "birthdate": null, "gender": "U", "zoneinfo": null, "locale": null, "updated_at": "2023-02-14T09:26:28.068Z", "email": "xxx@authing.cn", "email_verified": true, "phone_number": "185xxxx9995", "phone_number_verified": true, "username": "neo", "sub": "63eb53c441a5c2f05f24bb03" }
响应示例(失败)
{ "error": "invalid_grant", "error_description": "Access Token 无效" }
POST${host}/oidc/auth
此端点接受 access_token 、id_token 、refresh_token ,并返回一个布尔值,指示它是否处于活动状态。如果令牌处于活动状态,还将返回有关令牌的其他数据。如果 token 无效、过期或被吊销,则认为它处于非活动状态。
access_token 可以使用 RS256 签名算法或 HS256 签名算法进行签名。下面是这两种签名算法的区别:
RS256 是使用 RSA 算法的一种数字签名算法,它使用公钥 /私钥对来加密和验证信息。 RS256 签名生成的令牌比 HS256 签名生成的令牌更加安全,因为使用 RSA 密钥对进行签名可以提供更高的保护级别。使用 RS256 签名算法的令牌可以使用公钥进行验证,公钥可以通过 JWK 端点获取。
HS256 是使用对称密钥的一种数字签名算法。它使用同一个密钥进行签名和验证。 HS256 签名算法在性能方面比 RS256 签名算法更快,因为它使用的是对称密钥,而不是使用 RSA 公钥 /私钥对来签名和验证。使用 HS256 签名算法的令牌可以通过 shared secret (应用密钥)进行验证。
在实际应用中,RS256 算法更加安全,但同时也更加消耗资源,如果系统需要高性能,可以选择 HS256 签名算法。
验证 Token 分为两种方式
本地验证与使用 Authing 在线验证。我们建议在本地验证 JWT Token ,因为可以节省你的服务器带宽并加快验证速度。你也可以选择将 Token 发送到 Authing 的验证接口由 Authing 进行验证并返回结果,但这样会造成网络延迟,而且在网络拥塞时可能会有慢速请求。
以下是本地验证和在线验证的优劣对比:
在线校验
需要注意的是,id_token 目前无法在线校验,因为 id_token 只是一个标识,若需要校验 id_token 则需要您在离线自行校验
请求参数
请求示例
curl --location --request POST 'https://{host}/oidc/token/introspection' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'token={ token }' \ --data-urlencode 'token_type_hint={token_type_hint}'
校验 access_token 响应示例(校验通过)
{ "active": true, "sub": "63eb53c441a5c2f05f24bb03", "client_id": "63eb4585156d977101dd3750", "exp": 1677648467, "iat": 1676438867, "iss": "https://oidc-authorization-code.authing.cn/oidc", "jti": "ObgavGBUocr1wsrUvtDLHmuFSgoebxsiOY4JNRqIhaQ", "scope": "offline_access username profile openid phone email", "token_type": "Bearer" }
校验 access_token 响应示例(校验未通过)
{ "active": false }
校验 refresh_token 响应示例 (校验通过)
{ "active": true, "sub": "63eb53c441a5c2f05f24bb03", "client_id": "63eb4585156d977101dd3750", "exp": 1679030867, "iat": 1676438867, "iss": "https://oidc-authorization-code.authing.cn/oidc", "jti": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ", "scope": "offline_access username profile openid phone email" }
校验 refresh_token 响应示例(校验未通过)
{ "active": false }
离线校验 可参考文档( Authing 开发者文档): https://docs.authing.cn/v2/guides/faqs/how-to-validate-user-token.html#%E6%9C%AC%E5%9C%B0%E9%AA%8C%E8%AF%81
我们简单说下,若您使用离线校验应该对 token 进行如下规则的校验 1.格式校验 - 校验 token 格式是否是 JWT 格式 2.类型校验 - 校验 token 是否是目标 token 类型,比如 access_token 、id_token 、refresh_token 3.issuer 校验 - 校验 token 是否为信赖的 issuer 颁发 4.签名校验 - 校验 token 签名是否由 issuer 签发,防止伪造 5.有效期校验 - 校验 token 是否在有效期内 6.claims 校验 - 是否符合与预期的一致
示例代码 下面是一个示例 Java 代码,可以用于在本地校验 OIDC RS256 和 HS256 签发的 access_token
import com.nimbusds.jose.JWSObject; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import java.net.URL; import java.text.ParseException; import java.util.Date; public class OIDCValidator { private static final String ISSUER = "https://your-issuer.com"; private static final String AUDIENCE = "your-client-id"; private final URL jwkUrl; public OIDCValidator(final URL jwkUrl) { this.jwkUrl = jwkUrl; } public JWTClaimsSet validateToken(final String accessToken) throws ParseException { final SignedJWT signedJWT = SignedJWT.parse(accessToken); if (signedJWT == null) { throw new RuntimeException("Access token is null or empty"); } final JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); if (claims == null) { throw new RuntimeException("No claims present in the access token"); } if (!claims.getIssuer().equals(ISSUER)) { throw new RuntimeException("Invalid issuer in access token"); } if (!claims.getAudience().contains(AUDIENCE)) { throw new RuntimeException("Invalid audience in access token"); } final JWSObject jwsObject = signedJWT.getJWSObject(); if (jwsObject == null) { throw new RuntimeException("No JWS object found in the access token"); } // Fetch the JWKs from the JWK set URL final JWKSet jwkSet = JWKSet.load(jwkUrl); final JWK jwk = jwkSet.getKeyByKeyId(jwsObject.getHeader().getKeyID()); if (jwk == null) { throw new RuntimeException("No JWK found for the access token"); } if (!jwsObject.verify(jwk.getKey())) { throw new RuntimeException("Invalid signature in access token"); } if (claims.getExpirationTime() == null || claims.getExpirationTime().before(new Date())) { throw new RuntimeException("Expired access token"); } return claims; } }
这段代码使用 Nimbus JOSE+JWT 库来解析和验证 JWT token 。它使用指定的 issuer 和 audience 值对 access_token 进行验证,并验证 JWT 中 claims 的格式、类型、签名、有效期和 issuer 。如果发生任何验证错误,则将抛出 RuntimeException 。使用时需要传入对应的 JWK URL 和 access_token 进行调用,例如:
final URL jwkUrl = new URL("https://your-issuer.com/.well-known/jwks.json"); final OIDCValidator validator = new OIDCValidator(jwkUrl); final String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; final JWTClaimsSet claims = validator.validateToken(accessToken);
这个示例只校验了 RS256 和 HS256 签名算法。
POST${host}/oidc/token
此功能用于用户 token 的刷新操作,在 token 获取阶段需要先获取到 refresh_token 。
请求参数 请求参数
curl --location --request POST 'https://{host}/oidc/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'refresh_token={刷新令牌}' \ --data-urlencode 'grant_type=refresh_token'
响应示例(成功)
{ "refresh_token": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ", "scope": "offline_access username profile openid phone email", "token_type": "Bearer", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIHVzZXJuYW1lIHByb2ZpbGUgb3BlbmlkIHBob25lIGVtYWlsIiwiaWF0IjoxNjc2NDQ0MjY4LCJleHAiOjE2Nzc2NTM4NjgsImp0aSI6IkEtZUlQYkJ5N3lJLTliUmp1RnJHeXNCSXdjbWtOUl9WalpYODB2aU05VFkiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.Kk3jSK5BSUEDVTQMdMAdG5cBCxZt31vQiD-XYHNA84Gd3Mo8eDLcQpjMEzQ8HJ4_b9IgMOz5ydXz0zAQ6AjLMW3Rl49qhTGDB7Kq7tHTFmDO8itoO2LQTCLPCPtP3TkoOgptlFD_sd32nefH-HojNhuqwKw469Byw3xnW5xEs3wSuOoUdHwR2n9j1T1Zgp3e90xmBjbtbofQE1z0IWtCnrfJ9ujWsKXoN_7OAXbCTa-Ak_DhgLHU7xutQaaBOgD28lLLT5xclgBWfv7Leyx_kBnVGT5Jvo1tfA6AUEp6wJO4GUBzsijLefI04VDzBGypNuFJlw_jOhSp-SWxJjQSwQ", "expires_in": 1209600, "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzY0NDQyNjgsImV4cCI6MTY3NzY1Mzg2OCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJuYW1lIjpudWxsLCJnaXZlbl9uYW1lIjpudWxsLCJtaWRkbGVfbmFtZSI6bnVsbCwiZmFtaWx5X25hbWUiOm51bGwsIm5pY2tuYW1lIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOm51bGwsInByb2ZpbGUiOm51bGwsInBpY3R1cmUiOiJodHRwczovL2ZpbGVzLmF1dGhpbmcuY28vYXV0aGluZy1jb25zb2xlL2RlZmF1bHQtdXNlci1hdmF0YXIucG5nIiwid2Vic2l0ZSI6bnVsbCwiYmlydGhkYXRlIjpudWxsLCJnZW5kZXIiOiJVIiwiem9uZWluZm8iOm51bGwsImxvY2FsZSI6bnVsbCwidXBkYXRlZF9hdCI6IjIwMjMtMDItMTRUMDk6MjY6MjguMDY4WiIsImVtYWlsIjpudWxsLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInBob25lX251bWJlciI6IjE4NTE2ODI5OTk1IiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJ1c2VybmFtZSI6bnVsbH0.DGoJrzkgti44zw-MotVM1KpLxbJTzc5pfh-xYun_xDQ" }
响应示例(失败)
{ "error": "invalid_grant", "error_description": "Refresh Token 无效或已过期" }
POST${host}/oidc/auth
撤销 access_token / refresh_token 。
请求参数 请求示例
curl --location --request POST 'https://{host}/oidc/token/revocation' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'token= {token}' \ --data-urlencode 'token_type_hint={token_type_hint}'
响应示例(成功)
HTTP 200 OK
响应示例(失败)
{ "error": "xxxx", "error_description": "xxxx" }
GET${host}/oidc/session/end
使用此操作通过删除用户的浏览器会话来注销用户。 post_logout_redirect_uri
可以指定在执行注销后重定向的地址。否则,浏览器将重定向到默认页面 请求参数 请求示例(浏览器访问)
GET https://oidc-authorization-code.authing.cn/oidc/session/end?id_token_hint={id_token}&post_logout_redirect_uri=http://localhost:8080/&state=1676452381
本章我们介绍了 OIDC 授权码模式的接入流程以及相关接口的调用方式,对于小白来说可能需要整体跑一遍流程才能熟悉,我们也建议你 fork 我们的 postman collection 跑一遍流程,对 PKCE 模式你就基本掌握啦。 接下来我们还会介绍 OIDC 的授权码+PKCE 流程,以及接入 Authing 的方式,需要你对授权码模式的流程有一定了解哦。
往期精彩内容 什么是事件驱动( EDA )?为什么它是技术领域的主要驱动力?
本文将带各位详细了解 OAuth 2.0 & OIDC 及其授权模式。
OAuth 2.0 是一个授权框架,使应用程序能够获得对 HTTP 服务上用户帐户的有限访问权限,例如 Facebook 、GitHub 和 DigitalOcean 。它通过将用户认证委托给托管用户帐户的服务,并授权第三方应用程序访问用户帐户来实现。
OpenID Connect (OIDC) 是建立在 OAuth 2.0 框架之上的简单身份层。它在 OAuth 2.0 提供的授权的基础上添加了认证。
初看上面这段话你可能很难理解,这里用白话解释下,OAuth 2.0 在设计之初,是为了 API 安全的问题,它是一个授权协议,而 OIDC 则在 OAuth2.0 协议的基础上,提供了用户认证、获取用户信息等的标准实现,当然我们也可以理解获取用户信息也是一个 API ,用 OAuth 2.0 也没问题的,考虑到读者的感受,在这里不要过于纠结,只要记住 OIDC 是完全兼容 OAuth2.0 的,我们现在也推荐用 OIDC。
啥啥啥,写的这都是啥,小白看到这些脑袋都大了,我在这里重点说下 OIDC/OAuth2.0 协议交互时所参与的几个角色,等你对协议熟悉了,可以反过头来再看下相关的介绍,在接下来的授权模式介绍中,我们会结合这四个角色,介绍下不同授权模式的流程。
OAuth2.0 / OIDC 中定义了 2 种 Client 类型:
我们在这里解释下:
Confidential Clients 机密型应用:能够安全的存储凭证( client_secret ),例如有后端服务,你的前端是 Vue ,后台是 Java ,那么可以理解为机密性应用,因为你的后端能够安全的保存 client_secret ,而不会将 client_secret 直接暴露给用户,此时你可以使用授权码模式。
Public Clients 公共型应用:无法安全存储凭证( Client Secrets ),例如 SPA 、移动端、或者完全前后端分离的应用,应当使用授权码 + PKCE 模式
重点来啦!我们要了解授权模式,才能更好的针对系统类型进行授权模式的选型,避免由于授权模式选型不当所造成的开发工作增加和安全侧的漏洞。
授权模式
选型建议
授权码模式适合应用具备后端服务器的场景。授权码模式要求应用必须能够安全存储密钥,用于后续使用授权码换 Access Token 。授权码模式需要通过浏览器与终端用户交互完成认证授权,然后通过浏览器重定向将授权码发送到后端服务,之后进行授权码换 Token 以及 Token 换用户信息。
整体上,有以下流程:
流程图如下:
如果你的应用是一个 SPA 前端应用或移动端 App ,建议使用授权码 + PKCE 模式来完成用户的认证和授权。授权码 + PKCE 模式适合不能安全存储密钥的场景(例如前端浏览器) 。
我们解释下 code_verifier 和 code_challenge 。
code_verifier:在 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 范围内,生成 43-128 位的随机字符串。 code_challenge:则是对 code_verifier 通过 code_challenge_method 例如 sha256 转换得来的。
用大白话讲下就是在认证是用户携带的是加密后的 code_challenge ,在用户认证成功获取 Token 时,客户端证明自己的方式则是把 code_verifier 原文发送,认证中心收到获取 Token 请求时通过 code_verifier + code_challenge_method 进行转换,发现最终结果与 code_challenge 匹配则返回 Token ,否则拒绝。
整体上,有以下流程:
流程图如下:
Client Credentials 模式用于进行服务器对服务器间的授权( M2M 授权),期间没有用户的参与。你需要创建编程访问账号,并将 AK 、SK 密钥对交给你的资源调用方。
注意:Client Credentials 模式不支持 Refresh Token。
整体上,有以下流程:
流程图如下:
隐式模式适合不能安全存储密钥的场景(例如前端浏览器),不推荐此模式,建议采用其他模式。在隐式模式中,应用不需要使用 code 换 token ,无需请求 /token 端点,AccessToken 和 IdToken 会直接从认证端点返回。
注意:因为隐式模式用于不能安全存储密钥的场景,所以隐式模式不支持获取 Refresh Token。
整体上,有以下流程:
流程图如下:
不推荐使用此模式,尽量使用其他模式。只有其他模式都无法解决问题时才会考虑使用密码模式。如果使用密码模式,请确保你的应用代码逻辑非常安全,不会被黑客攻击,否则将会直接泄露用户的账密。一般用于改造集成非常古老的应用,否则绝对不要把它作为你的第一选择。
整体上,有以下流程:
流程图如下:
对于一些连接到互联网的输入受限设备,设备不会直接验证用户身份,而是让用户通过链接或二维码转到手机或电脑上进行认证,从而避免了用户无法轻松输入文本所带来的糟糕体验。
Device Code Flow 这个与前面几个不太一样,开始不再是由资源持有者发起,而是由客户端开始。甚至登录的方法与客户端还没有特别的关联。
大致流程说明如下:
流程图如下:
一个设计良好的系统,都需要一个标准、安全、可扩展的用户认证协议,无论是 ToC 还是 ToE ,接来下我们还会针对具体的授权模式结合实际场景讲一下具体的模式。
本章我们介绍了 OIDC 和 OAuth2.0 两个协议,在使用起来,这两个协议并没有太大的区别,OAuth2.0 是一个授权协议,OIDC 协议则是在 OAuth2.0 的提供的授权的基础上添加了认证。
授权模式,我们分别介绍了授权码模式( Authorization Code ) 、授权码 + PKCE 模式( Authorization Code With PKCE ) 、客户端凭证模式( Client Credentials ) 、隐式模式( Implicit )(不推荐)、密码模式( Password ) (不推荐)、设备代码模式( Device Code )(几乎用不到) 。
授权模式选择
接入 Authing 后的优势: 在整个 Authing 的身份源中,已经包含了社会化登录方式 微信、微博、QQ 、FB 、TW ...等等,企业登录方式 飞书、钉钉、企微、AD 等等,只要你完成了接入 Authing 就意味着你的业务系统具备了这些能力。
整体上,有以下流程:
流程图如下:
创建应用:
配置登录回调和登出回调,配置为你实际项目的地址,我们在这里配置 localhost 用于测试。
若你想匹配多个登录 /登出回调,可以使用 ‘*’ 号进行通配,登录 /登出回调可以是如下格式:
在协议配置中,我们勾选 authorization_code 并且使用 code 作为返回类型,如下图所示:
GET${host}/oidc/auth 发起登录(拼接你的发起登录地址) POST${host}/oidc/token 获取 Token GET${host}/session/me 获取用户信息 POST${host}/oidc/token/introspection 校验 Token POST${host}/oidc/token 刷新 Token POST${host}/oidc/revocation 吊销 Token GET${host}/session/end 登出
以下要介绍的接口可以通过我们的在线 postman collection 自行 fork 体验。
GET${host}/oidc/auth
这是基于浏览器的 OIDC 的起点,请求对用户进行身份验证,并会在验证成功后返回授权码到您所指定的 redirect_uri 。
拼接发起登录地址(浏览器中打开): https://{host}/oidc/auth?scope=openid+profile+email+phone+username&redirect_uri={redirect_uri}&response_type=code&client_id={应用 ID}&state={state}
如您需要额外获取 refresh_token 则请求格式为: https://{host}/oidc/auth? scope=openid+profile+offline_access+username+email+phone&redirect_uri=http://localhost:8080/&response_type=code&client_id={应用 ID}&prompt=consent&state={state}
参数说明:
POST${host}/oidc/token
用户在 Authing 侧完成登录操作后,Authing 会将生成的 code 作为参数回调到 redirect_uri 地址,此时通过 code 换 token 接口即可拿到对应的访问令牌 access_token 。
请求参数
请求事例
curl --location --request POST 'https://{host}/oidc/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'redirect_uri={发起登录时指定的 redirect_uri}' \ --data-urlencode 'code={/oidc/auth 返回的 code}'
响应示例 (成功)
{ "scope": "openid username email phone offline_access profile", "token_type": "Bearer", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9wZW5pZCB1c2VybmFtZSBlbWFpbCBwaG9uZSBvZmZsaW5lX2FjY2VzcyBwcm9maWxlIiwiaWF0IjoxNjc2MzY2OTE0LCJleHAiOjE2Nzc1NzY1MTQsImp0aSI6ImVmVU04enNrbl92LXYzeXZfbDVHRV9fQ2JEY0NNZDhEVDFnYVI0bHRqcHAiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.E3gAYzCQbJmrtM5zl91OPHm2YPnDxzRejw75oVMF1tLqCS0trj6CSBxyxP3Z9t6Eb_oAu1f_3I6XC3KC-l0DTM6q7_R2rnW4LWlik2rDCLuGpG0FqFScLZhwafmrPsVn93yaBQfEEoaLviqKhj3DgOymKqHZzFG3taaz2k_pWsxt4z97DtKjRTiqyMvcSfHsVrjSKELaC-5S_PHPWcQ70iX85IwUb6i5ldZGxYmODCvChNC9p4D4IOT3atvyEHgBTmjA9ZKI-T7hCVHSO91WZY3l1p4iWdi6KdP1oMGTy8WbmUHG9SiWO1Efh_9I5ZpRzVNWXINLv-lZ0d2aZKjg2w", "expires_in": 1209600, "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzYzNjY5MTQsImV4cCI6MTY3NzU3NjUxNCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJub25jZSI6IjhiYjg3MjdhLWU1MGUtNDUzOC05ZmZmLWZhOTFlNWQ0Y2MwYSIsIm5hbWUiOm51bGwsImdpdmVuX25hbWUiOm51bGwsIm1pZGRsZV9uYW1lIjpudWxsLCJmYW1pbHlfbmFtZSI6bnVsbCwibmlja25hbWUiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6bnVsbCwicHJvZmlsZSI6bnVsbCwicGljdHVyZSI6Imh0dHBzOi8vZmlsZXMuYXV0aGluZy5jby9hdXRoaW5nLWNvbnNvbGUvZGVmYXVsdC11c2VyLWF2YXRhci5wbmciLCJ3ZWJzaXRlIjpudWxsLCJiaXJ0aGRhdGUiOm51bGwsImdlbmRlciI6IlUiLCJ6b25laW5mbyI6bnVsbCwibG9jYWxlIjpudWxsLCJ1cGRhdGVkX2F0IjoiMjAyMy0wMi0xNFQwOToyNjoyOC4wNjhaIiwiZW1haWwiOm51bGwsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicGhvbmVfbnVtYmVyIjoiMTg1MTY4Mjk5OTUiLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOnRydWUsInVzZXJuYW1lIjpudWxsfQ.GweoWBCEyHQGP6G9ohbfBMUMALlbZMM9hRAes1De7BM", "refresh_token": "KanvCEmonS_FgCRdFftOCwka2f8Qjj4tcsIfJF-VC1W" }
响应示例 (失败)
{ "error": "invalid_grant", "error_description": "授权码无效或已过期" }
GET${host}/session/me 获取用户信息
此端点是 OIDC 获取用户端点,可以通过 AccessToken 获取有关当前登录用户的信息。
请求参数
请求示例
curl --location --request GET 'https://{host}/oidc/me?access_token={access_token}'
响应示例 (成功)
{ "name": null, "given_name": null, "middle_name": null, "family_name": null, "nickname": null, "preferred_username": null, "profile": null, "picture": "https://files.authing.co/authing-console/default-user-avatar.png", "website": null, "birthdate": null, "gender": "U", "zoneinfo": null, "locale": null, "updated_at": "2023-02-14T09:26:28.068Z", "email": "xxx@authing.cn", "email_verified": true, "phone_number": "185xxxx9995", "phone_number_verified": true, "username": "neo", "sub": "63eb53c441a5c2f05f24bb03" }
响应示例 (失败)
{ "error": "invalid_grant", "error_description": "Access Token 无效" }
POST${host}/oidc/token/introspection
此端点接受 access_token 、id_token 、refresh_token ,并返回一个布尔值,指示它是否处于活动状态。如果令牌处于活动状态,还将返回有关令牌的其他数据。如果 token 无效、过期或被吊销,则认为它处于非活动状态。
1.验证 Token 分为两种方式:
本地验证与使用 Authing 在线验证。我们建议在本地验证 JWT Token ,因为可以节省你的服务器带宽并加快验证速度。你也可以选择将 Token 发送到 Authing 的验证接口由 Authing 进行验证并返回结果,但这样会造成网络延迟,而且在网络拥塞时可能会有慢速请求。
access_token 可以使用 RS256 签名算法或 HS256 签名算法进行签名。下面是这两种签名算法的区别:
RS256 是使用 RSA 算法的一种数字签名算法,它使用公钥 /私钥对来加密和验证信息。RS256 签名生成的令牌比 HS256 签名生成的令牌更加安全,因为使用 RSA 密钥对进行签名可以提供更高的保护级别。使用 RS256 签名算法的令牌可以使用公钥进行验证,公钥可以通过 JWK 端点获取。
HS256 是使用对称密钥的一种数字签名算法。它使用同一个密钥进行签名和验证。HS256 签名算法在性能方面比 RS256 签名算法更快,因为它使用的是对称密钥,而不是使用 RSA 公钥 /私钥对来签名和验证。使用 HS256 签名算法的令牌可以通过 shared secret (应用密钥)进行验证。 在实际应用中,RS256 算法更加安全,但同时也更加消耗资源,如果系统需要高性能,可以选择 HS256 签名算法。
以下是本地验证和在线验证的优劣对比:
2.在线验证
需要注意的是,id_token 目前无法在线校验,因为 id_token 只是一个标识,若需要校验 id_token 则需要您在离线自行校验。
请求参数
请求示例
curl --location --request POST 'https://{host}/oidc/token/introspection' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'token={ token }' \ --data-urlencode 'token_type_hint={token_type_hint}'
校验 access_token 响应示例(校验通过)
{ "active": true, "sub": "63eb53c441a5c2f05f24bb03", "client_id": "63eb4585156d977101dd3750", "exp": 1677648467, "iat": 1676438867, "iss": "https://oidc-authorization-code.authing.cn/oidc", "jti": "ObgavGBUocr1wsrUvtDLHmuFSgoebxsiOY4JNRqIhaQ", "scope": "offline_access username profile openid phone email", "token_type": "Bearer" }
校验 access_token 响应示例(校验未通过)
{ "active": false }
校验 refresh_token 响应示例 (校验通过)
{ "active": true, "sub": "63eb53c441a5c2f05f24bb03", "client_id": "63eb4585156d977101dd3750", "exp": 1679030867, "iat": 1676438867, "iss": "https://oidc-authorization-code.authing.cn/oidc", "jti": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ", "scope": "offline_access username profile openid phone email" }
校验 refresh_token 响应示例(校验未通过)
{ "active": false }
3.离线校验
可参考文档:离线校验( 文档链接: https://docs.authing.cn/v2/guides/faqs/how-to-validate-user-token.html# )
我们简单说下,若您使用离线校验应该对 token 进行如下规则的校验:
1.格式校验 - 校验 token 格式是否是 JWT 格式 2.类型校验 - 校验 token 是否是目标 token 类型,比如 access_token 、id_token 、refresh_token 3.issuer 校验 - 校验 token 是否为信赖的 issuer 颁发 4.签名校验 - 校验 token 签名是否由 issuer 签发,防止伪造 5.时间校验 - 校验 token 是否在有效期内 6.claims 校验 - 是否符合与预期的一致
以上 6 点均校验通过,我们才能认为 token 是有效且合法的。
下面是一个示例 Java 代码,可以用于在本地校验 OIDC RS256 和 HS256 签发的 access_token 。
import com.nimbusds.jose.JWSObject; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import java.net.URL; import java.text.ParseException; import java.util.Date; public class OIDCValidator { private static final String ISSUER = "https://your-issuer.com"; private static final String AUDIENCE = "your-client-id"; private final URL jwkUrl; public OIDCValidator(final URL jwkUrl) { this.jwkUrl = jwkUrl; } public JWTClaimsSet validateToken(final String accessToken) throws ParseException { final SignedJWT signedJWT = SignedJWT.parse(accessToken); if (signedJWT == null) { throw new RuntimeException("Access token is null or empty"); } final JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); if (claims == null) { throw new RuntimeException("No claims present in the access token"); } if (!claims.getIssuer().equals(ISSUER)) { throw new RuntimeException("Invalid issuer in access token"); } if (!claims.getAudience().contains(AUDIENCE)) { throw new RuntimeException("Invalid audience in access token"); } final JWSObject jwsObject = signedJWT.getJWSObject(); if (jwsObject == null) { throw new RuntimeException("No JWS object found in the access token"); } // Fetch the JWKs from the JWK set URL final JWKSet jwkSet = JWKSet.load(jwkUrl); final JWK jwk = jwkSet.getKeyByKeyId(jwsObject.getHeader().getKeyID()); if (jwk == null) { throw new RuntimeException("No JWK found for the access token"); } if (!jwsObject.verify(jwk.getKey())) { throw new RuntimeException("Invalid signature in access token"); } if (claims.getExpirationTime() == null || claims.getExpirationTime().before(new Date())) { throw new RuntimeException("Expired access token"); } return claims; } }
这段代码使用 Nimbus JOSE+JWT 库来解析和验证 JWT token 。它使用指定的 issuer 和 audience 值对 access_token 进行验证,并验证 JWT 中 claims 的格式、类型、签名、有效期和 issuer 。如果发生任何验证错误,则将抛出 RuntimeException 。使用时需要传入对应的 JWK URL 和 access_token 进行调用,例如:
final URL jwkUrl = new URL("https://your-issuer.com/.well-known/jwks.json"); final OIDCValidator validator = new OIDCValidator(jwkUrl); final String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; final JWTClaimsSet claims = validator.validateToken(accessToken);
这个示例只校验了 RS256 和 HS256 签名算法。
POST${host}/oidc/token
此功能用于用户 token 的刷新操作,在 token 获取阶段需要先获取到 refresh_token 。
请求参数
请求示例
curl --location --request POST 'https://{host}/oidc/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'refresh_token={刷新令牌}' \ --data-urlencode 'grant_type=refresh_token'
响应示例(成功)
{ "refresh_token": "6F2TO1v1YZ1_N7I3jXYHjK-vZzKtlD0IiP5KPoUFUCQ", "scope": "offline_access username profile openid phone email", "token_type": "Bearer", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImVtSzBGbVRIa0xlQWFjeS1YWEpVT3J6SzkxV243TkdoNGFlYUVlSjVQOUUifQ.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIHVzZXJuYW1lIHByb2ZpbGUgb3BlbmlkIHBob25lIGVtYWlsIiwiaWF0IjoxNjc2NDQ0MjY4LCJleHAiOjE2Nzc2NTM4NjgsImp0aSI6IkEtZUlQYkJ5N3lJLTliUmp1RnJHeXNCSXdjbWtOUl9WalpYODB2aU05VFkiLCJpc3MiOiJodHRwczovL29pZGMtYXV0aG9yaXphdGlvbi1jb2RlLmF1dGhpbmcuY24vb2lkYyJ9.Kk3jSK5BSUEDVTQMdMAdG5cBCxZt31vQiD-XYHNA84Gd3Mo8eDLcQpjMEzQ8HJ4_b9IgMOz5ydXz0zAQ6AjLMW3Rl49qhTGDB7Kq7tHTFmDO8itoO2LQTCLPCPtP3TkoOgptlFD_sd32nefH-HojNhuqwKw469Byw3xnW5xEs3wSuOoUdHwR2n9j1T1Zgp3e90xmBjbtbofQE1z0IWtCnrfJ9ujWsKXoN_7OAXbCTa-Ak_DhgLHU7xutQaaBOgD28lLLT5xclgBWfv7Leyx_kBnVGT5Jvo1tfA6AUEp6wJO4GUBzsijLefI04VDzBGypNuFJlw_jOhSp-SWxJjQSwQ", "expires_in": 1209600, "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2M2ViNTNjNDQxYTVjMmYwNWYyNGJiMDMiLCJhdWQiOiI2M2ViNDU4NTE1NmQ5NzcxMDFkZDM3NTAiLCJpYXQiOjE2NzY0NDQyNjgsImV4cCI6MTY3NzY1Mzg2OCwiaXNzIjoiaHR0cHM6Ly9vaWRjLWF1dGhvcml6YXRpb24tY29kZS5hdXRoaW5nLmNuL29pZGMiLCJuYW1lIjpudWxsLCJnaXZlbl9uYW1lIjpudWxsLCJtaWRkbGVfbmFtZSI6bnVsbCwiZmFtaWx5X25hbWUiOm51bGwsIm5pY2tuYW1lIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOm51bGwsInByb2ZpbGUiOm51bGwsInBpY3R1cmUiOiJodHRwczovL2ZpbGVzLmF1dGhpbmcuY28vYXV0aGluZy1jb25zb2xlL2RlZmF1bHQtdXNlci1hdmF0YXIucG5nIiwid2Vic2l0ZSI6bnVsbCwiYmlydGhkYXRlIjpudWxsLCJnZW5kZXIiOiJVIiwiem9uZWluZm8iOm51bGwsImxvY2FsZSI6bnVsbCwidXBkYXRlZF9hdCI6IjIwMjMtMDItMTRUMDk6MjY6MjguMDY4WiIsImVtYWlsIjpudWxsLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInBob25lX251bWJlciI6IjE4NTE2ODI5OTk1IiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJ1c2VybmFtZSI6bnVsbH0.DGoJrzkgti44zw-MotVM1KpLxbJTzc5pfh-xYun_xDQ" }
响应示例(失败)
{ "error": "invalid_grant", "error_description": "Refresh Token 无效或已过期" }
POST${host}/oidc/token/revocation
撤销 access_token / refresh_token
请求参数
请求示例
curl --location --request POST 'https://{host}/oidc/token/revocation' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id={应用 ID}' \ --data-urlencode 'client_secret={应用密钥}' \ --data-urlencode 'token= {token}' \ --data-urlencode 'token_type_hint={token_type_hint}'
响应示例(成功)
HTTP 200 OK
响应示例(失败)
{ "error": "xxxx", "error_description": "xxxx" }
GET${host}/oidc/session/end
使用此操作通过删除用户的浏览器会话来注销用户。 post_logout_redirect_uri
可以指定在执行注销后重定向的地址。否则,浏览器将重定向到默认页面
请求参数
请求示例 (浏览器访问)
GET https://oidc-authorization-code.authing.cn/oidc/session/end?id_token_hint={id_token}&post_logout_redirect_uri=http://localhost:8080/&state=1676452381
SSO(Single Sign-On) 单点登录,即同时访问多个应用仅需要登录一次。
举例:我们现在有两个站点分别是 uthing.com ething.com 我们希望用户在 uthing.com 进行认证后,跳转到 ething 后无需二次认证,反之也是一样的。
具体流程:
1.用户访问 uthing.com ,uthing 前端发现用户未认证,跳转至 Authing 进行认证。
2.用户在 Authing 发起认证完成,Authing 创建 SSO Session ,下发临时授权码 (code) 重定向到 uthing 后台。
需要注意的是,在这里我们也可以重定向到前端页面,再由前端页面自行判断如果是 Authing 回调请求,则携带临时 code 到 uthing 后台去获取 token 。
3.uthing 后台通过 code 向 Authing 换取 access_token 、id_token 、refresh_token 等,并下发给前端。
4.uthing 前端通过 access_token 可以直接向 Authing OIDC 用户信息端点获取当前用户信息。
5.uthing 前端在在后续请求后台时,携带由 Authing 颁发的 access_token ,后台在接受到用户请求后去 Authing 校验 Token 是否有效,有效则可放行,若 Token 校验失败或已过期则返回错误信息。
6.用户访问 ething.com ,ething 跳转至 Authing 进行认证。
7.由于用户在 Authing 已经完成认证,创建了 sso_session ,Authing 侧直接下发临时授权码 (code) ,无需二次认证,后续流程同 1 。
我们发现,用户在 uthing 认证成功的时候,再访问 ething 的时候会向 Authing 跳转一下,才能完成后续流程,这是由于 ething.com 和 uthing.com 并不是同一个站点,无法实现 cookie 共享,如果你的产品地址是 : uthing.xxx.com ething.xxx.com 我们则可以利用相同域下 cookie 共享的方式实现 SSO ,从而避免此问题。
SLO(Single Logout) 单点登出,即多个应用仅需要登出一次,其他应用也自动登出 。
则 SLO 流程如下:
1.用户在某个站点登出,我们则需要调用 OIDC 登出端点销毁 Token ,由于是 cookie 共享实现的 SSO ,然后清除 xxx.com 对应会话的 cookie 即可。
2.ething / uting 应当在每次发起请求前,判断 cookie 中是否存在登录态,若不存在,则需要跳转默认页面提示用户已经登出。
如果你的产品地址不是同一个域,例如: uthing.com ething.com
则 SLO 流程如下:
1.用户在某个站点登出,我们则需要调用 OIDC 登出端点销毁 Token ,在 Authing 的应用配置中,你应当先把应用都添加到 SSO 中,或者 uthing.com 和 ething.com 使用 Authing 的同一个自建应用,当用户在某个站点登出后,另外一个站点的 Token 也会失效。 2.用户未登出的站点发起请求,当后台校验 Token 失败后,则下发清除 cookie 的命令并跳转默认页面提示用户已经登出,需要登录。
本章我们介绍了 OIDC 授权码模式的接入流程以及相关接口的调用方式,对于小白来说可能需要整体跑一遍流程才能熟悉,我们也建议你 fork 我们的 postman collection 跑一遍流程,对授权码模式你就基本掌握啦。
接下来我们还会介绍 OIDC 的授权码+PKCE 流程,以及接入 Authing 的方式,需要你对授权码模式的流程有一定了解哦。
]]>欢迎 Star 跟进! Github:Fluocean
主要特性:
Github OAuth 示例
洋流提供了许多的重载方法,用于应对不同情况下的请求,有些带自有请求头的,也有要求必须要 Scope 的。
// 创建 OAuth 原始处理器 OAuth<GithubAccess, GithubAccess.Wrong> oauth = new GithubOAuth( "id", "secret", "https://exmaple.com/api/v0/login/oauth/github/callback" ); // 生成授权 url String url = oauth.authorize(); // 生成带参数或指定 scope String spec = oauth.authorize(List.of("email"), Map.of("Accept", "application/json")); //解析回调的 url 并获取 token // 输入原始 url 自动解析 code 以及 state oauth.token("url"); // 更改回调地址 oauth.token("url", "redirect"); // 手动指定参数 oauth.token("code", "state", "redirect"); // 处理返回值 oauth.token("code", "state", "redirect") .succeed(data -> System.out.println(data.accessToken())) .fail(wrong -> System.out.println(wrong.errorDescription())) .except(throwable -> System.out.println(throwable.getMessage())); // 假设请求成功 直接获取数据 GithubAccess access = oauth.token("code", "state", "redirect").data();
使用 Socks 代理
oauth.proxy(() -> new Request.Proxy( Proxy.Type.SOCKS, "127.0.0.1", 10086, "username", "password" ) );
更换 State 生成器
默认随机生成 UUID 并设置 300 秒有效期
oauth.state( Lazy.of(() -> new OAuthState(300, () -> UUID.randomUUID().toString())) );
更换 Http 客户端
默认使用 Okhttp 发起请求
// 实现比较繁杂 就不展示啦 x oauth.request(Lazy.of(OAuthRequest::new));
更换 Json 解析器
默认使用 Gson 作为 Json 解析器
oauth.serialization( Lazy.of(() -> new Serialization() { final Gson gson = new Gson(); @Override public <T> T object(Class<T> type, String raw) { return gson.fromJson(raw, type); } @Override public <K, V> Map<K, V> map(Class<K> key, Class<V> value, String raw) { return gson.fromJson(raw, new TypeToken<Map<K, V>>() { }.getType()); } @Override public <T> List<T> list(Class<T> type, String raw) { return gson.fromJson(raw, new TypeToken<List<T>>() { }.getType()); } }) );
]]>很多手机 app 用第三方身份提供商通过 OAuth2.0 (或者类似 OpenID Connect 的方式)做登录的,身份提供商一般是允许手机应用登录的时候用 WebView 打开提供商的登录页面的。
WebView 的请求内容可以被手机应用拦截,那用 OAuth2.0 的意义呢,密码不是会被手机应用获取到吗,或者一般像 QQ 这些是靠他们提供的 SDK 保证密码安全的吗?
或者说是客户端类型是 public 的相关的问题, OAuth2.0 协议里认为手机 app 这类的程序是不能保证客户端密码安全的。
public 类型的客户端如果是 web app 还好,它是有一个固定的 URL 的,也就是说通过redirect_uri
能保证只有这个 web app 能获得授权码,或者说也能保证只有这个网站能用这个客户端 ID 。
但是本地应用程序这类的,redirect_uri
也挺不靠谱的:
127.0.0.1:port
之类的重定向 uri 。scheme:XXXXX
。urn:ietf:wg:oauth:2.0:oob
这样的redirect_uri
来打开拷贝授权码的页面。这些客户端在授权服务器上注册的意义似乎并不是很大啊,而且如果客户端 ID (客户端 ID 没有要求要保密)被其他人获取了以后,其他人也很有可能能冒充使用啊。
或者说这情况是不是还是应该让这类本地安装的程序的服务器端来做授权,让程序和他的服务器端来通信更佳合适,或者客户端ID也应该要随机生成不能随意遍历和预测?
]]>