AspNetCore3. 1_ Security source code analysis_ 5_Authentication_OAuth

Article catalogue

Introduction to OAuth

Now any website doesn't need to register, just scan wechat, and then you can log in automatically. Then your wechat avatar and nickname appear in the upper right corner of the third-party website. How do you do that?

sequenceDiagram user - > > xsite: request wechat login to xsite - > > wechat: request oauth token wechat - > > User: xsite requests basic information permission. Do you agree? User - > > wechat: agree to wechat - > > xsite: token x site - > > wechat: request user's basic information (token) wechat - > wechat: verify token wechat - > > xsite: user's basic information

In this sense, OAuth allows third parties to obtain limited authorization to obtain resources.

Getting started reading blogs

https://www.cnblogs.com/linianhui/p/oauth2-authorization.html

Good English, basic direct reading agreement

https://tools.ietf.org/html/rfc6749

Dependency injection

Configuration class: OAuthOptions
Processor class: OAuthHandler

public static class OAuthExtensions
{
    public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions> configureOptions)
        => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, configureOptions);

    public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions> configureOptions)
        => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, displayName, configureOptions);

    public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions> configureOptions)
        where TOptions : OAuthOptions, new()
        where THandler : OAuthHandler<TOptions>
        => builder.AddOAuth<TOptions, THandler>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);

    public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
        where TOptions : OAuthOptions, new()
        where THandler : OAuthHandler<TOptions>
    {
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
        return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
    }
}

OAuthOptions - configuration class

classDiagram class OAuthOptions{ ClientId ClientSecret AuthorizationEndpoint TokenEndPoint UserInformationEndPoint Scope Events ClaimActions StateDataFormat } class RemoteAuthenticationOptions{ BackchannelTimeout BackchannelHttpHandler Backchannel DataProtectionProvider CallbackPath AccessDeniedPath ReturnUrlParameter SignInScheme RemoteAuthenticationTimeout SaveTokens } class AuthenticationSchemeOptions{ } OAuthOptions-->RemoteAuthenticationOptions RemoteAuthenticationOptions-->AuthenticationSchemeOptions

The following is the verification logic. These configurations are required.

public override void Validate()
{
    base.Validate();

    if (string.IsNullOrEmpty(ClientId))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientId)), nameof(ClientId));
    }

    if (string.IsNullOrEmpty(ClientSecret))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientSecret)), nameof(ClientSecret));
    }

    if (string.IsNullOrEmpty(AuthorizationEndpoint))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AuthorizationEndpoint)), nameof(AuthorizationEndpoint));
    }

    if (string.IsNullOrEmpty(TokenEndpoint))
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(TokenEndpoint)), nameof(TokenEndpoint));
    }

    if (!CallbackPath.HasValue)
    {
        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(CallbackPath)), nameof(CallbackPath));
    }
}

OAuthPostConfigureOptions - configuration processing

  1. If the DataProtectionProvider is not configured, the default implementation will be used
  2. If the Backchannel is not configured, the default configuration will be processed
  3. If StateDataFormat is not configured, use PropertiesDataFormat
public void PostConfigure(string name, TOptions options)
{
    options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
    if (options.Backchannel == null)
    {
        options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
        options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth handler");
        options.Backchannel.Timeout = options.BackchannelTimeout;
        options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
    }

    if (options.StateDataFormat == null)
    {
        var dataProtector = options.DataProtectionProvider.CreateProtector(
            typeof(THandler).FullName, name, "v1");
        options.StateDataFormat = new PropertiesDataFormat(dataProtector);
    }
}

This StateDataFormat handles the encryption and decryption of the state field. State is used to prevent cross site forgery attacks and store some state information in the authentication process. Let's take a look at the definition of the protocol

 state
         RECOMMENDED.  An opaque value used by the client to maintain
         state between the request and callback.  The authorization
         server includes this value when redirecting the user-agent back
         to the client.  The parameter SHOULD be used for preventing
         cross-site request forgery as described in Section 10.12.

For example, the bounce back address after authentication is stored here. Therefore, if you want to decrypt the information from the state field, you need to use the PropertiesDataFormat. PropertiesDataFormat has no code and inherits from SecureDataFormat. Why are there so many introductions here? Because this has been used in actual projects.

public class SecureDataFormat<TData> : ISecureDataFormat<TData>
{
    private readonly IDataSerializer<TData> _serializer;
    private readonly IDataProtector _protector;

    public SecureDataFormat(IDataSerializer<TData> serializer, IDataProtector protector)
    {
        _serializer = serializer;
        _protector = protector;
    }

    public string Protect(TData data)
    {
        return Protect(data, purpose: null);
    }

    public string Protect(TData data, string purpose)
    {
        var userData = _serializer.Serialize(data);

        var protector = _protector;
        if (!string.IsNullOrEmpty(purpose))
        {
            protector = protector.CreateProtector(purpose);
        }

        var protectedData = protector.Protect(userData);
        return Base64UrlTextEncoder.Encode(protectedData);
    }

    public TData Unprotect(string protectedText)
    {
        return Unprotect(protectedText, purpose: null);
    }

    public TData Unprotect(string protectedText, string purpose)
    {
        try
        {
            if (protectedText == null)
            {
                return default(TData);
            }

            var protectedData = Base64UrlTextEncoder.Decode(protectedText);
            if (protectedData == null)
            {
                return default(TData);
            }

            var protector = _protector;
            if (!string.IsNullOrEmpty(purpose))
            {
                protector = protector.CreateProtector(purpose);
            }

            var userData = protector.Unprotect(protectedData);
            if (userData == null)
            {
                return default(TData);
            }

            return _serializer.Deserialize(userData);
        }
        catch
        {
            // TODO trace exception, but do not leak other information
            return default(TData);
        }
    }
}

The difference between AddRemoteSchema and AddShema is that the following processing is done to confirm that there is always a SignInSchema that is not a remote schema

private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
{
    private readonly AuthenticationOptions _authOptions;

    public EnsureSignInScheme(IOptions<AuthenticationOptions> authOptions)
    {
        _authOptions = authOptions.Value;
    }

    public void PostConfigure(string name, TOptions options)
    {
        options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
    }
}

OAuthHandler

  • Decrypt state
  • Verify the CorrelationId to prevent cross site forgery attacks
  • If error is not empty, it indicates failure and returns an error
  • Get the authorization code in exchange for the token
  • If SaveTokens is set to true, access will be_ token,refresh_ token,token_ Type is stored in properties
  • Create credentials and return success
  protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var query = Request.Query;

            var state = query["state"];
            var properties = Options.StateDataFormat.Unprotect(state);

            if (properties == null)
            {
                return HandleRequestResult.Fail("The oauth state was missing or invalid.");
            }

            // OAuth2 10.12 CSRF
            if (!ValidateCorrelationId(properties))
            {
                return HandleRequestResult.Fail("Correlation failed.", properties);
            }

            var error = query["error"];
            if (!StringValues.IsNullOrEmpty(error))
            {
                // Note: access_denied errors are special protocol errors indicating the user didn't
                // approve the authorization demand requested by the remote authorization server.
                // Since it's a frequent scenario (that is not caused by incorrect configuration),
                // denied errors are handled differently using HandleAccessDeniedErrorAsync().
                // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
                if (StringValues.Equals(error, "access_denied"))
                {
                    return await HandleAccessDeniedErrorAsync(properties);
                }

                var failureMessage = new StringBuilder();
                failureMessage.Append(error);
                var errorDescription = query["error_description"];
                if (!StringValues.IsNullOrEmpty(errorDescription))
                {
                    failureMessage.Append(";Description=").Append(errorDescription);
                }
                var errorUri = query["error_uri"];
                if (!StringValues.IsNullOrEmpty(errorUri))
                {
                    failureMessage.Append(";Uri=").Append(errorUri);
                }

                return HandleRequestResult.Fail(failureMessage.ToString(), properties);
            }

            var code = query["code"];

            if (StringValues.IsNullOrEmpty(code))
            {
                return HandleRequestResult.Fail("Code was not found.", properties);
            }

            var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));

            if (tokens.Error != null)
            {
                return HandleRequestResult.Fail(tokens.Error, properties);
            }

            if (string.IsNullOrEmpty(tokens.AccessToken))
            {
                return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
            }

            var identity = new ClaimsIdentity(ClaimsIssuer);

            if (Options.SaveTokens)
            {
                var authTokens = new List<AuthenticationToken>();

                authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
                if (!string.IsNullOrEmpty(tokens.RefreshToken))
                {
                    authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
                }

                if (!string.IsNullOrEmpty(tokens.TokenType))
                {
                    authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
                }

                if (!string.IsNullOrEmpty(tokens.ExpiresIn))
                {
                    int value;
                    if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                    {
                        // https://www.w3.org/TR/xmlschema-2/#dateTime
                        // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                        var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                        authTokens.Add(new AuthenticationToken
                        {
                            Name = "expires_at",
                            Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                        });
                    }
                }

                properties.StoreTokens(authTokens);
            }

            var ticket = await CreateTicketAsync(identity, properties, tokens);
            if (ticket != null)
            {
                return HandleRequestResult.Success(ticket);
            }
            else
            {
                return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
            }
        }

actual combat

Recently, we did a third-party docking project. We have multiple sites and our own identity server authentication center. This joint project requires that our system be nested in their menus in the form of iframe. The whole docking process is roughly as follows.

sequenceDiagram third party - > > Third Party: log in to the third party - > > our system: click the menu to request the address our system - > > Third Party: jump to OAuth silent authorization address (1) third party - > > our system: jump to callback address with authorization code (2) our system - > > Third Party: use code for token (3) our system - > > Third Party: use token to read personal data (4) Our company's system - > > our company's system: user name password mode and our company's Certification Center silent authorization (5) our company's system - > > our company's system: Claims required for context injection, log in using CookieSchema to maintain the login state (6) our company's system - > > our company's system: jump back to the address at the beginning of authorization (7)

Using Microsoft framework, it can be realized quickly

  1. Definition, inherited from xxoautoptions

    • ClientId: required, client id
    • ClientSecret: required, client secret key
    • AuthorizationEndpoint: required, authorization address, corresponding step (1)
    • TokenEndpoint: required. The middleware will jump to this address with the authorization code to exchange for a token. The corresponding steps are (2, 3)
    • UserInformationEndpoint: optional. User information interface address. The framework does not use this attribute and needs to implement it by itself. The corresponding step (4)
    • CallbackPath: required. The return address after the authorization process is completed, corresponding to step (7)
    • Subscribe to events: events OnCreatingTicket += async (OAuthCreatingTicketContext context) =>
      {
      //Triggered when the user credentials are issued, synchronize the user information to the company, and use the ClientCredential mode to communicate with the company
      //The company realizes silent authorization for identity server authentication center communication
      //Then fill the relevant session information of the company into the credentials
      };
    • SignInSchema: login schema name after authentication (Cookies recommended)
    • If there is a unique configuration, it is also defined here
  2. Define XXOAuthHandler, which inherits from OAuthHandler

    • Rewrite ExchangeCodeAsync. This method is responsible for exchanging code for token. The parent class implementation uses form post. If any place does not match the actual situation, it can be rewritten
    • Rewrite the HandleChallengeAsync method, which is responsible for building the challenge address, that is, the silent authorization address + callback address in step (1)
    • Rewrite the CreateTicketAsync method, which is responsible for building user credentials, including all information that needs to be maintained in cookies in the future. You can request UserInformationEndpoint to request user information here, and then fill it in the voucher.
    • Override HandleRemoteAuthenticateAsync: this method is a trunk logical method. If it is different from the actual method, it can be overridden. Otherwise, it can be implemented by using the parent class.

Added by bmpco on Tue, 08 Mar 2022 23:56:29 +0200