IdentityServer4源碼解析_4_令牌發放介面

来源:https://www.cnblogs.com/holdengong/archive/2020/03/28/12589436.html
-Advertisement-
Play Games

目錄 "identityserver4源碼解析_1_項目結構" "identityserver4源碼解析_2_元數據介面" "identityserver4源碼解析_3_認證介面" "identityserver4源碼解析_4_令牌發放介面" "identityserver4源碼解析_5_查詢用戶信 ...


目錄

協議

Token介面

oidc服務需要提供token介面,提供AccessToken,IdToken,以及RefreshToken(可選)。在授權碼模式下,token介面必須使用https。

請求

必須使用POST方法,使用x-www-form-urlencoded序列化參數,clientId:clientSecret使用Basic加密放在Authorization頭中

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

請求校驗

認證服務必須校驗下列內容:

  • 驗證client是否頒發了秘鑰
  • 驗證為該客戶端頒發了授權碼
  • 驗證授權碼有效性
  • 如果可能的話,驗證授權碼是否被使用過
  • 驗證redirect_uri 與發起認證請求時的值一致

成功響應

在收到token請求,並校驗通過之後,認證服務返回成功報文,報文包含了身份令牌和通行令牌。數據格式使用application/json。token_type必須返回Bearer,其他類型token不在本協議範圍內。在OAuth2.0響應報文基礎上,oidc增加了id_tken。所有token包含了token或者其他敏感信息的響應報文,必須包含以下響應頭。

Cache-Control no-store
Pragma no-cache

失敗響應

如果認證失敗返回application/json格式錯誤消息,狀態碼400

  HTTP/1.1 400 Bad Request
  Content-Type: application/json
  Cache-Control: no-store
  Pragma: no-cache

  {
   "error": "invalid_request"
  }

id token校驗

客戶端必須校驗返回的id token, 校驗條件如下。對照這些條件,就可以更懂Microsoft.Authentication.OpenIdConnect裡面的代碼了,要做的事情很多。

  1. 如果id token被加密,使用客戶端註冊時候約定的秘鑰和演算法解密。如果約定了加密方式,id token未被加密,客戶端應該拒絕。
  2. 簽發方標識必須與iss聲明一致
  3. 客戶端必須校驗aud聲明包含了它的客戶端id,如果id token未返回正確的audience或者反悔了不被新人的audience,應該拒絕
  4. 如果id token包含多個audience,需要校驗是否有azp聲明。azp即Authorized party,標識被授權的client。
  5. 如果包含azp聲明,客戶端需要校驗其值是否為自己的客戶端id
  6. 如果id token由token介面直接頒發給客戶端(授權碼模式就是如此),客戶端必鬚根據alg參數值的演算法驗證簽名。客戶端必須使用簽發方提供的秘鑰。
  7. alg值預設為RS256,客戶端可以在註冊的時候使用id_token_signed_response_alg參數指定配置。
  8. 如果jwt的alg頭使用了基於mac地址的加密演算法,如HS256, HS384,HS512,aud聲明中的位元組會用作驗簽。(意思是會把mac地址相關信息寫在aud聲明上?)
  9. The current time MUST be before the time represented by the exp Claim.
    當前時間必須早於exp(token過期時間)。
  10. iat(簽發時間)可以用於拒絕過早、或者過於頻繁簽發的token,可以用於預防重放攻擊。可接受時間範圍由客戶端自行決定。
  11. 如果認證請求包含了nonce參數,客戶端必須交驗認證響應中返回的nonce值是否一致。防止重放攻擊。
  12. 如果客戶端請求了acr聲明(Authentication Context Class Reference,認證會話上下文,用於表示當前認證會話),必須交驗acr值是否合法。
  13. 如果客戶端請求了auth_time聲明,客戶端應該校驗認證時間是否已經超出,是否需要重新認證。

access token校驗

如果id_token中包含了at_hash聲明,需要做下麵的校驗。at_hash標明瞭access_token和id_token之間的會話關聯關係,做這個校驗可以防跨站偽造。

  1. 用idtoken的alg頭標明的演算法加密access_token,比如alg位RS256,則是用HSA-256演算法加密。
  2. 取hash值左邊一般使用base64url加密
  3. id token中的at_hash值必須跟上個步驟得到的值一致

校驗規則很多,瞭解一下即可,絕大部分屬於客戶端需要做的部分,絕大部分跟安全有關。這一塊的實現可以參考Microsoft.Authentication.OpenIdConnect,這是客戶端的實現。我們現在看的IdentityServer是認證服務端的實現。

源碼

五種授權模式

有下麵幾種授權模式可以請求token介面

  • 授權碼模式:最常用的code換token
  • 混合模式:混合模式是授權碼模式+簡化模式混合使用的方式,在用授權碼code找token介面換通行/身份令牌的邏輯與授權碼模式的邏輯是一樣的。idsv4中,混合模式沒有自己的單獨實現,只是把授權碼+簡化模式的代碼同時調用。
  • 客戶端密鑰模式:一般用於完全信任的內部系統,密鑰換取access_token,由於沒有用戶參與,scope包含open_id是非法的
  • 用戶名密碼模式:一般用於第三方對接、無界面交互場景。即username+password換token/id_token,password不一定是密碼,也可以是驗證碼或其他的什麼東西,這個完全取決於開發自己的實現
  • 設備流模式(略)

註意:簡化模式所有的token都是由認證介面(authorize)一次性返回的,不能使用token介面。

校驗請求方法

token介面僅允許POST方法,Content-Type必須為application/x-www-form-urlencoded,否則拋出InvalidRequest錯誤。

public async Task<IEndpointResult> ProcessAsync(HttpContext context)
    {
        _logger.LogTrace("Processing token request.");

        // validate HTTP
        if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType)
        {
            _logger.LogWarning("Invalid HTTP request for token endpoint");
            return Error(OidcConstants.TokenErrors.InvalidRequest);
        }

        return await ProcessTokenRequestAsync(context);
    }

處理流程

  • 校驗客戶端
  • 校驗請求參數
  • 創建返回值
  • 返回結果
private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context)
{
    _logger.LogDebug("Start token request.");

    // validate client
    var clientResult = await _clientValidator.ValidateAsync(context);

    if (clientResult.Client == null)
    {
        return Error(OidcConstants.TokenErrors.InvalidClient);
    }

    // validate request
    var form = (await context.Request.ReadFormAsync()).AsNameValueCollection();
    _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName);
    var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult);

    if (requestResult.IsError)
    {
        await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult));
        return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse);
    }

    // create response
    _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName);
    var response = await _responseGenerator.ProcessAsync(requestResult);

    await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
    LogTokens(response, requestResult);

    // return result
    _logger.LogDebug("Token request success.");
    return new TokenResult(response);
}

校驗客戶端

  • 解碼客戶端秘鑰,對應的處理類是BasicAuthenticationSecretParser,客戶端id和秘鑰用base64url加密方法放在Authorzaition頭上。base64url基本是明文的,因為授權碼換token是後端進行的,所以安全性沒有問題
  • 解碼得到客戶端id和秘鑰之後,跟store對比校驗客戶端是否存在,秘鑰是否一致。
public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context)
{
    _logger.LogDebug("Start client validation");

    var fail = new ClientSecretValidationResult
    {
        IsError = true
    };

    var parsedSecret = await _parser.ParseAsync(context);
    if (parsedSecret == null)
    {
        await RaiseFailureEventAsync("unknown", "No client id found");

        _logger.LogError("No client identifier found");
        return fail;
    }

    // load client
    var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id);
    if (client == null)
    {
        await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client");

        _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id);
        return fail;
    }

    SecretValidationResult secretValidationResult = null;
    if (!client.RequireClientSecret || client.IsImplicitOnly())
    {
        _logger.LogDebug("Public Client - skipping secret validation success");
    }
    else
    {
        secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);
        if (secretValidationResult.Success == false)
        {
            await RaiseFailureEventAsync(client.ClientId, "Invalid client secret");
            _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId);

            return fail;
        }
    }

    _logger.LogDebug("Client validation success");

    var success = new ClientSecretValidationResult
    {
        IsError = false,
        Client = client,
        Secret = parsedSecret,
        Confirmation = secretValidationResult?.Confirmation
    };

    await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type);
    return success;
}

校驗請求參數

  • 客戶端的PortocalType必須位oidc,否則報錯InvalidClient
  • 校驗GrantType,必填,長度不能超過100。
  • GrantType預設支持以下幾種類型,還可以自定義GrantType
    • authorization_code:授權碼換token
    • client_credentials:客戶端秘鑰換token
    • password:用戶名密碼換token
    • refresn_token:刷新令牌換token
    • urn:ietf:params:oauth:grant-type:device_code:deviceflow,略
public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult)
{
    _logger.LogDebug("Start token request validation");

    _validatedRequest = new ValidatedTokenRequest
    {
        Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)),
        Options = _options
    };

    if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult));

    _validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation);

    /////////////////////////////////////////////
    // check client protocol type
    /////////////////////////////////////////////
    if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect)
    {
        LogError("Invalid protocol type for client",
            new
            {
                clientId = _validatedRequest.Client.ClientId,
                expectedProtocolType = IdentityServerConstants.ProtocolTypes.OpenIdConnect,
                actualProtocolType = _validatedRequest.Client.ProtocolType
            });

        return Invalid(OidcConstants.TokenErrors.InvalidClient);
    }

    /////////////////////////////////////////////
    // check grant type
    /////////////////////////////////////////////
    var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType);
    if (grantType.IsMissing())
    {
        LogError("Grant type is missing");
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    if (grantType.Length > _options.InputLengthRestrictions.GrantType)
    {
        LogError("Grant type is too long");
        return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
    }

    _validatedRequest.GrantType = grantType;

    switch (grantType)
    {
        case OidcConstants.GrantTypes.AuthorizationCode:
            return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
        case OidcConstants.GrantTypes.ClientCredentials:
            return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
        case OidcConstants.GrantTypes.Password:
            return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
        case OidcConstants.GrantTypes.RefreshToken:
            return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
        case OidcConstants.GrantTypes.DeviceCode:
            return await RunValidationAsync(ValidateDeviceCodeRequestAsync, parameters);
        default:
            return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
    }
}

參數校驗 - 授權碼模式

  • 客戶端AllowedGrantTypes必須包含authorization_code或者hybrid,否則報錯UnauthorizedClient。
  • code必填,code長度不能超過100
  • 客戶端傳過來的code只是授權碼的id,從store中取出來授權碼對象,如果不存在返回錯誤InvalidGrant
  • 從store中移除授權碼,此處實現了code只是用一次
  • 如果授權碼超出有效時長,返回錯誤invalidGrant
  • 校驗授權碼對象的客戶端id與當前客戶端是否一致
  • redirect_uri必填,且必須與授權碼對象保存的redirect_uri一致,否則返回錯誤UnauthorizedClient
  • 如果請求中沒有任何scope,返回錯誤invalidRequest
  • 判斷用戶是否啟用,這個判斷是由可插拔服務IProfileService的IsActive方法來實現的,開發可以註入自己的實現。如果用戶禁用將返回錯誤InvalidGrant。
private async Task<TokenRequestValidationResult> ValidateAuthorizationCodeRequestAsync(NameValueCollection parameters)
    {
        _logger.LogDebug("Start validation of authorization code token request");

        /////////////////////////////////////////////
        // check if client is authorized for grant type
        /////////////////////////////////////////////
        if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.AuthorizationCode) &&
            !_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.Hybrid))
        {
            LogError("Client not authorized for code flow");
            return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
        }

        /////////////////////////////////////////////
        // validate authorization code
        /////////////////////////////////////////////
        var code = parameters.Get(OidcConstants.TokenRequest.Code);
        if (code.IsMissing())
        {
            LogError("Authorization code is missing");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        if (code.Length > _options.InputLengthRestrictions.AuthorizationCode)
        {
            LogError("Authorization code is too long");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        _validatedRequest.AuthorizationCodeHandle = code;

        var authZcode = await _authorizationCodeStore.GetAuthorizationCodeAsync(code);
        if (authZcode == null)
        {
            LogError("Invalid authorization code", new { code });
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        await _authorizationCodeStore.RemoveAuthorizationCodeAsync(code);

        if (authZcode.CreationTime.HasExceeded(authZcode.Lifetime, _clock.UtcNow.UtcDateTime))
        {
            LogError("Authorization code expired", new { code });
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        /////////////////////////////////////////////
        // populate session id
        /////////////////////////////////////////////
        if (authZcode.SessionId.IsPresent())
        {
            _validatedRequest.SessionId = authZcode.SessionId;
        }

        /////////////////////////////////////////////
        // validate client binding
        /////////////////////////////////////////////
        if (authZcode.ClientId != _validatedRequest.Client.ClientId)
        {
            LogError("Client is trying to use a code from a different client", new { clientId = _validatedRequest.Client.ClientId, codeClient = authZcode.ClientId });
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        /////////////////////////////////////////////
        // validate code expiration
        /////////////////////////////////////////////
        if (authZcode.CreationTime.HasExceeded(_validatedRequest.Client.AuthorizationCodeLifetime, _clock.UtcNow.UtcDateTime))
        {
            LogError("Authorization code is expired");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        _validatedRequest.AuthorizationCode = authZcode;
        _validatedRequest.Subject = authZcode.Subject;

        /////////////////////////////////////////////
        // validate redirect_uri
        /////////////////////////////////////////////
        var redirectUri = parameters.Get(OidcConstants.TokenRequest.RedirectUri);
        if (redirectUri.IsMissing())
        {
            LogError("Redirect URI is missing");
            return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
        }

        if (redirectUri.Equals(_validatedRequest.AuthorizationCode.RedirectUri, StringComparison.Ordinal) == false)
        {
            LogError("Invalid redirect_uri", new { redirectUri, expectedRedirectUri = _validatedRequest.AuthorizationCode.RedirectUri });
            return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
        }

        /////////////////////////////////////////////
        // validate scopes are present
        /////////////////////////////////////////////
        if (_validatedRequest.AuthorizationCode.RequestedScopes == null ||
            !_validatedRequest.AuthorizationCode.RequestedScopes.Any())
        {
            LogError("Authorization code has no associated scopes");
            return Invalid(OidcConstants.TokenErrors.InvalidRequest);
        }

        /////////////////////////////////////////////
        // validate PKCE parameters
        /////////////////////////////////////////////
        var codeVerifier = parameters.Get(OidcConstants.TokenRequest.CodeVerifier);
        if (_validatedRequest.Client.RequirePkce || _validatedRequest.AuthorizationCode.CodeChallenge.IsPresent())
        {
            _logger.LogDebug("Client required a proof key for code exchange. Starting PKCE validation");

            var proofKeyResult = ValidateAuthorizationCodeWithProofKeyParameters(codeVerifier, _validatedRequest.AuthorizationCode);
            if (proofKeyResult.IsError)
            {
                return proofKeyResult;
            }

            _validatedRequest.CodeVerifier = codeVerifier;
        }
        else
        {
            if (codeVerifier.IsPresent())
            {
                LogError("Unexpected code_verifier: {codeVerifier}. This happens when the client is trying to use PKCE, but it is not enabled. Set RequirePkce to true.", codeVerifier);
                return Invalid(OidcConstants.TokenErrors.InvalidGrant);
            }
        }

        /////////////////////////////////////////////
        // make sure user is enabled
        /////////////////////////////////////////////
        var isActiveCtx = new IsActiveContext(_validatedRequest.AuthorizationCode.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.AuthorizationCodeValidation);
        await _profile.IsActiveAsync(isActiveCtx);

        if (isActiveCtx.IsActive == false)
        {
            LogError("User has been disabled", new { subjectId = _validatedRequest.AuthorizationCode.Subject.GetSubjectId() });
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }

        _logger.LogDebug("Validation of authorization code token request success");

        return Valid();
    }

參數校驗 - 客戶端秘鑰模式

  • 校驗Client是否允許使用客戶端秘鑰認證模式
  • 校驗客戶端是否允許訪問請求的授權範圍scope
  • scope中包含openid的話返回錯誤invlidScope,因為本模式沒有涉及用戶信息
  • cope中包含offline_access則返回錯誤InvalidScope,本模式不允許使用refresh_token
private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters)
    {
        _logger.LogDebug("Start client credentials token request validation");

        /////////////////////////////////////////////
        // check if client is authorized for grant type
        /////////////////////////////////////////////
        if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials))
        {
            LogError("Client not authorized for client credentials flow, check the AllowedGrantTypes setting", new { clientId = _validatedRequest.Client.ClientId });
            return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
        }

        /////////////////////////////////////////////
        // check if client is allowed to request scopes
        /////////////////////////////////////////////
        if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true))
        {
            return Invalid(OidcConstants.TokenErrors.InvalidScope);
        }

        if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes)
        {
            LogError("Client cannot request OpenID scopes in client credentials flow", new { clientId = _validatedRequest.Client.ClientId });
            return Invalid(OidcConstants.TokenErrors.InvalidScope);
        }

        if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope)
        {
            LogError("Client cannot request a refresh token in client credentials flow", new { clientId = _validatedRequest.Client.ClientId });
            return Invalid(OidcConstants.TokenErrors.InvalidScope);
        }

        _logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId);
        return Valid();
    }

參數校驗 - 用戶名密碼模式

  • 校驗客戶端是否允許使用用戶名密碼模式,校驗失敗返回UnauthoriedClient錯誤
  • 校驗客戶端是否有權訪問所請求的所有scope,校驗失敗返回InvalidScope錯誤
  • 從請求中獲取username和password參數,未提供username返回InvalidGrant錯誤,未提供password則設置password位空值
  • username和password長度不能超過100,否則返回InvalidGrant錯誤
  • 使用IResourceOwnerPasswordValidator校驗username和password,此介面需要開發實現後註入,否則會拋出異常
  • GrantValidationResult的Subject不能為null,因此開發的IResourceOwnerPasswordValidator實現,校驗成功後必須給GrantValidationResult賦值
  • 校驗用戶是否禁用,根據可插拔服務IProfileService的IsAcrtive方法判斷,如果禁用返回IvalidGrant錯誤。
private async Task<TokenRequestValidationResult> ValidateResourceOwnerCredentialRequestAsync(NameValueCollection parameters)
{
    _logger.LogDebug("Start resource owner password token request validation");

    /////////////////////////////////////////////
    // check if client is authorized for grant type
    /////////////////////////////////////////////
    if (!_validatedRequest.Client.AllowedGrantTypes.Contains(GrantType.ResourceOwnerPassword))
    {
        LogError("Client not authorized for resource owner flow, check the AllowedGrantTypes setting", new { client_id = _validatedRequest.Client.ClientId });
        return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
    }

    /////////////////////////////////////////////
    // check if client is allowed to request scopes
    /////////////////////////////////////////////
    if (!(await ValidateRequestedScopesAsync(parameters)))
    {
        return Invalid(OidcConstants.TokenErrors.InvalidScope);
    }

    /////////////////////////////////////////////
    // check resource owner credentials
    /////////////////////////////////////////////
    var userName = parameters.Get(OidcConstants.TokenRequest.UserName);
    var password = parameters.Get(OidcConstants.TokenRequest.Password);

    if (userName.IsMissing())
    {
        LogError("Username is missing");
        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    if (password.IsMissing())
    {
        password = "";
    }

    if (userName.Length > _options.InputLengthRestrictions.UserName ||
        password.Length > _options.InputLengthRestrictions.Password)
    {
        LogError("Username or password too long");
        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    _validatedRequest.UserName = userName;


    /////////////////////////////////////////////
    // authenticate user
    /////////////////////////////////////////////
    var resourceOwnerContext = new ResourceOwnerPasswordValidationContext
    {
        UserName = userName,
        Password = password,
        Request = _validatedRequest
    };
    await _resourceOwnerValidator.ValidateAsync(resourceOwnerContext);

    if (resourceOwnerContext.Result.IsError)
    {
        // protect against bad validator implementations
        resourceOwnerContext.Result.Error = resourceOwnerContext.Result.Error ?? OidcConstants.TokenErrors.InvalidGrant;

        if (resourceOwnerContext.Result.Error == OidcConstants.TokenErrors.UnsupportedGrantType)
        {
            LogError("Resource owner password credential grant type not supported");
            await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "password grant type not supported", resourceOwnerContext.Request.Client.ClientId);

            return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType, customResponse: resourceOwnerContext.Result.CustomResponse);
        }

        var errorDescription = "invalid_username_or_password";

        if (resourceOwnerContext.Result.ErrorDescription.IsPresent())
        {
            errorDescription = resourceOwnerContext.Result.ErrorDescription;
        }

        LogInformation("User authentication failed: ", errorDescription ?? resourceOwnerContext.Result.Error);
        await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, errorDescription, resourceOwnerContext.Request.Client.ClientId);

        return Invalid(resourceOwnerContext.Result.Error, errorDescription, resourceOwnerContext.Result.CustomResponse);
    }

    if (resourceOwnerContext.Result.Subject == null)
    {
        var error = "User authentication failed: no principal returned";
        LogError(error);
        await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, error, resourceOwnerContext.Request.Client.ClientId);

        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    /////////////////////////////////////////////
    // make sure user is enabled
    /////////////////////////////////////////////
    var isActiveCtx = new IsActiveContext(resourceOwnerContext.Result.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.ResourceOwnerValidation);
    await _profile.IsActiveAsync(isActiveCtx);

    if (isActiveCtx.IsActive == false)
    {
        LogError("User has been disabled", new { subjectId = resourceOwnerContext.Result.Subject.GetSubjectId() });
        await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "user is inactive", resourceOwnerContext.Request.Client.ClientId);

        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    _validatedRequest.UserName = userName;
    _validatedRequest.Subject = resourceOwnerContext.Result.Subject;

    await RaiseSuccessfulResourceOwnerAuthenticationEventAsync(userName, resourceOwnerContext.Result.Subject.GetSubjectId(), resourceOwnerContext.Request.Client.ClientId);
    _logger.LogDebug("Resource owner password token request validation success.");
    return Valid(resourceOwnerContext.Result.CustomResponse);
}

參數校驗 - RefreshToken

RefreshToken-刷新令牌。顧名思義,用於刷新通行令牌的憑證。擁有offline_access許可權的客戶端可以使用刷新令牌。只有授權碼、混合流程等由後端參與的授權模式才允許使用刷新令牌。

  • 從請求中獲取refresh_token參數值,如果為空則返回InvalidRequest錯誤
  • 如果刷新令牌長度超過100,返回InvalidGrant錯誤
  • 判斷刷新令牌是否存在且有效
    • 是否能從store中查詢到刷新令牌對象
    • 校驗刷新令牌是否過期
    • 校驗刷新令牌是否屬於當前客戶端
    • 校驗客戶端是否仍然有offline_access許可權
    • 校驗用戶是否被禁用
private async Task<TokenRequestValidationResult> ValidateRefreshTokenRequestAsync(NameValueCollection parameters)
{
    _logger.LogDebug("Start validation of refresh token request");

    var refreshTokenHandle = parameters.Get(OidcConstants.TokenRequest.RefreshToken);
    if (refreshTokenHandle.IsMissing())
    {
        LogError("Refresh token is missing");
        return Invalid(OidcConstants.TokenErrors.InvalidRequest);
    }

    if (refreshTokenHandle.Length > _options.InputLengthRestrictions.RefreshToken)
    {
        LogError("Refresh token too long");
        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    var result = await _tokenValidator.ValidateRefreshTokenAsync(refreshTokenHandle, _validatedRequest.Client);

    if (result.IsError)
    {
        LogWarning("Refresh token validation failed. aborting");
        return Invalid(OidcConstants.TokenErrors.InvalidGrant);
    }

    _validatedRequest.RefreshToken = result.RefreshToken;
    _validatedRequest.RefreshTokenHandle = refreshTokenHandle;
    _validatedRequest.Subject = result.RefreshToken.Subject;

    _logger.LogDebug("Validation of refresh token request success");
    return Valid();
}

生成響應報文 - 授權碼模式

  • 生成accessToken和refreshToken
    • 通行令牌由ITokenService介面,預設實現DefaultTokenService的CreateAccessTokenAsync方法生成
    • 如果請求了offline_access才生成refresh_token
    • 如果code是oidc授權碼,生成id_token。DefaultTokenService的CreateIdentityTokenAsync方法生成Token對象,CreateSecurityTokenAsync方法將Token對象加密為jwt。
protected virtual async Task<TokenResponse> ProcessAuthorizationCodeRequestAsync(TokenRequestValidationResult request)
    {
        Logger.LogTrace("Creating response for authorization code request");

        //////////////////////////
        // access token
        /////////////////////////
        (var accessToken, var refreshToken) = await CreateAccessTokenAsync(request.ValidatedRequest);
        var response = new TokenResponse
        {
            AccessToken = accessToken,
            AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime,
            Custom = request.CustomResponse,
            Scope = request.ValidatedRequest.AuthorizationCode.RequestedScopes.ToSpaceSeparatedString(),
        };

        //////////////////////////
        // refresh token
        /////////////////////////
        if (refreshToken.IsPresent())
        {
            response.RefreshToken = refreshToken;
        }

        //////////////////////////
        // id token
        /////////////////////////
        if (request.ValidatedRequest.AuthorizationCode.IsOpenId)
        {
            // load the client that belongs to the authorization code
            Client client = null;
            if (request.ValidatedRequest.AuthorizationCode.ClientId != null)
            {
                client = await Clients.FindEnabledClientByIdAsync(request.ValidatedRequest.AuthorizationCode.ClientId);
            }
            if (client == null)
            {
                throw new InvalidOperationException("Client does not exist anymore.");
            }

            var resources = await Resources.FindEnabledResourcesByScopeAsync(request.ValidatedRequest.AuthorizationCode.RequestedScopes);

            var tokenRequest = new TokenCreationRequest
            {
                Subject = request.ValidatedRequest.AuthorizationCode.Subject,
                Resources = resources,
                Nonce = request.ValidatedRequest.AuthorizationCode.Nonce,
                AccessTokenToHash = response.AccessToken,
                StateHash = request.ValidatedRequest.AuthorizationCode.StateHash,
                ValidatedRequest = request.ValidatedRequest
            };

            var idToken = await TokenService.CreateIdentityTokenAsync(tokenRequest);
            var jwt = await TokenService.CreateSecurityTokenAsync(idToken);
            response.IdentityToken = jwt;
        }

        return response;
    }

生成響應報文 - 客戶端密鑰模式

  • 僅生成accessToken
protected virtual Task<TokenResponse> ProcessClientCredentialsRequestAsync(TokenRequestValidationResult request)
{
    Logger.LogTrace("Creating response for client credentials request");

    return ProcessTokenRequestAsync(request);
}

protected virtual async Task<TokenResponse> ProcessTokenRequestAsync(TokenRequestValidationResult validationResult)
    {
        (var accessToken, var refreshToken) = await CreateAccessTokenAsync(validationResult.ValidatedRequest);
        var response = new TokenResponse
        {
            AccessToken = accessToken,
            AccessTokenLifetime = validationResult.ValidatedRequest.AccessTokenLifetime,
            Custom = validationResult.CustomResponse,
            Scope = validationResult.ValidatedRequest.Scopes.ToSpaceSeparatedString()
        };

        if (refreshToken.IsPresent())
        {
            response.RefreshToken = refreshToken;
        }

        return response;
    }

生成響應報文 - 用戶名密碼模式

  • 生成accessToken
  • 如果申請了offline_access且有許可權,同時返回refresh_token
  • 不會返回id_token,我理解的是授權碼等模式是有限授權,需要code換id_token,才能拿到用戶id以及其他的基本信息。而密碼模式是完全信任授權,賬號密碼都給你了,還整id_token幹嘛,你要啥信息,自己實現IResourceOwnerPasswordValidator,自己去庫里取就完事了,還要啥自行車。
protected virtual Task<TokenResponse> ProcessPasswordRequestAsync(TokenRequestValidationResult request)
    {
        Logger.LogTrace("Creating response for password request");

        return ProcessTokenRequestAsync(request);
    }

生成響應報文 - 刷新令牌

  • 從請求中取出舊通行令牌
  • 判斷客戶端配置UpdateAccessTokenClaimsOnRefresh-是否在刷新令牌的時候更新通行令牌的claims,預設false。如果為true,則創建新的token對象,否則使用舊的token,只是刷新token的創建時間和有效時間。
  • 判斷客戶端配置RefreshTokenUsage - 刷新令牌用法,0:ReUse可重覆使用 1:OnTimeOnly一次性,預設1。如果是一次性的話,從store中刪除舊的刷新令牌,創建新的刷新令牌。
  • 判斷客戶端配置RefreshTokenExpiration - 刷新令牌過期類型,0:Sliding,1:Absolute,預設1。如果是0,需要重新計算相對時間。
  • 如果刷新令牌請求包含了任意身份資源,創建新的身份令牌。
protected virtual async Task<TokenResponse> ProcessRefreshTokenRequestAsync(TokenRequestValidationResult request)
    {
        Logger.LogTrace("Creating response for refresh token request");

        var oldAccessToken = request.ValidatedRequest.RefreshToken.AccessToken;
        string accessTokenString;

        if (request.ValidatedRequest.Client.UpdateAccessTokenClaimsOnRefresh)
        {
            var subject = request.ValidatedRequest.RefreshToken.Subject;

            var creationRequest = new TokenCreationRequest
            {
                Subject = subject,
                ValidatedRequest = request.ValidatedRequest,
                Resources = await Resources.FindEnabledResourcesByScopeAsync(oldAccessToken.Scopes)
            };

            var newAccessToken = await TokenService.CreateAccessTokenAsync(creationRequest);
            accessTokenString = await TokenService.CreateSecurityTokenAsync(newAccessToken);
        }
        else
        {
            oldAccessToken.CreationTime = Clock.UtcNow.UtcDateTime;
            oldAccessToken.Lifetime = request.ValidatedRequest.AccessTokenLifetime;

            accessTokenString = await TokenService.CreateSecurityTokenAsync(oldAccessToken);
        }

        var handle = await RefreshTokenService.UpdateRefreshTokenAsync(request.ValidatedRequest.RefreshTokenHandle, request.ValidatedRequest.RefreshToken, request.ValidatedRequest.Client);

        return new TokenResponse
        {
            IdentityToken = await CreateIdTokenFromRefreshTokenRequestAsync(request.ValidatedRequest, accessTokenString),
            AccessToken = accessTokenString,
            AccessTokenLifetime = request.ValidatedRequest.AccessTokenLifetime,
            RefreshToken = handle,
            Custom = request.CustomResponse,
            Scope = request.ValidatedRequest.RefreshToken.Scopes.ToSpaceSeparatedString()
        };
    }

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、屬性賦值順序 1.屬性可以賦值的位置 ①預設初始化; ②顯示初始化; ③構造器中初始化; ④通過“對象.屬性”或“對象.方法”的方式對屬性進行賦值; 2.先後順序 ① - ② - ③ - ④ 二、JavaBean 所謂JavaBean,是指符合以下標準的類: 1.類是公共的; 2.有一個公共的無 ...
  • 一、構造器的作用 1.創建對象; 2.初始化對象的信息。 二、說明 1.如果沒顯式的定義類的構造器的話,則系統預設提供一個空參的構造器; 2.定義構造器的格式:許可權修飾符 類名(形參列表){}; 3.一個類中定義的多個構造器,彼此構成重載; 4.一旦我們顯式的定義了類的構造器之後,系統就不再提供預設 ...
  • 方法1 初始化方法參數說明 name:自定義日誌的名字, 預設是root, 但是我這裡是使用調用文件的__name__ 作為預設名字 path:生成的日誌的文件名 level:日誌的級別,我這裡把所有的級別都預設設置了level=DEBUG 方法2 使用logging.fileconfig這個模塊實 ...
  • 方法一: 知識點:random.sample(sequence, k) 從指定序列中隨機獲取指定長度的片斷 方法二: 知識點:random.choice(sequence) 從序列中獲取一個隨機元素 方法三: 知識點:random.randint(a,b) 用於生成一個指定範圍內的整數 方法四: 列 ...
  • 前言 忘了在哪看到一位編程大牛調侃,他說程式員每天就做兩件事,其中之一就是處理字元串。相信不少同學會有同感。 在Python中,我們經常會遇到字元串的拼接問題,幾乎任何一種編程語言,都把字元串列為最基礎和不可或缺的數據類型。而拼接字元串是必備的一種技能。今天,我跟大家一起來學習Python拼接字元串 ...
  • class Ticket implements Runnable { private static int tick = 100; boolean flag = true; @Override public void run() { if (flag) { while (true) { synchr ...
  • 一、為何要引入封裝性? 程式設計的重點是追求高內聚、低耦合: > 高內聚:類的內部數據操作細節自己完成,不允許外部干涉 > 低耦合:僅對外暴露少量的方法用於使用 隱藏對象內部的複雜性,只對外公開簡單的介面。便於外界調用,從而提高系統的可擴展性、可維護性。 二、問題的引入 當我們創建一個類的對象以後, ...
  • 前言 今天和某個人聊天聊到了 C 的 LINQ,發現我認識的 LINQ 似乎和大多數人認識的 LINQ 不太一樣,怎麼個不一樣法呢?其實 LINQ 也可以用來搞函數式編程。 當然,並不是說寫幾個 和用用像 Java 那樣的 之類的就算叫做 LINQ 了,LINQ 其實是一個另外的一些東西。 LINQ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...