JWT 的好处是能够在其中携带信息。有了可用于您的应用程序的此信息,您可以轻松强制执行令牌过期并减少 API 调用次数。此外,由于它们经过加密签名,您可以验证它们是否未被篡改。
根据 OIDC 规范的规定,与身份相关的信息有两个主要来源。id_token
一个来源是编码到JWT中的信息。另一个是来自端点的响应/userinfo
,可以使用access_token
作为不记名令牌访问。
请求中有很多查询参数的组合/authorization
,它们决定了哪些信息将被编码到id_token
. 影响最终将在返回的令牌和/userinfo
端点中找到的内容的两个查询参数是response_type
和scope
。
目前,我们将搁置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 规范中不需要访问令牌的特定格式,我们使用 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
}
我们取回sub
,email
和email_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
}
请注意,我们将sub
和email
声明直接编码在 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
将所有可用范围与所有可能的响应类型相结合,会产生大量要呈现的信息:准确地说是 48 种组合。首先,我将列举每个范围产生的结果,然后我们将看一些结合 和 的真实世界request_type
示例scope
。
id_token
首先要注意的是,不同的范围会对端点中编码的信息和从端点返回的信息产生影响/userinfo
。
scope | resultant claims | |
---|---|---|
openid | (required for all OIDC flows) | |
profile | name, family_name, given_name, middle_name, nickname, preferred_username | |
profile (cont’d) | profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at | |
email, email_verified | ||
address | address | |
phone | 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 的实现为此提供了支持。
下面的屏幕截图显示了我的授权服务器的声明选项卡:
单击“添加声明”按钮会弹出一个对话框:
response_type=id_token
使用带有and的隐式流scope=openid+profile
,我们现在返回一个id_token
其中编码了这些声明的 an :
{
"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 的一个强大组件。它允许高度信任令牌没有以任何方式被篡改。并且,正因为如此,可以安全地强制执行其中包含的信息(例如到期)。