title: "AspNetCore3.1_Secutiry源码解析_5_Authentication_OAuth"
date: 2020-03-24T23:27:45+08:00
draft: false
Series of articles directory
- AspNetCore3.1_Secutiry source directory resolve _1_
- AspNetCore3.1_Secutiry source parsing _2_Authentication_ core processes
- AspNetCore3.1_Secutiry source parsing _3_Authentication_Cookies
- AspNetCore3.1_Secutiry source parsing _4_Authentication_JwtBear
- AspNetCore3.1_Secutiry source parsing _5_Authentication_OAuth ( https://holdengong.com/aspnetcore3.1_secutiry source parsing _5_authentication_oauth )
- AspNetCore3.1_Secutiry source parsing _6_Authentication_OpenIdConnect
- AspNetCore3.1_Secutiry other source to resolve _7_Authentication_
- AspNetCore3.1_Secutiry source parsing _8_Authorization_ core projects
- AspNetCore3.1_Secutiry source parsing _9_Authorization_Policy
OAuth Profile
Now just a website, no registration, only micro-channel sweep swept away, and then you can automatically log in, and then the upper right corner of your third-party sites also appear micro letter head and nicknames, how do?
Probably such a meaning, OAuth allows a third party to obtain a limited license to access resources.
See blog entry
https://www.cnblogs.com/linianhui/p/oauth2-authorization.html
English look good and based on direct agreements
Dependency Injection
Configuration classes: OAuthOptions
handler 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 - class configuration
The following is a checking logic, which configuration is 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 process
- DataProtectionProvider not configured, then the default implementation
- Backchannel is not configured, then the default configuration process structure
- StateDataFormat not configured, then 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 encrypted state is decrypted field, state to prevent cross-site forgery attacks and to store some state information in the certification process, we 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, after the rebound address authentication is stored here. So if you want to get the decryption information from the state field, then you need to use to PropertiesDataFormat. PropertiesDataFormat no code, inherited from SecureDataFormat. Why introduce it here so much, because the actual project used before this.
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);
}
}
}
AddRemoteSchema and AddShema difference is to do the following treatment, there are not always confirm the remote schema of SignInSchema
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
- Decrypted state
- Check CorrelationId, anti-cross-site forgery attack
- If the error is not empty Description failed return error
- Get the authorization code code, in exchange for token
- If SaveTokens set to true, the access_token, refresh_token, token_type stored in properties in
- Creating credentials 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);
}
}