IdentityServer4原始碼解析_5_查詢使用者資訊介面
阿新 • • 發佈:2020-03-29
# 目錄
- [IdentityServer4原始碼解析_1_專案結構](https://www.cnblogs.com/holdengong/p/12578558.html)
- [IdentityServer4原始碼解析_2_元資料介面](https://www.cnblogs.com/holdengong/p/12580738.html)
- [IdentityServer4原始碼解析_3_認證介面](https://www.cnblogs.com/holdengong/p/12585466.html)
- [IdentityServer4原始碼解析_4_令牌發放介面](https://www.cnblogs.com/holdengong/p/12589436.html)
- [IdentityServer4原始碼解析_5_查詢使用者資訊介面](https://www.cnblogs.com/holdengong/p/12594007.html)
- [IdentityServer4原始碼解析_6_結束會話介面]
- [IdentityServer4原始碼解析_7_查詢令牌資訊介面]
- [IdentityServer4原始碼解析_8_撤銷令牌介面]
# 協議簡析
UserInfo介面是OAuth2.0中規定的需要認證訪問的介面,可以返回認證使用者的宣告資訊。請求UserInfo介面需要使用通行令牌。響應報文通常是json資料格式,包含了一組claim鍵值對集合。與UserInfo介面通訊必須使用https。
根據RFC2616協議,UserInfo必須支援GET和POST方法。
UserInfo介面必須接受Bearer令牌。
UserInfo介面應該支援javascript客戶端跨域訪問,可以使用CORS協議或者其他方案。
## UserInfo請求
推薦使用GET方法,使用Authorization頭承載Bearer令牌來請求UserInfo介面。
```http
GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG
```
## 成功響應
如果某個claim為空或者null,不返回該鍵。
必須返回sub(subject)宣告。
必須校驗UserInfo返回的sub與id_token中的sub是否一致
content-type必須是application/json,必須使用utf-8編碼
如果加密位jwt返回,content-type必須位application/jwt
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "[email protected]",
"picture": "http://example.com/janedoe/me.jpg"
}
```
## 失敗響應
```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: error="invalid_token",
error_description="The Access Token expired"
```
## 響應校驗
客戶端必須校驗如下內容
- 校驗認證服務身份(https)
- 如果客戶端註冊時設定了userinfo_encrypted_response_alg ,收到響應時用對應演算法解密
- 如果響應有簽名,客戶端需要驗籤
# 原始碼解析
## 校驗通行令牌
- 首先會嘗試從`Authorizaton`頭中獲取`Bearer Token`的值,找到的話則返回
- 如果content-type為表單型別,嘗試從表單中獲取`access_token`引數值
- 兩處都沒有獲取到`Beaer Token`的話則返回校驗失敗結果
```csharp
public async Task ValidateAsync(HttpContext context)
{
var result = ValidateAuthorizationHeader(context);
if (result.TokenFound)
{
_logger.LogDebug("Bearer token found in header");
return result;
}
if (context.Request.HasFormContentType)
{
result = await ValidatePostBodyAsync(context);
if (result.TokenFound)
{
_logger.LogDebug("Bearer token found in body");
return result;
}
}
_logger.LogDebug("Bearer token not found");
return new BearerTokenUsageValidationResult();
}
```
## 校驗請求引數
由`IUserInfoRequestValidator`的預設實現`UserInfoRequestValidator`對入參進行校驗。
1. `accessToken`,必須包括`openid`宣告的許可權
2. 必須有`sub`宣告,`sub`是`subject`的縮寫,代表使用者唯一標識
3. 收集`accessToken`所有`claim`,移除以下與使用者資訊無關的`claim`。
at_hash,aud,azp,c_hash,client_id,exp,iat,iss,jti,nonce,nbf,reference_token_id,sid,scope
用篩選後的`claim`建立名稱為`UserInfo`的`Principal`
4. 呼叫`IProfileService`的`IsAcriveAsync`方法判斷使用者是否啟用,不是啟動狀態的話返回`invalid_token`錯誤
5. 返回校驗成功結果物件,包括步驟3構建的`Principal`
```csharp
public async Task ValidateRequestAsync(string accessToken)
{
// the access token needs to be valid and have at least the openid scope
var tokenResult = await _tokenValidator.ValidateAccessTokenAsync(
accessToken,
IdentityServerConstants.StandardScopes.OpenId);
if (tokenResult.IsError)
{
return new UserInfoRequestValidationResult
{
IsError = true,
Error = tokenResult.Error
};
}
// the token must have a one sub claim
var subClaim = tokenResult.Claims.SingleOrDefault(c => c.Type == JwtClaimTypes.Subject);
if (subClaim == null)
{
_logger.LogError("Token contains no sub claim");
return new UserInfoRequestValidationResult
{
IsError = true,
Error = OidcConstants.ProtectedResourceErrors.InvalidToken
};
}
// create subject from incoming access token
var claims = tokenResult.Claims.Where(x => !Constants.Filters.ProtocolClaimsFilter.Contains(x.Type));
var subject = Principal.Create("UserInfo", claims.ToArray());
// make sure user is still active
var isActiveContext = new IsActiveContext(subject, tokenResult.Client, IdentityServerConstants.ProfileIsActiveCallers.UserInfoRequestValidation);
await _profile.IsActiveAsync(isActiveContext);
if (isActiveContext.IsActive == false)
{
_logger.LogError("User is not active: {sub}", subject.GetSubjectId());
return new UserInfoRequestValidationResult
{
IsError = true,
Error = OidcConstants.ProtectedResourceErrors.InvalidToken
};
}
return new UserInfoRequestValidationResult
{
IsError = false,
TokenValidationResult = tokenResult,
Subject = subject
};
}
```
## 生成響應報文
呼叫`IUserInfoResponseGenerator`介面的預設實現`UserInfoResponseGenerator`的`ProcessAsync`方法生成響應報文。
1. 從校驗結果中獲取`scope`宣告值,查詢`scope`值關聯的`IdentityResource`(身份資源)及其關聯的所有`claim`。得到的結果就是使用者請求的所有`claim`
2. 呼叫`DefaultProfileService`的`GetProfileDataAsync`方法,返回校驗結果`claim`與使用者請求`claim`的交集。
3. 如果`claim`集合中沒有`sub`,取校驗結果中的`sub`值。如果`IProfileService`返回的`sub`宣告值與校驗結果的`sub`值不一致丟擲異常。
4. 返回`claim`集合。
5. 響應頭寫入`Cache-Control:no-store, no-cache, max-age=0`,`Pragma:no-cache`
6. `claim`集合用json格式寫入響應內容
```csharp
public virtual async Task> ProcessAsync(UserInfoRequestValidationResult validationResult)
{
Logger.LogDebug("Creating userinfo response");
// extract scopes and turn into requested claim types
var scopes = validationResult.TokenValidationResult.Claims.Where(c => c.Type == JwtClaimTypes.Scope).Select(c => c.Value);
var requestedClaimTypes = await GetRequestedClaimTypesAsync(scopes);
Logger.LogDebug("Requested claim types: {claimTypes}", requestedClaimTypes.ToSpaceSeparatedString());
// call profile service
var context = new ProfileDataRequestContext(
validationResult.Subject,
validationResult.TokenValidationResult.Client,
IdentityServerConstants.ProfileDataCallers.UserInfoEndpoint,
requestedClaimTypes);
context.RequestedResources = await GetRequestedResourcesAsync(scopes);
await Profile.GetProfileDataAsync(context);
var profileClaims = context.IssuedClaims;
// construct outgoing claims
var outgoingClaims = new List();
if (profileClaims == null)
{
Logger.LogInformation("Profile service returned no claims (null)");
}
else
{
outgoingClaims.AddRange(profileClaims);
Logger.LogInformation("Profile service returned the following claim types: {types}", profileClaims.Select(c => c.Type).ToSpaceSeparatedString());
}
var subClaim = outgoingClaims.SingleOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (subClaim == null)
{
outgoingClaims.Add(new Claim(JwtClaimTypes.Subject, validationResult.Subject.GetSubjectId()));
}
else if (subClaim.Value != validationResult.Subject.GetSubjectId())
{
Logger.LogError("Profile service returned incorrect subject value: {sub}", subClaim);
throw new InvalidOperationException("Profile service returned incorrect subject value");
}
return outgoingClaims.ToClaimsDictionary()