Article catalogue
- AspNetCore3. 1_ Security source code analysis_ 1_ catalogue
- AspNetCore3. 1_ Security source code analysis_ 2_Authentication_ Core project
- AspNetCore3. 1_ Security source code analysis_ 3_Authentication_Cookies
- AspNetCore3. 1_ Security source code analysis_ 4_Authentication_JwtBear
- AspNetCore3. 1_ Security source code analysis_ 5_Authentication_OAuth
- AspNetCore3. 1_ Security source code analysis_ 6_Authentication_OpenIdConnect
- AspNetCore3. 1_ Security source code analysis_ 7_Authentication_ other
- AspNetCore3. 1_ Security source code analysis_ 8_Authorization_ Authorization framework
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?
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
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
- If the DataProtectionProvider is not configured, the default implementation will be used
- If the Backchannel is not configured, the default configuration will be processed
- 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.
Using Microsoft framework, it can be realized quickly
-
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
-
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.