本文提供一个Okta与阿里云进行OIDC角色SSO的示例,使Okta中的应用通过临时身份凭证(STS Token)安全访问阿里云资源。
前提条件
请提前在Okta中注册一个OIDC应用,并获取应用的颁发者URL和客户端ID(Client ID)。本示例中使用的数据如下:- 颁发者URL:https://dev-xxxxxx.okta.com
- 客户端ID:0oa294vi1vJoClev****
步骤一:在阿里云创建OIDC身份提供商
本步骤中将创建一个名为TestOidcProvider
的OIDC身份提供商。颁发者URL为https://dev-xxxxxx.okta.com
,客户端ID为0oa294vi1vJoClev****
。
使用RAM管理员登录RAM控制台。
在左侧导航栏,选择 。
在角色SSO页签,先单击OIDC页签,然后单击创建身份提供商。
在创建身份提供商页面,设置身份提供商信息。
参数
说明
身份提供商名称
同一个阿里云账号下必须唯一。
颁发者URL
颁发者URL由外部IdP提供。颁发者URL必须以
https
开头,符合标准URL格式,但不允许带有query参数(以?
标识)、fragment片段(以#
标识)和登录信息(以@
标识)。验证指纹
为了防止颁发者URL被恶意劫持或篡改,您需要配置外部IdP的HTTPS CA证书生成的验证指纹。
填写完颁发者URL后,您可以单击获取指纹,阿里云会辅助您自动计算出验证指纹,但是建议您在本地自己计算一次(例如:使用OpenSSL计算指纹),与阿里云计算的指纹进行对比。如果对比发现不同,则说明该颁发者URL可能已经受到攻击,请您务必再次确认,并填写正确的指纹。
说明当您的IdP计划进行证书轮转时,请在轮转前生成新证书的指纹并添加到阿里云OIDC身份提供商信息中,一段时间(至少一天)以后再进行证书轮转,证书轮转确认可以换取到STS Token后再删除旧的指纹。
客户端ID
您的应用在外部IdP注册的时候,会生成一个客户端ID(Client ID)。当您从外部IdP申请签发OIDC令牌(OIDC Token)时必须使用该客户端ID,签发出来的OIDC Token也会通过
aud
字段携带该客户端ID。在创建OIDC身份提供商时配置该客户端ID,然后在使用OIDC Token换取STS Token时,阿里云会校验OIDC Token中aud
字段所携带的客户端ID与OIDC身份提供商中配置的客户端ID是否一致。只有一致时,才允许扮演角色。如果您有多个应用需要访问阿里云,您可以配置多个客户端ID,但最多不能超过20个。
最早颁发时间限制
在该限制时间之前颁发的OIDC Token不允许换取STS Token。
默认值:12小时。取值范围:1~168小时。
备注
身份提供商的描述信息。
单击确定。
步骤二:在阿里云创建可信实体为OIDC身份提供商的RAM角色
本步骤中将创建一个名为testoidc
的RAM角色,身份提供商选择步骤一创建的TestOidcProvider
。
使用RAM管理员登录RAM控制台。
在左侧导航栏,选择 。
在角色页面,单击创建角色。
在创建角色页面,选择可信实体类型为身份提供商,然后单击下一步。
输入角色名称和备注。
选择身份提供商类型为OIDC。
选择身份提供商并设置限制条件。
支持的限制条件如下表所示:
限制条件关键字
说明
是否必选
示例
oidc:iss
OIDC颁发者(Issuer)。用来扮演角色的OIDC令牌中的iss字段值必须满足该限制条件要求,角色才允许被扮演。
该限定条件必须使用StringEquals作为条件操作类型,条件值只能是您在OIDC身份提供商中填写的颁发者URL。该限制条件用于确保只有受信颁发者颁发的OIDC令牌才能扮演角色。
是
https://dev-xxxxxx.okta.com
oidc:aud
OIDC受众(Audience)。用来扮演角色的OIDC令牌中的aud字段值必须满足该限制条件要求,角色才允许被扮演。
该限定条件必须使用StringEquals作为条件操作类型,您可选择在OIDC身份提供商中配置的一个或多个客户端ID(Client ID)作为条件值。该限制条件用于确保只有您设置的Client ID生成的OIDC令牌才能扮演角色。
是
0oa294vi1vJoClev****
oidc:sub
OIDC主体(Subject)。用来扮演角色的OIDC令牌中的sub字段值必须满足该限制条件要求时,角色才允许被扮演。
该限定条件可以使用任何String类的条件操作类型,且您可以最多设置10个OIDC主体作为条件值。该限制条件用于进一步限制允许扮演角色的身份主体,您也可以不指定该限制条件。
否
00u294e3mzNXt4Hi****
单击完成。
单击关闭。
步骤三:为RAM角色授权
您可以根据实际需要,为步骤二创建的RAM角色testoidc
授予访问阿里云资源的权限。
步骤四:在Okta签发OIDC令牌(OIDC Token)
阿里云不支持使用OIDC登录控制台,所以您需要使用程序访问的方式完成OIDC SSO流程。由于生成OIDC Token本质上是个OAuth流程,所以您需要通过标准的OAuth 2.0流程从OIDC IdP(例如:Okta)获取OIDC Token。OAuth支持多种流程,例如:比较常见的Authorization Code Flow。但由于该流程较为复杂,为演示方便,如下将以比较简单的Implicit Flow为例,为您介绍获取OIDC Token并最终完成SSO的流程,其中简化了标准协议要求的部分步骤。
- 搭建一个客户端Web应用,用于接收Okta颁发的OIDC Token。本示例中,将提供一个使用Java Spring Boot和Thymeleaf搭建的极简客户端Web应用。在本机8080端口部署Web应用,绑定的localhost指向127.0.0.1,因此在本机通过浏览器访问localhost:8080就可以访问到该Web应用。相关的示例代码如下:
- 静态页面示例代码
按照OAuth 2.0协议要求Okta回调给客户端Web应用的信息是通过锚点(fragment)来传递的,您可以通过一个Web页面,直接提取出锚点参数来获取回调的OIDC Token。假设您制作了如下这个简单的静态页面,直接进行参数透传。该页面的完整地址为
http://localhost:8080/accessTokenCallback
,也就是Okta应用配置的回调地址redirect_uri
。<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <script> window.onload = function () { let fragment = window.location.hash.substring(1); window.location.href = "/receiveAccessToken?" + fragment; }; </script> </head> </html>
- 类示例代码
创建一个类,作为上述静态页面的控制器。
package com.aliyun.oauthtest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class CallbackController { @RequestMapping("accessTokenCallback") public String callback() { return "accessTokenCallback"; } }
- 静态页面示例代码
- 登录Okta,向Okta申请签发OIDC Token。您需要先登录Okta,然后基于步骤1搭建的客户端Web应用,直接构造并访问URL:
https://dev-xxxxxx.okta.com/oauth2/v1/authorize?client_id=0oa294vi1vJoClev****&scope=openid&response_type=token%20id_token&state=testState&nonce=a_unique_nonce_1&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2FaccessTokenCallback
参数含义如下:
client_id
:Okta中注册的OIDC应用的客户端ID。scope
:取值为openid
。response_type
:Implicit Flow流程中取值为token id_token
。state
:表示客户端的当前状态,可以指定任意值。nonce
:防止重放攻击,可以指定任意值。redirect_uri
:接收access_token
或id_token
的回调地址,即步骤1中的客户端Web应用的地址。
本示例中已经预先登录了Okta,所以系统会根据用户设置的
redirect_uri
重定向到回调地址。如下地址中的id_token
就是OIDC Token。HTTP/1.1 302 Found Location: http://localhost:8080/accessTokenCallback#id_token=eyJraWQiOiJ6OUV0e****&access_token=eyJraWQiOiJseEQ3R****&token_type=Bearer&expires_in=3600&scope=openid&state=testState
- 解析OIDC Token。
您可以对步骤2获取的结果进行简单地解析,将
header
和payload
展开。请求示例:
package com.aliyun.oauthtest; import java.util.Base64; import java.util.Base64.Decoder; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ClientAppController { @RequestMapping(value = "/receiveAccessToken", method = {RequestMethod.POST, RequestMethod.GET}, produces = "application/json") public Map<String, Object> receiveAccessToken(@RequestParam("access_token") String accessToken, @RequestParam("id_token") String idToken, @RequestParam("token_type") String tokenType, @RequestParam("expires_in") Long expireTime, @RequestParam("scope") String scope, @RequestParam("state") String state) { Map<String, Object> result = new TreeMap<>(); result.put("access_token", accessToken); result.put("id_token", idToken); result.put("token_type", tokenType); result.put("expires_in", "" + expireTime); result.put("scope", scope); result.put("state", state); String[] jwt = idToken.split("\\."); Decoder decoder = Base64.getDecoder(); result.put(" id token jwt header", JSON.parse(new String(decoder.decode(jwt[0])))); result.put(" id token jwt payload", JSON.parse(new String(decoder.decode(jwt[1])))); result.put(" id token jwt signature", jwt[2]); return result; } }
返回示例:
{ " id token jwt header": { "kid": "z9EtyT345d-JLIJo2-5ySDO27LG4FPeOotbwJPT****", "alg": "RS256" }, " id token jwt payload": { "at_hash": "KKsdN3prZWTvBEMn-g****", "sub": "00u294e3mzNXt4Hi****", "aud": "0oa294vi1vJoClev****", "ver": 1, "idp": "0oa294iehxjUCZIO****", "amr": [ "pwd" ], "auth_time": 1636373097, "iss": "https://dev-xxxxxx.okta.com", "exp": 1636377759, "iat": 1636374159, "nonce": "a_unique_nonce_1", "jti": "ID.lmSU5AD2iKLCVu6_KLMIr52dpCprncxW38v-NCA****" }, "id token jwt signature": "ZEJEGIv4Zoau63****", "access_token": "eyJraWQiOiJseEQ3R****", "expires_in": "3600", "id_token": "eyJraWQiOiJ6OUV0e****", "scope": "openid", "state": "testState", "token_type": "Bearer" }
步骤五:使用OIDC Token换取STS Token
您可以直接调用AssumeRoleWithOIDC API,使用从步骤四获取的未解析的OIDC Token换取STS Token。
请求示例:
public static void main(String[] args)
{
IAcsClient client = initialization();
String jwtToken = "eyJraWQiOiJ6OUV0e****"; //从Okta获取的未解析的id_token。
AssumeRoleWithOIDCRequest request = new AssumeRoleWithOIDCRequest();
request.setDurationSeconds(3600L);
request.setOIDCProviderArn("acs:ram::113511544585****:oidc-provider/TestOidcProvider");
request.setOIDCToken(jwtToken);
request.setRoleArn("acs:ram::113511544585****:role/testoidc");
request.setRoleSessionName("TestOidcAssumedRoleSession");
try
{
AssumeRoleWithOIDCResponse resp = client.getAcsResponse(request);
System.out.println("success requestId: " + resp.getRequestId());
System.out.println("success assume role arn: " + resp.getAssumedRoleUser().getArn());
System.out.println("success sts credential accessKey id: " + resp.getCredentials().getAccessKeyId());
System.out.println("success sts credential accessKey secret: " + resp.getCredentials().getAccessKeySecret());
System.out.println("success resp: " + JSON.toJSONString(resp));
}
catch(ClientException | SystemException e)
{
e.printStackTrace();
}
}
返回示例:
success requestId: 3D57EAD2-8723-1F26-B69C-F8707D8B565D
success assume role arn: acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession
success sts credential accessKey id: STS.NUgYrLnoC37mZZCNnAbez****
success sts credential accessKey secret: CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****
success resp:
{
"AssumedRoleUser":
{
"Arn": "acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession",
"AssumedRoleId": "33157794895460****:TestOidcAssumedRoleSession"
},
"Credentials":
{
"AccessKeyId": "STS.NUgYrLnoC37mZZCNnAbez****",
"AccessKeySecret": "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****",
"Expiration": "2021-10-20T04:27:09Z",
"SecurityToken": "CAIShwJ1q6Ft5B2yfSjIr****"
},
"OIDCTokenInfo":
{
"ClientIds": "0oa294vi1vJoClev****",
"Issuer": "https://dev-xxxxxx.okta.com",
"Subject": "00u294e3mzNXt4Hi****"
},
"RequestId": "3D57EAD2-8723-1F26-B69C-F8707D8B565D"
}
其中Credentials
中的信息即为STS Token。
步骤六:使用STS Token访问阿里云资源
使用从步骤五获取的STS Token访问有权限的阿里云资源。