1. 程式人生 > >IdentityServer4原始碼解析_5_查詢使用者資訊介面

IdentityServer4原始碼解析_5_查詢使用者資訊介面

# 目錄 - [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()