所有文章 > API安全 > OpenID Connect入门指南
OpenID Connect入门指南

OpenID Connect入门指南

身份、声明和令牌

一开始,有专有方法与外部身份提供商合作进行身份验证和授权。后来出现了 SAML(安全断言标记语言)——一种使用 XML 作为消息交换类型的开放标准。然后,出现了 OAuth 和 OAuth 2.0——同样是开放的,并且是一种使用 JSON 作为媒介的现代 RESTful 授权方法。现在,在 OAuth 2.0 上运行的“安全委托访问”的圣杯是 OpenID Connect(以下简称 OIDC)。

但是等等。OAuth 2.0 有什么问题?为了更好地理解,我们首先放弃“安全委托访问”这个术语。它太模糊了,导致身份验证 (authn) 和授权 (authz) 之间的混淆。

如果没有安全的外部身份验证和授权,您就必须相信每个应用程序和每个开发人员不仅会考虑您的最大利益和隐私,而且还知道如何保护您的身份并愿意遵循安全最佳实践。这是一个相当艰巨的任务,对吧?借助 OIDC(OpenID Connect),您可以使用受信任的外部提供商向给定应用程序证明您就是您所说的那个人,而无需授予该应用程序访问您凭据的权限。

OAuth 2.0 将很多细节留给了实施者。例如,它支持范围,但未指定范围名称。它支持访问令牌,但未指定这些令牌的格式。使用 OIDC,定义了许多特定的范围名称,每个名称都会产生不同的结果。OIDC 既有访问令牌,也有 ID 令牌。ID 令牌必须是 JSON Web 令牌 (JWT)。由于规范规定了令牌格式,因此可以更轻松地跨实现使用令牌。

关键概念:范围、声明和响应类型

在深入研究 OIDC 的细节之前,让我们先回顾一下并讨论如何与它交互。

所有 OIDC 交互都涉及两个主要参与者:OpenID 提供商 (OP) 和依赖方 (RP)。OP 是一个OAuth 2.0服务器,能够对最终用户进行身份验证,并向依赖方提供有关身份验证结果和最终用户的信息。依赖方是一个 OAuth 2.0 应用程序,它“依赖”OP 来处理身份验证请求。

通常,您可以通过使用 HTTP GET 访问端点来启动 OIDC 交互/authorization。许多查询参数指示您在身份验证后可以返回的内容以及您可以访问的内容(授权)。

通常,您需要/token使用 HTTP POST 访问端点来获取用于进一步交互的令牌。

OIDC 还具有一个/introspect用于验证令牌的端点和一个/userinfo用于获取用户身份信息的端点。

所有上述端点都是惯例,但 OP 可以将其定义为任何内容。OIDC 的一大改进是元数据机制,用于从提供商发现端点。例如,如果您导航到:https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/.well-known/openid-configuration,您将获得一个 JSON 格式的文档,其中包含标识 OP(在本例中为 Okta)所有可用端点的元数据。

什么是范围?

范围是用空格分隔的标识符列表,用于指定请求的访问权限。有效的范围标识符在RFC 6749中指定。

OIDC 有许多内置范围标识符。openid是必需的范围。所有其他范围(包括自定义范围)都是可选的。内置范围包括:

范围目的
轮廓请求访问默认配置文件声明
电子邮件请求访问电子邮件和 email_verified 声明
地址请求访问地址索赔
电话请求访问 phone_number 和 phone_number_verified 声明

默认的配置文件声明为:

  • name
  • family_name
  • given_name
  • middle_name
  • nickname
  • preferred_username
  • profile
  • picture
  • website
  • gender
  • birthdate
  • zoneinfo
  • locale
  • updated_at

注意范围是如何与声明关联的。你可能会问,声明到底是什么?

什么是索赔?

简而言之,声明是包含有关用户的信息以及有关 OIDC 服务的元信息的名称/值对。规范中的官方定义是“关于实体的断言信息”。

以下是一组典型的主张:

{
"family_name": "Silverman",
"given_name": "Micah",
"locale": "en-US",
"name": "Micah Silverman",
"preferred_username": "micah.silverman@okta.com",
"sub": "00u9vme99nxudvxZA0h7",
"updated_at": 1490198843,
"zoneinfo": "America/Los_Angeles"
}

上面包含了一些声明profile。这是因为对用户信息的请求是使用通过范围获得的令牌进行的profile。换句话说,发出的请求会导致发出令牌。该令牌包含基于原始请求中指定的范围的某些信息。

什么是响应类型?

使用 OIDC 时,您会听到各种“流程”的讨论。这些流程用于描述不同的常见身份验证和授权场景。考虑因素包括应用程序的类型(如基于 Web 或原生移动应用程序)、您希望如何验证令牌(在应用程序中还是在后端),以及您希望如何访问其他身份信息(进行另一个 API 调用或将其直接编码到令牌中)。

主要有三种流程:授权码隐式混合response_type。这些流程由请求中的查询参数控制/authorization。在考虑使用哪种流程时,请考虑前向通道与后向通道的要求。前向通道是指直接与 OpenID 提供商 (OP) 交互的用户代理(例如 SPA 或移动应用程序)。当需要前向通道通信时,隐式流程是一个不错的选择。后向通道是指与 OP 交互的中间层客户端(例如 Spring Boot 或 Express)。当需要后向通道通信时,授权码流程是一个不错的选择。

授权码流程使用response_type=code。身份验证成功后,响应将包含一个code值。此代码稍后可以交换为 和access_tokenid_token请稍等,稍后我们将更深入地讨论令牌。)当架构中包含“中间件”时,此流程非常有用。中间件具有 和client idclient secret这是code通过点击/token端点将 交换为令牌所必需的。然后可以将这些令牌返回给最终用户应用程序(例如浏览器),而无需浏览器知道client secret。此流程允许通过使用 实现长寿命会话refresh tokens。 的唯一目的refresh tokens是获取新的access tokens以延长用户会话。

隐式流程使用response_type=id_token tokenresponse_type=id_token。身份验证成功后,响应将在第一种情况下包含id_token和,在第二种情况下仅包含 。当您的应用程序直接与后端对话以获取没有中间件的令牌时,此流程很有用。它不支持长寿命会话。access_tokenid_token

混合流将上述两种流以不同的组合方式组合在一起 – 以适合用例的方式。例如response_type=code id_token。此方法可实现一种场景,您可以在应用程序中拥有一个长寿命会话,并立即从/authorization端点获取令牌。

关于Token

有了范围、声明和响应类型的基础,我们现在可以讨论令牌了!OIDC 中有三种类型的令牌:id_tokenaccess_tokenrefresh_token

ID Token

根据OIDC 规范, Anid_tokenJWT。这意味着:

  • 用户身份信息被编码到令牌中,并且
  • 该令牌可以被最终验证,以证明其未被篡改。

规范中有一组规则id_token用于验证。 在 中编码的声明中id_token有一个到期日期 ( exp),必须在验证过程中遵守。 此外,JWT 的签名部分与密钥一起使用,以验证整个 JWT 未以任何方式被篡改。

访问Token

访问令牌用作不记名令牌。不记名令牌意味着不记名令牌持有者无需进一步识别即可访问授权资源。因此,保护不记名令牌非常重要。如果我能以某种方式获得并“持有”您的访问令牌,我就可以伪装成您。

为了提高安全性,这些令牌通常具有较短的使用寿命(由其到期时间决定)。也就是说,当访问令牌到期时,用户必须再次进行身份验证才能获得新的访问令牌,从而限制其为不记名令牌这一事实的暴露。

尽管 OIDC 规范没有强制要求,Okta 使用 JWT 作为访问令牌,因为(除其他外)到期时间内置于令牌中。

OIDC 指定/userinfo返回身份信息且必须受到保护的端点。出示访问令牌即可使端点可访问。

以下是使用HTTPie的示例:

http https://micah.oktapreview.com/oauth2/.../v1/userinfo

HTTP/1.1 400 Bad Request
...
WWW-Authenticate: Bearer error="invalid_request", error_description="The access token is missing."
...

让我们使用过期的访问令牌再试一次:

http https://micah.oktapreview.com/oauth2/.../v1/userinfo \
Authorization:"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ..."

HTTP/1.1 401 Unauthorized
...
WWW-Authenticate: Bearer error="invalid_token", error_description="The token has expired."
...

最后,让我们尝试使用有效的访问令牌:

http https://micah.oktapreview.com/oauth2/.../v1/userinfo \
Authorization:"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ..."

HTTP/1.1 200 OK
...
{
"family_name": "Silverman",
"given_name": "Micah",
"groups": [
"ABC123",
"Everyone"
],
"locale": "en-US",
"name": "Micah Silverman",
"preferred_username": "micah+okta@afitnerd.com",
"sub": "...",
"updated_at": 1490198843,
"zoneinfo": "America/Los_Angeles"
}

刷新Token

刷新令牌用于获取新的访问令牌。通常,刷新令牌是长期有效的,而访问令牌是短期有效的。这允许长期存在的会话在必要时可以被终止。以下是一个典型的场景:

  1. 用户登录并获取访问令牌和刷新令牌
  2. 应用程序检测到访问令牌已过期
  3. 应用程序使用刷新令牌获取新的访问令牌
  4. 重复 2 和 3,直到刷新令牌过期
  5. 刷新令牌过期后,用户必须重新进行身份验证

你可能会问:为什么要这样做?这种方法在用户体验和安全性之间取得了平衡。想象一下,如果用户以某种方式受到威胁。或者,他们的订阅到期了。或者,他们被解雇了。在任何时候,管理员都可以撤销刷新令牌。然后,上面的第三步将失败,用户将被迫(尝试)通过身份验证建立新会话。如果他们的帐户已被暂停,他们将无法进行身份验证。

识别类型

区分不同的 token 类型有时会令人困惑。以下是快速参考:

  1. ID 令牌携带在令牌本身中编码的身份信息,该令牌必须是 JWT
  2. 访问令牌用于通过将资源用作承载令牌来获取对资源的访问权限
  3. 刷新令牌仅用于获取更多访问令牌

OIDC 实际应用

您从 OIDC 流中返回的令牌和/userinfo端点的内容取决于请求的流类型和范围。您可以在OIDC 流测试站点scope上实时查看。在这里,您可以为和设置不同的切换response_type,这决定了您的应用的流类型。

您的用例将决定使用哪种流程。您是否正在构建需要直接与 OpenID 提供程序 (OP) 交互的 SPA 或移动应用程序?您是否有可以与 OP 交互的中间件,例如 Spring Boot 或 Node.js Express?下面,我们将深入探讨一些可用的流程以及何时适合使用它们。

授权码流程

如果您有一个连接到 OIDC OP 的中间件客户端,并且(不一定)希望令牌返回到最终用户应用程序(例如浏览器),则这种方法很合适。这也意味着最终用户应用程序永远不需要知道密钥。

以下是使用 Okta 开始此流程的示例:

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=code&scope=openid&state=little-room-greasy-pie&nonce=b1e7b75d-6248-4fc7-bad0-ac5ae0f2e581&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

让我们详细分析一下:

钥匙价值描述
组织 URLhttps://micah.okta.comOkta 租户
授权网址/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/授权贵组织的默认授权端点
客户端 ID0oa2yrbf35Vcbom491t7Okta 中定义的 OIDC 应用程序的客户端 ID
响应类型代码指示代码流的响应类型
范围开放标识openid 范围是必需的
状态小房间油腻馅饼流程结束时返回随机值
随机数b1e7b75d-6248-4fc7-bad0-ac5ae0f2e581编码到 id_token 中的随机值,用于稍后验证
重定向 urihttps%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_resultOP 重定向到的 URL 编码 URL

浏览器中的情况如下:

请注意,在新屏幕上,您将被重定向回redirect_uri最初指定的页面:

在后台,会使用固定的用户名和密码建立会话。则当您单击链接时,您将被重定向到登录,然后重定向回同一页面。

在上面的屏幕截图中,您可以看到返回的代码和原始的state

现在,中间层(在本例中为 Spring Boot 应用程序)可以将该代码交换为id_token和。这个中间层将验证我们之前在授权请求中发送的状态,并使用客户端密钥发出请求,为用户创建和。access_token/tokenaccess_tokenid_token

隐式流

当您想要直接与 OIDC OP 交互的客户端(例如单页应用程序或移动应用程序)工作时,这是一种合适的方法。

以下是使用 Okta 开始此流程的示例:

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=id_token+token&scope=openid&state=shrill-word-accessible-iron&nonce=f8c658f0-1eb9-4f8d-8692-5da4e2f24cf0&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

它与授权码流程几乎相同,只是 是response_typeid_tokentoken下面id_token+token,我们将详细介绍这些令牌中的内容及其驱动方式,但请记住:id_token编码身份信息,access_token(如果token指定 则返回)是用于访问资源的承载令牌。Okta 还使用 JWT 进行access_token,这样可以将其他信息编码到其中。

以下是浏览器中的流程:

您将被重定向回redirect_uri最初指定的位置(带有返回的令牌和原始的state):

应用程序现在可以在id_token本地验证。使用/introspect端点来验证access_token。它还可以使用access_token作为承载令牌来访问受保护的资源,例如/userinfo端点。

混合流

当您希望最终用户应用程序能够立即访问短期令牌(例如id_token身份信息),并且还希望使用后端服务通过刷新令牌将授权码交换为更长期令牌时,这是一种合适的方法。

它是授权代码和隐式代码流的组合。您可以通过查看它response_type必须包含code中的一个或两个来发现它id_tokentoken

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=code+id_token+token&scope=openid&state=shrill-word-accessible-iron&nonce=f8c658f0-1eb9-4f8d-8692-5da4e2f24cf0&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

浏览器中的情况如下:

您将被重定向回redirect_uri最初指定的页面(包含返回的代码、令牌和原始内容state):

在下一篇文章中,我们将深入研究如何控制这些令牌中的内容,但现在先来介绍一下:

这些令牌是在启用所有默认范围的混合流下产生的。

/userinfo以下是使用access_token作为承载令牌的端点的响应:

{
"sub": "00u2yulup4eWbOttd1t7",
"name": "Okta OIDC Fun",
"locale": "en-US",
"email": "okta_oidc_fun@okta.com",
"preferred_username": "okta_oidc_fun@okta.com",
"given_name": "Okta OIDC",
"family_name": "Fun",
"zoneinfo": "America/Los_Angeles",
"updated_at": 1499922371,
"email_verified": true
}

Token中有什么?

根据 OIDC 规范,与身份相关的信息有两个主要来源。一个来源是编码到id_token JWT中的信息。另一个来源是来自端点的响应/userinfo,可以使用access_token不记名令牌访问。在 Okta,我们也选择将访问令牌设为 JWT,这提供了第三个信息来源。(您会在许多 OIDC 实现中看到这一点。)

请求中有很多查询参数组合/authorization,它们决定了哪些信息将被编码到 中。影响最终在返回的令牌和端点id_token中找到的内容的两个查询参数是和。/userinforesponse_typescope

OIDC 响应类型

目前,我们将把scope重点放在一边response_type。在下面的例子中,我们只使用范围openid(必需)和email。我们还将使用隐式流,因为它会立即返回令牌。

给出这个请求:

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=token&scope=openid+email&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

请注意,这response_type=token将产生一个access_token。OIDC 规范中对访问令牌没有特定的格式要求,但在 Okta 中我们使用 JWT。查看返回的令牌,我们会看到:

{
"active": true,
"scope": "openid email",
"username": "okta_oidc_fun@okta.com",
"exp": 1501531801,
"iat": 1501528201,
"sub": "okta_oidc_fun@okta.com",
"aud": "test",
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"jti": "AT.upPJqU-Ism6Fwt5Fpl8AhNAdoUeuMsEgJ_VxJ3WJ1hk",
"token_type": "Bearer",
"client_id": "0oa2yrbf35Vcbom491t7",
"uid": "00u2yulup4eWbOttd1t7"
}

这主要是资源信息,包括到期日期(exp)和用户 ID(uid)。

如果我们想要获取用户的身份/userinfo信息,我们必须使用作为承载令牌的端点。使用HTTPieaccess_token的情况如下:

http https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/userinfo Authorization:"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ..."
HTTP/1.1 200 OK
...
{
"sub": "00u2yulup4eWbOttd1t7",
"email": "okta_oidc_fun@okta.com",
"email_verified": true
}

我们返回了subemailemail_verified声明。这是因为scope=openid+email原始请求的默认值。我们将在范围部分查看一些更详细的响应。

我们来尝试另一个请求:

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=id_token&scope=openid+email&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

这次,我使用 请求 ID 令牌response_type=id_token。响应是 JWT(符合 OIDC 规范的要求),其中包含以下信息:

{
"sub": "00u2yulup4eWbOttd1t7",
"email": "okta_oidc_fun@okta.com",
"ver": 1,
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud": "0oa2yrbf35Vcbom491t7",
"iat": 1501528456,
"exp": 1501532056,
"jti": "ID.4Mmzy2kj5_B8nGZ_PT4dt8-fzu1tA2W3C5dbEF-N6Us",
"amr": [
"pwd"
],
"idp": "00o1zyyqo9bpRehCw1t7",
"nonce": "c96fa468-ca1b-46f0-8974-546f23f9ee6f",
"email_verified": true,
"auth_time": 1501528157
}

请注意,我们将subemail声明直接编码在 JWT 中。在这种类型的隐式流程中,我们没有针对/userinfo端点使用的承载令牌,因此身份信息直接嵌入到 JWT 中。

最后我们来看看最后一类隐式流:

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=id_token+token&scope=openid+email&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

在这里,我们请求响应中的id_token和。access_token

我们的access_token主张与以前相同。的id_token主张如下:

{
"sub": "00u2yulup4eWbOttd1t7",
"email": "okta_oidc_fun@okta.com",
"ver": 1,
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud": "0oa2yrbf35Vcbom491t7",
"iat": 1501528536,
"exp": 1501532136,
"jti": "ID.fyybPizTmYLoQR20vlR7mpo8WTxB7JwkxplMQom-Kf8",
"amr": [
"pwd"
],
"idp": "00o1zyyqo9bpRehCw1t7",
"nonce": "c96fa468-ca1b-46f0-8974-546f23f9ee6f",
"auth_time": 1501528157,
"at_hash": "T7ij7o69gBtjo6bAJvaVBQ"
}

请注意,这次的信息较少id_token(在本例中,没有email_verified声明)。因为我们还请求了access_token,所以我们预计会从端点获取其余可用的身份信息(基于范围)/userinfo。在这种情况下,它产生的信息与之前我们仅请求access_token

OIDC 范围

将所有可用范围与所有可能的响应类型相结合,可呈现大量信息:确切地说是 48 种组合。首先,我将列举每个范围产生的结果,然后我们将查看结合request_type和的几个真实示例scope

首先要注意的是,不同的范围会对端点中编码id_token和返回的信息产生影响。以下是范围和结果声明的表格。

范围由此产生的索赔
开放标识(所有 OIDC 流程都需要)
轮廓姓名、姓氏、名字、中间名、昵称、首选用户名
简介(续)个人资料、图片、网站、性别、出生日期、区域信息、语言环境、updated_at
电子邮件电子邮件,email_verified
地址地址
电话phone_number, phone_number_verified

让我们使用所有可能的(默认)范围类型尝试每个隐式流。

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=token&scope=openid+profile+email+address+phone&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

与之前相比,结果的唯一区别access_token是所有范围都被编码到scp数组声明中。

这次,当我使用access_token来访问/userinfo端点时,我得到了更多信息:

http https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/userinfo Authorization:"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ..."
HTTP/1.1 200 OK
...
{
"sub": "00u2yulup4eWbOttd1t7",
"name": "Okta OIDC Fun",
"locale": "en-US",
"email": "okta_oidc_fun@okta.com",
"preferred_username": "okta_oidc_fun@okta.com",
"given_name": "Okta OIDC",
"family_name": "Fun",
"zoneinfo": "America/Los_Angeles",
"updated_at": 1499922371,
"email_verified": true
}

注意:虽然这不是从profile范围定义的声明的完整列表,但它是我的 Okta 中的用户具有价值的所有声明。

让我们尝试id_token隐式流程(仍然具有所有默认范围):

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=id_token&scope=openid+profile+email+address+phone&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

id_token以下是我收到的编码内容:

{
"sub": "00u2yulup4eWbOttd1t7",
"name": "Okta OIDC Fun",
"locale": "en-US",
"email": "okta_oidc_fun@okta.com",
"ver": 1,
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud": "0oa2yrbf35Vcbom491t7",
"iat": 1501532222,
"exp": 1501535822,
"jti": "ID.Zx8EclaZmhSckGHOCRzOci2OaduksmERymi9-ad7ML4",
"amr": [
"pwd"
],
"idp": "00o1zyyqo9bpRehCw1t7",
"nonce": "c96fa468-ca1b-46f0-8974-546f23f9ee6f",
"preferred_username": "okta_oidc_fun@okta.com",
"given_name": "Okta OIDC",
"family_name": "Fun",
"zoneinfo": "America/Los_Angeles",
"updated_at": 1499922371,
"email_verified": true,
"auth_time": 1501528157
}

所有(可用的)身份信息都被直接编码到令牌中,因为我没有承载令牌来访问端点/userinfo

最后,让我们尝试隐式流程的最后一种变体response_type=id_token+token

https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize?client_id=0oa2yrbf35Vcbom491t7&response_type=code+id_token+token&scope=openid+profile+email+address+phone&state=aboard-insect-fresh-smile&nonce=c96fa468-ca1b-46f0-8974-546f23f9ee6f&redirect_uri=https%3A%2F%2Fokta-oidc-fun.herokuapp.com%2Fflow_result

在这种情况下,我们将一些声明编码为id_token

{
"sub": "00u2yulup4eWbOttd1t7",
"name": "Okta OIDC Fun",
"email": "okta_oidc_fun@okta.com",
"ver": 1,
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud": "0oa2yrbf35Vcbom491t7",
"iat": 1501532304,
"exp": 1501535904,
"jti": "ID.1C2NQext2hM0iJy55cLc_Ryc45urVYC1wJ0S-KebkpI",
"amr": [
"pwd"
],
"idp": "00o1zyyqo9bpRehCw1t7",
"nonce": "c96fa468-ca1b-46f0-8974-546f23f9ee6f",
"preferred_username": "okta_oidc_fun@okta.com",
"auth_time": 1501528157,
"at_hash": "GB5O9CpSSOUSfVZ9CRekRg",
"c_hash": "mRNStYQm-QU4rwcfv88VKA"
}

如果我们使用结果access_token来到达/userinfo端点,在这种情况下,我们会得到:

http https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/userinfo Authorization:"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ..."
HTTP/1.1 200 OK
...
{
"sub": "00u2yulup4eWbOttd1t7",
"name": "Okta OIDC Fun",
"locale": "en-US",
"email": "okta_oidc_fun@okta.com",
"preferred_username": "okta_oidc_fun@okta.com",
"given_name": "Okta OIDC",
"family_name": "Fun",
"zoneinfo": "America/Los_Angeles",
"updated_at": 1499922371,
"email_verified": true
}

这将完善范围内所请求的所有身份信息。

自定义范围和声明

OIDC 规范可容纳自定义范围和声明。在令牌(可通过加密验证)中包含自定义声明的能力是身份提供者的一项重要功能。Okta 的实现为此提供了支持。

下面的屏幕截图显示了我的授权服务器的声明选项卡:

点击“添加声明”按钮将弹出一个对话框:

在上面的屏幕截图中,自定义声明是使用 Okta 的表达式语言定义的。表达式语言是 Okta 独有的,它是一种灵活的方式来描述构建属性以包含(或不包含)在自定义声明中的规则。

response_type=id_token使用带有和的隐式流scope=openid+profile,我们现在返回一个id_token包含以下编码声明的:

{
"sub": "00u2yulup4eWbOttd1t7",
"ver": 1,
"iss": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud": "0oa2yrbf35Vcbom491t7",
"iat": 1501533536,
"exp": 1501537136,
"jti": "ID.TsKlBQfGmiJcl2X3EuhzyyLfmzqi0OCd66rJ3Onk7FI",
"amr": [
"pwd"
],
"idp": "00o1zyyqo9bpRehCw1t7",
"nonce": "c96fa468-ca1b-46f0-8974-546f23f9ee6f",
"auth_time": 1501528157,
"at_hash": "hEjyn3mbKjuWanuSAF-z4Q",
"full_name": "Okta OIDC Fun"
}

请注意full_name中的声明id_token

验证令牌

可以通过访问端点来验证访问令牌/introspect。对于active令牌,您将获得如下响应:

http --auth <OIDC Client ID>:<OIDC Client Secret> -f POST \
https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/introspect \
token=eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNelJhOEV1elY5emgyREl6X3RVRUkifQ...
HTTP/1.1 200 OK
...

{
"active": true,
"aud": "https://afitnerd.com/test",
"client_id": "xdgqP32nYN148gn3gJsW",
"exp": 1498517509,
"fullName": "Micah Silverman",
"iat": 1498513909,
"iss": "https://micah.oktapreview.com/oauth2/aus9vmork8ww5twZg0h7",
"jti": "AT.JdXQPAuh-JTqhspCL8nLe2WgbfjcK_-jmlp7zwaYttE",
"scope": "openid profile",
"sub": "micah+okta@afitnerd.com",
"token_type": "Bearer",
"uid": "00u9vme99nxudvxZA0h7",
"username": "micah+okta@afitnerd.com"
}

由于它需要 OIDC 客户端 ID 和密钥,因此此操作通常在可以安全获取这些凭据的应用服务器中完成。您不会希望最终用户 Web 或移动应用等能够访问 OIDC 客户端密钥。

如果token参数无效或者已过期,/introspect端点将返回以下内容:

http --auth <OIDC Client ID>:<OIDC Client Secret> -f POST \
https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/introspect \
token=bogus
HTTP/1.1 200 OK
...
{
"active": false
}

可以使用JWK端点验证 ID 令牌。JWK 是一种表示加密密钥的 JSON 数据结构。JWK 端点从用于 API 发现的 OIDC“众所周知”端点公开。这会返回大量信息。以下是摘录:

http https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/.well-known/openid-configuration
HTTP/1.1 200 OK
...
{
"authorization_endpoint": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/authorize",
...
"introspection_endpoint": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/introspect",
...
"issuer": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"jwks_uri": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/keys",
...
"userinfo_endpoint": "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/userinfo"
}

其中一些端点(例如/userinfo/authorize)现在看起来应该很熟悉。我们感兴趣的是/keys中显示的端点jwks_uri

http https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7/v1/keys
HTTP/1.1 200 OK
...
{
"keys": [
{
"alg": "RS256",
"e": "AQAB",
"kid": "cbkhWG0YmFsGiNO1LEkWSEszDCTNfwvJPpXxuVf_kX0",
"kty": "RSA",
"n": "g2XQgdyc5P6F4K26ioKiUzrdgfy90eBgIbcrKkspKZmzRJ3CIssv69f1ClJvT784J-...",
"use": "sig"
}
]
}

注意kid声明。它kid与我们标题中的声明相匹配id_token

{
"typ": "JWT",
"alg": "RS256",
"kid": "cbkhWG0YmFsGiNO1LEkWSEszDCTNfwvJPpXxuVf_kX0"
}

我们还可以看到使用的算法是RS256。使用声明中找到的公钥n以及安全库,我们可以确认 ID 令牌未被篡改。所有这些都可以在最终用户 SPA、移动应用程序等上安全地完成。

下面是一个 Java 示例,它使用上述声明jwks_uri来验证id_token: https: //github.com/dogeared/JWKTokenVerifier

java -jar target/jwk-token-verifier-0.0.1-SNAPSHOT-spring-boot.jar \
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik93bFNJS3p3Mmt1Wk8zSmpnMW5Dc2RNel... \
g2XQgdyc5P6F4K26ioKiUzrdgfy90eBgIbcrKkspKZmzRJ3CIssv69f1ClJvT784J-... \
AQAB

Verified Access Token
{
"header" : {
"alg" : "RS256",
"kid" : "cbkhWG0YmFsGiNO1LEkWSEszDCTNfwvJPpXxuVf_kX0"
},
"body" : {
"ver" : 1,
"jti" : "AT.LT9cRL_Kzd3T8Izw_ONZxHJ5xGBPD0m13iiEIDK_Nbw",
"iss" : "https://micah.okta.com/oauth2/aus2yrcz7aMrmDAKZ1t7",
"aud" : "test",
"iat" : 1501533536,
"exp" : 1501537136,
"cid" : "0oa2yrbf35Vcbom491t7",
"uid" : "00u2yulup4eWbOttd1t7",
"scp" : [ "openid" ],
"sub" : "okta_oidc_fun@okta.com"
},
"signature" : "ZV_9tYxt4v4bp9WEEDu038b7v_OHsbMZw13daR1s5_tI56oayBgJlnqf-..."
}

如果 JWT 的任何部分id_token被篡改,你会看到以下信息:

io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

使用终端和 JWK 验证 JWT/introspect是 OIDC 的一个强大组件。它能够高度确保令牌未以任何方式被篡改。因此,令牌中包含的信息(例如有效期)可以安全地执行。

#你可能也喜欢这些API文章!