使用OIDC进行角色SSO的示例

本文提供一个Okta与阿里云进行OIDC角色SSO的示例,使Okta中的应用通过临时身份凭证(STS Token)安全访问阿里云资源。

前提条件

请提前在Okta中注册一个OIDC应用,并获取应用的颁发者URL和客户端ID(Client ID)。本示例中使用的数据如下:
  • 颁发者URL:https://dev-xxxxxx.okta.com
  • 客户端ID:0oa294vi1vJoClev****

步骤一:在阿里云创建OIDC身份提供商

本步骤中将创建一个名为TestOidcProvider的OIDC身份提供商。颁发者URLhttps://dev-xxxxxx.okta.com客户端ID0oa294vi1vJoClev****

  1. 使用RAM管理员登录RAM控制台

  2. 在左侧导航栏,选择集成管理 > SSO管理

  3. 角色SSO页签,先单击OIDC页签,然后单击创建身份提供商

  4. 创建身份提供商页面,设置身份提供商信息。

    参数

    说明

    身份提供商名称

    同一个阿里云账号下必须唯一。

    颁发者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小时。

    备注

    身份提供商的描述信息。

  5. 单击确定

步骤二:在阿里云创建可信实体为OIDC身份提供商的RAM角色

本步骤中将创建一个名为testoidc的RAM角色,身份提供商选择步骤一创建的TestOidcProvider

  1. 使用RAM管理员登录RAM控制台

  2. 在左侧导航栏,选择身份管理 > 角色

  3. 角色页面,单击创建角色

  4. 创建角色页面,选择可信实体类型为身份提供商,然后单击下一步

  5. 输入角色名称备注

  6. 选择身份提供商类型为OIDC

  7. 选择身份提供商并设置限制条件。

    支持的限制条件如下表所示:

    限制条件关键字

    说明

    是否必选

    示例

    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****

  8. 单击完成

  9. 单击关闭

步骤三:为RAM角色授权

您可以根据实际需要,为步骤二创建的RAM角色testoidc授予访问阿里云资源的权限。

  1. 使用RAM管理员登录RAM控制台

  2. 在左侧导航栏,选择身份管理 > 角色

  3. 角色页面,单击目标RAM角色操作列的新增授权

    image

    您也可以选中多个RAM角色,单击角色列表下方的新增授权,为RAM角色批量授权。

  4. 新增授权面板,为RAM角色授权。

    1. 选择资源范围。

      • 账号级别:权限在当前阿里云账号内生效。

      • 资源组级别:权限在指定的资源组内生效。

        说明

        指定资源组授权生效的前提是该云服务及资源类型已支持资源组,详情请参见支持资源组的云服务

    2. 选择授权主体。

      授权主体即需要添加权限的RAM角色。系统会自动选择当前的RAM角色。

    3. 选择权限策略。

      权限策略是一组访问权限的集合。支持批量选中多条权限策略。

      • 系统策略:由阿里云创建,策略的版本更新由阿里云维护,用户只能使用不能修改。更多信息,请参见支持RAM的云服务

        说明

        系统会自动标识出高风险系统策略(例如:AdministratorAccess、AliyunRAMFullAccess等),授权时,尽量避免授予不必要的高风险权限策略。

      • 自定义策略:由用户管理,策略的版本更新由用户维护。用户可以自主创建、更新和删除自定义策略。更多信息,请参见创建自定义权限策略

    4. 单击确认新增授权

  5. 单击关闭

步骤四:在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的流程,其中简化了标准协议要求的部分步骤。

  1. 搭建一个客户端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";
          }
      }
  2. 登录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_tokenid_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
  3. 解析OIDC Token。

    您可以对步骤2获取的结果进行简单地解析,将headerpayload展开。

    请求示例:

    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访问有权限的阿里云资源。