IdentityServer4源碼解析_5_查詢用戶信息接口


目錄

協議簡析

UserInfo接口是OAuth2.0中規定的需要認證訪問的接口,可以返回認證用戶的聲明信息。請求UserInfo接口需要使用通行令牌。響應報文通常是json數據格式,包含了一組claim鍵值對集合。與UserInfo接口通訊必須使用https。

根據RFC2616協議,UserInfo必須支持GET和POST方法。

UserInfo接口必須接受Bearer令牌。

UserInfo接口應該支持javascript客戶端跨域訪問,可以使用CORS協議或者其他方案。

UserInfo請求

推薦使用GET方法,使用Authorization頭承載Bearer令牌來請求UserInfo接口。

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/1.1 200 OK
Content-Type: application/json

{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "janedoe@example.com",
"picture": "http://example.com/janedoe/me.jpg"
}

失敗響應

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的話則返回校驗失敗結果
public async Task<BearerTokenUsageValidationResult> 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聲明,subsubject的縮寫,代表用戶唯一標識
  3. 收集accessToken所有claim,移除以下與用戶信息無關的claim
    at_hash,aud,azp,c_hash,client_id,exp,iat,iss,jti,nonce,nbf,reference_token_id,sid,scope
    用篩選后的claim創建名稱為UserInfoPrincipal
  4. 調用IProfileServiceIsAcriveAsync方法判斷用戶是否啟用,不是啟動狀態的話返回invalid_token錯誤
  5. 返回校驗成功結果對象,包括步驟3構建的Principal

public async Task<UserInfoRequestValidationResult> 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接口的默認實現UserInfoResponseGeneratorProcessAsync方法生成響應報文。

  1. 從校驗結果中獲取scope聲明值,查詢scope值關聯的IdentityResource(身份資源)及其關聯的所有claim。得到的結果就是用戶請求的所有claim
  2. 調用DefaultProfileServiceGetProfileDataAsync方法,返回校驗結果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格式寫入響應內容
 public virtual async Task<Dictionary<string, object>> 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<Claim>();

    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();
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM