自研应用接入 SSO

本文介绍如何使用OIDC协议授权码模式,将自研应用接入IDaaS的单点登录(SSO)服务。帮助开发者轻松实现应用的SSO功能,提升用户体验和管理效率。

背景介绍

IDaaS 采用标准的 OIDC 协议授权码模式来支持常规企业自研应用接入。钉钉、微信等社交身份均采用 OAuth 协议实现扫码登录。IDaaS 采用的 OIDC 协议是 OAuth 协议的升级版。

说明

兼容 OAuth:OIDC(OpenID Connect) 1.0 协议在 OAuth2.0 协议之上建立了用户身份层,OIDC 协议也因此兼容 OAuth2.0 协议。OIDC 授权码模式和 OAuth2.0 授权码模式流程一致,区别是 OIDC 对用户信息端点进行了标准化,并在 Token 端点会返回用户的 ID Token。

授权码流程介绍

自研应用 SSO 对接采用 OIDC 授权码模式。您的应用只需要完成与 IDaaS 之间的两个接口交互(授权端点、令牌端点),即可完成 SSO 的主体流程。登录将由 IDaaS 完全托管,您的应用只需解析登录结果即可。

a

对接 SSO

  1. 创建自研(或 OIDC 协议)应用

    您需要在 IDaaS 中创建一个自研应用或标准协议(OIDC)应用,并获取应用密钥。若您已经获取到,可以跳过这个步骤。请参考自研应用完成应用创建。

    在该应用的通用配置标签页中,即可获取到 client_idclient_secret。这对密钥将用于后续接口请求。

    image

    若希望对密钥进行管理或替换,请参考应用的 基本配置

  2. 请求授权端点 Authorization Endpoint

    下面步骤需要应用开发者处理。

    在用户尝试访问您的应用时,应用需要判断当前是否有可用的已登录身份。

    若用户需要登录,您需要向 IDaaS 发起授权登录的请求。您可以在应用 > 登录访问标签页,下方的应用配置信息中获取应用的授权端点。

    image

    请参照如下示例,在授权端点地址的基础上,拼装出完整的授权请求访问地址,并在浏览器中发起 302 跳转。

    {{授权端点 Authorization Endpoint}}?
      client_id=app_***&
      redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F***&
      response_type=code&
      scope=openid&
      state=525f49cc-***

    字段

    必填

    示例

    说明

    client_id

    app_michs7r****6pye

    上一步骤获取到的 client_id。

    scope

    openid email profile

    自研应用默认配置为 openid email profile,意味着应用可获取到已登录账户的 ID、用户名、邮箱信息。若您使用 OIDC 标准模板应用,管理员还可在配置页选择 phone。scope 对应的字段范围,可见文档末尾说明。

    response_type

    code

    此值固定为 code,代表采用授权码模式。

    redirect_uri

    http://localhost:3000/user/oauth2/aliyunidaas/callback

    用户登录完成后,IDaaS 向应用返回登录结果的重定向地址。该地址应该为应用接收 IDaaS 参数的中继地址,并能接收授权码 code

    state

    525f49cc-87c4-4655-b79c-4c4f971b1ad1

    state 是由应用生成的随机字符串,建议长度 32 位以上。该 state 值会在后续步骤返回给应用,应用届时应验证 state 值是否与发起传入的一致,以确保同一会话,以规避 XSRF 安全漏洞。非必填,但强烈建议填写。

  3. 用户自助登录

    若请求顺利,向应用授权端点的请求,会跳转到 IDaaS 登录页。

    用户可以通过任意已配置的登录方式,完成身份验证。IDaaS 提供了多样的、不同安全级别的登录能力,包括钉钉扫码登录、短信登录等。详情参考:登录方式

    登录成功后,浏览器会 302 跳转回应用指定的 redirect_uri,并在 URL 参数中携带 code 和 state 参数。

    参考示例:

    {{redirect_uri}}?
      code=CO***&
      state=525f49cc-***

    字段

    示例

    说明

    code

    COE59pkCTm4A9nmowJUsfsfarGEaiShj3TuDc7NCzLCYu9

    即授权码。在获取令牌的请求中使用。

    state

    525f49cc-87c4-4655-b79c-4c4f971b1ad1

    应用接收到后,应确保与调用授权端点时传入的 state 一致。

  4. 请求令牌端点 Token Endpoint

    上一步接收到 code 授权码后,应用应使用获得的 code 向令牌端点(Token Endpoint)发起 POST 请求。

    与上述授权端点一样,令牌端点也可以在应用 > 登录访问标签页下方的应用配置信息中获取到。

    image

    请求示例如下:

    POST /v2/<instance_id>/<app_id>/oauth2/token HTTP/1.0
    Host: eiam-api-cn-hangzhou.aliyuncs.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
      &code=n0esc3N*****5acc3f0ogp4
      &client_id=s6BhdR*****kqt3
      &client_secret=7Fjfp0ZBr1*****KtDRbnfVdmIw
      &redirect_uri=http%3A%2F%2Fwww.example.com%2Fsso%2Fcallback

    响应结果如下:

    {
      "token_type": "Bearer",
      "access_token": "ATM4SoVDqWgUq***********wk3ZS5mtn6fcSp8NH8",
      "expires_in": 1200,
      "expires_at": 1644843164,
      "id_token": "eyJraWQiOiJLRVkyV************gRIadj-frOIRFChA"
    }

    至此,您的用户已经成功完成登录。您可以选择以下两种方式,进一步获取当前登录身份信息,并完成应用侧登录态的创建:

    1. 使用响应结果中的 id_token,经过验证后,直接拿到用户标识。

    2. 使用响应结果中的 access_token,调用 IDaaS 用户端点,获取当前已登录用户信息。

    具体获取方式请参考下面章节。

    重要

    可以获取到的用户数据范围,由第一步授权端点请求中的 scope 参数指定。

  5. 通过程序解析id_token

    id_token是一个包含身份信息的签名令牌信息(JWT 格式)。IDaaS 签发的id_token中,包含着解码即明文可见的用户数据,以及签名Signature。

    为了方便理解,您可以将id_token完整内容,粘贴到JWT 解码网站中,查看其包含的内容。

    内容示例如下:

    {
      "kid": "KEY2Ty1qL6u21NGKmccv3jwfd2ndmgtQPnag",
      "alg": "RS256"
    }.{
      "sub": "user_uyvefotjn7kpbejfmxoos3rtmm",
      "jti": "jwt_aaaac7xyhclac6aqkgtjaxsthw5yotn5d77pmki",
      "iss": "https://pre-eiam-api-cn-hangzhou.aliyun-inc.com/v2/********/oidc",
      "iat": 1644841965,
      "nbf": 1644841965,
      "exp": 1644842265,
      "aud": "app_mhylgo3iairjqjdx5eop6uaf34",
      "at_hash": "XHEaGpMooM9zvQXaMzCNEA",
      "name": "testuser",
      "preferred_username": "testuser",
    }.[Signature]

    在使用其中内容进行应用登录前,您需要对签名 [Signature] 进行验证,以确保令牌是 IDaaS 签发,而非任何其他三方,以保障登录的安全性。这一安全性是必须的。

    1. 获取验签公钥

      在进行签名验证之前,需要首先获得 IDaaS 公开的验签公钥端点。

      仍然在应用 > 登录访问标签页下方的应用配置信息中获取,我们可以看到应用的验签公钥端点。image

      应用可通过访问这一端点,获取到当前的公钥信息。您也可以直接将此地址在浏览器中打开,即可展示公钥信息。

      示例如下:

      {
        "keys": [
          {
            "kty": "RSA",
            "e": "AQAB",
            "use": "sig",
            "kid": "KEYkYnc55G********CTvT7So44RGDYdbfs",
            "n": "pXmYkIpy1vaNjTMclU86BQjfmDhjlqMAX8ySVvh9gO-nae4ayvG_*********-v4gP27T7u6bUy0GXTlh3eKE0v1LYB81nfqjF2uazlPwPR5yYOhhWcK-gMNByLfE3CnkDc1YGwA3dZmIz-ZjOCKy8xLaBuqjrvwn5tpMpAoYEEaH4jIm7unTdhbKEKspNR-UXKD8q9RppMh5Tn2sB6oPHlQANudJDgqSwEOevIrdmHU0Zqxrb9cscGH9hH0QjmYEu72yI8BVeliPo3jK6JIoqCIcj5K_t8BJlFQ9QLJ8_o9tmd3BFv5_LVsh4BKGw"
          }
        ]
      }

      接下来您即可以通过代码完成验签并获取 id_token 中的内容了。

    2. 验签并登录

      您可以通过JWT官方库列表找到对应语言的工具,并在代码中使用工具对 id_token 进行验签与解析。

      下面以 Java 库:org.bitbucket.b_c:jose4j 为例。

      首先加入对应的 Maven 依赖:

      <dependency>
        <groupId>org.bitbucket.b_c</groupId>
        <artifactId>jose4j</artifactId>
        <version>0.7.12</version>
      </dependency>

      代码示例如下:

      import org.jose4j.jwk.JsonWebKey;
      import org.jose4j.jwk.JsonWebKeySet;
      import org.jose4j.jwt.JwtClaims;
      import org.jose4j.jwt.consumer.JwtConsumer;
      import org.jose4j.jwt.consumer.JwtConsumerBuilder;
      
      public class IdTokenTest {
      
          public static void main(String[] args) throws Exception {
              // EIAMIssuer标识(OIDC签发者URL)
              String issuer = "https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_padyrlux3mphrlsex4uonyqhxu/**********/oidc";
              // 当前应用的唯一标识(从EIAM获取)
              String appId = "app_mkif4*****pxpzbasqmu";
              // 请参照如下方式,设定解析用的应用公钥(JSON格式,用于验证JWT签名)
              String jwkJson = "{\n" + "  \"keys\": [\n" + "    {\n" + "      \"kty\": \"RSA\",\n" + "      \"e\": \"AQAB\",\n" + "      \"use\": \"sig\",\n" + "      \"kid\": \"KEY2H82C2at57itnW4onT3p1ySjwH4nirjCk\",\n" + "      \"n\": \"w7Jl3fAUJp_9GuxV*****QsOA4lnXR5OD4kF4QbIeBiDiH8_MThrFi9k2MB6YMkSzf5JfIkpAS3JCqZ7k6Wooydp4pzaZNZAk3SGzdsa022RmAT" + "-Iayi4Yj6J9tSdTQCjwh2XkzzsIxA_Hla8rWiQ8Vhw1" +
                  "-7QArgObfe67nSR7LxD55MFLxk9FU0*****RlGhrQGE_0LUuGWtCJG1r1e6aKquyswfxxAr3Rvj8QGIeJrG0R1Pv8m8d1_5OdULhB7149VqjM6D98WFjab0U2SNv0UlREZXTcS4p-2QNm_1egYRRpJEY_00FZqNSYsmErMGepYhO_61KoGqd8cphWQ\"\n" + "    }\n" + "  ]\n" + "}";
              String jwt = "eyJraWQiOiJLRVkySDgyQzJhdD*****uaXJqQ2siLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2Vy*****lmNjRjZjR3amFrbnBieGpjd3V1IiwianRpIjoiand0X2FhYWFkYWllYTc2eWg1cW0zcm11bnoyeGg0eHd5aTJzZHBoNjR6aSIsImlzcyI6Imh0dHBzOi8vZWlhbS1hcGktY24taGFuZ3pob3UuYWxpeXVuY3MuY29tL3YyL2lkYWFzX3BhZHlybHV4M21waHJsc2V4NHVvbnlxaHh1L2FwcF9ta2lmNGR3bHBlaDZkbnM0cHhwemJhc3FtdS9vaWRjIiwiaWF0IjoxNjUzNjMwMDQxLCJuYmYiOjE2NTM2MzAwNDEsImV4cCI6MTY1MzYzMDM0MSwiYXVkIjoiYXBwX21raWY0ZHdscGVoNmRuczRweHB6YmFzcW11IiwibmFtZSI6InRlc3QiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0IiwidXBkYXRlZF9hdCI6MTY1MzYyODU5MH0.pAsUNB8OkdpIxJMZRfLJ7Pa31tsJyl44a1jVIlvdQxwOtPULAwrFxnB0X3eQx89hUGCdvYWl9FO9o-5kT7L-RER0wJYz9YNKqrVNBnaRwINRZyeYLRVurWMMzODQz-V0ULd9raM1M_i2f_SoWFs1gPFtYh_ijUARHISi7Q3q93ZfAuY8Lq2Nq07QunmDbosvioUd5wJG7WCxW5XXZYDUQe9p5IEYd1MSvnWuTOLbg7rKn0Vm4dNYGWjz1WuoAyCsc_QxOCqpmQ_2czoqPeN-SvPJAQ2CykLk7DSnGpABw1aNrjDidLS9Beqsga9VDCth86sk_0lyTZOaORtUrfVTtQ";
              //解析JWK公钥集合
              JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson);
              //创建JWT验证器
              JwtConsumer jwtConsumer = createJwtConsumer(jsonWebKeySet, issuer, appId);
              //执行验签并解析claims
              JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt);
              // 已经验签完成,打印输出 id_token 中包含的用户信息
              System.out.println(jwtClaims);
          }
      
          // 验签工具方法
          public static JwtConsumer createJwtConsumer(JsonWebKeySet jsonWebKeySet, String issuer, String appId) {
              // 使用建造者模式配置JWT验证器
              final JwtConsumerBuilder jwtConsumerBuilder = new JwtConsumerBuilder();
              // 必须验证的字段配置
              jwtConsumerBuilder.setExpectedIssuer(issuer);
              jwtConsumerBuilder.setRequireIssuedAt();
              jwtConsumerBuilder.setRequireExpirationTime();
              jwtConsumerBuilder.setAllowedClockSkewInSeconds(60);
              jwtConsumerBuilder.setExpectedAudience(appId);
              // 设置公钥解析器(通过kid匹配JWK)
              jwtConsumerBuilder.setVerificationKeyResolver((jws, nestingContext) - > {
                  // 从JWT头部获取kid
                  final String signKeyId = jws.getKeyIdHeaderValue();
                  // 遍历JWK集合查找匹配的密钥
                  for (JsonWebKey jsonWebKey: jsonWebKeySet.getJsonWebKeys()) {
                      if (signKeyId.equals(jsonWebKey.getKeyId())) {
                          return jsonWebKey.getKey();
                      }
                  }
                  throw new RuntimeException("Cannot find verification key: " + signKeyId);
              });
              // 构建验证器实例
              return jwtConsumerBuilder.build();
          }
      }
      重要

      请将代码中的issuer、appId、jwkJsonjwt部分替换为您从EIAM获取的实际信息。

      输出示例如下:

      JWT Claims Set:{sub=user_dt6kj6yf64cf4wjaknpbxjcwuu, 
                      jti=jwt_aaaadaiea76yh5qm3rmunz2xh4xwyi2sdph64zi, 
                      iss=https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_padyrlux3mphrlsex4uonyqhxu/app_**********/oidc, 
                      iat=1653630041, 
                      nbf=1653630041, 
                      exp=1653630341, 
                      aud=app_**********, 
                      name=test, 
                      preferred_username=test, 
                      updated_at=1653628590
                     }

      由此获取到 IDaaS 中已登录身份信息,应用可用其顺利登录。

  6. 通过UserInfo端点获取用户信息

    除了可以通过解析 id_token 获取用户信息外,还可以通过 UserInfo 端点(用户信息端点)获取用户信息。

    仍然在应用 > 登录访问标签页下方的应用配置信息中获取,我们可以看到应用的验签公钥端点。

    image

    UserInfo请求遵循标准 RFC6750,请求示例如下:

    GET /v2/<instance_id>/<app_id>/oauth2/userinfo HTTP/1.0
    Host: eiam-api-cn-hangzhou.aliyuncs.com
    Authorization: Bearer <AccessToken>
    
    返回参数示例:
    {
        "sub": "user_dt6kj6yf64cf4wjaknpbxjcwuu",
        "name": "test",
        "preferred_username": "test",
        "updated_at": 1653899948
    }
    说明

    UserInfo 端点返回的业务字符与 id_token 中的字段是保持一致的,即在 “扩展 id_token” 中配置的字段也会在 UserInfo 端点中返回。

其他高级设置

若您对 OIDC 协议有深入了解,您可能会用到下列概念或能力,供参考。

  1. OIDC Discovery 应用发现端点说明

    OIDC 应用的 issuer 是对令牌发行方(即 IDaaS)的唯一标识,格式如下:

    https://<idaas-api-domain>/v2/<instance_id>/<application_id>/oidc

    尖括号中的参数如下:

    字段

    说明

    示例

    idaas-api-domain

    用户门户地址

    https://******.aliyunidaas.com

    instance_id

    实例 ID

    idaas_m********r2ed22e6m

    application_id

    应用 ID

    app_m********jy6rbau

    IDaaS 支持 OpenID Connect Discovery 1.0 标准,在 issuer 后再加上 /.well-known/openid-configuration 就是该应用的 OIDC 发现端点地址。

    通过请求发现端点,您可以自动发现如下端点信息。所有的请求端点都可以直接从应用配置信息中获取。

    端点

    说明

    authorization_endpoint

    授权端点

    device_authorization_endpoint

    设备模式 需要标准OIDC应用支持该功能,自研应用暂时不支持设备码流登录

    token_endpoint

    令牌端点

    revocation_endpoint

    令牌吊销端点

    userinfo_endpoint

    用户信息端点

    jwks_uri

    JWK公钥端点

  2. Scope 与字段权限的对应关系

    OIDC 中 id_token 中包含的用户信息与 scope 的对应关系如下:

    字段

    scope

    说明

    sub

    openid

    用户的 userId

    jti

    openid

    JWT 令牌ID,辅助字段

    iss

    openid

    JWT 签发的 issuer,辅助字段

    iat

    openid

    JWT 签发时间,辅助字段

    nbf

    openid

    JWT 令牌有效开始时间,辅助字段

    exp

    openid

    JWT 令牌过期时间,辅助字段

    aud

    openid

    即应用的 ClientID,辅助字段

    at_hash

    openid

    AccessToken 哈希值,辅助字段

    phone_number

    phone

    电话号码,比如 +86 130****5678

    phone_number_verified

    phone

    电话号码是否被验证过,目前默认电话号码是已验证

    email

    email

    电子邮箱,比如 al***@example.com

    email_verified

    email

    电子邮箱是否被验证过,目前默认电子邮箱是已验证

    name

    profile

    用户显示名

    preferred_username

    profile

    用户的 username

    updated_at

    profile

    用户资料最后更新时间

  3. 令牌端点支持的认证方式

    根据 OIDC 协议指明,IDaaS 提供灵活性,允许以下 4 种不同方式进行身份验证。

    在发现端点中返回的字段 token_endpoint_auth_methods_supported 指定了支持的认证方法。

    取值

    说明

    none

    用于 Public 客户端,通过 none 认证方式认证时 grant_type 不能是 client_credentials

    client_secret_basic

    按规范 RFC 6749 - The OAuth 2.0 Authorization Framework 实现

    client_secret_post

    按规范 RFC 6749 - The OAuth 2.0 Authorization Framework 实现

    client_secret_jwt

    按规范 OpenID Connect Core 1.0 实现

    上一步接收到 code 授权码,并验证请求合法(验证 state 与发起请求传入的一致)后,应用的后端服务,应使用获得的 code 向令牌端点(Token Endpoint)发起 POST 请求,请求示例如下:

    client_secret_basic 为例,令牌端点请求样例为:

    POST /token HTTP/1.0
    Host: api.aliyunidaas.com
    Authorization: Basic YXBwX21pY2hzN3I0*******cHllOkNTKioqKioq
    
    grant_type=authorization_code&
    code=COE59pkCTm4J*******arGEaiShj7NCzLCYu9

    更多说明参看 OIDC Core 1.0 规范。

  4. 应用 Client Secret 轮转

    请参考 基本配置 中密钥轮转章节说明。

相关标准