AspNetCore3.1_Secutiry原始碼解析_5_Authentication_OAuth
阿新 • • 發佈:2020-03-25
title: "AspNetCore3.1_Secutiry原始碼解析_5_Authentication_OAuth"
date: 2020-03-24T23:27:45+08:00
draft: false
---
系列文章目錄
- AspNetCore3.1_Secutiry原始碼解析_1_目錄
- AspNetCore3.1_Secutiry原始碼解析_2_Authentication_核心流程
- AspNetCore3.1_Secutiry原始碼解析_3_Authentication_Cookies
- AspNetCore3.1_Secutiry原始碼解析_4_Authentication_JwtBear
- AspNetCore3.1_Secutiry原始碼解析_5_Authentication_OAuth(https://holdengong.com/aspnetcore3.1_secutiry原始碼解析_5_authentication_oauth)
- AspNetCore3.1_Secutiry原始碼解析_6_Authentication_OpenIdConnect
- AspNetCore3.1_Secutiry原始碼解析_7_Authentication_其他
- AspNetCore3.1_Secutiry原始碼解析_8_Authorization_核心專案
- AspNetCore3.1_Secutiry原始碼解析_9_Authorization_Policy
OAuth簡介
現在隨便一個網站,不用註冊,只用微信掃一掃,然後就可以自動登入,然後第三方網站右上角還出現了你的微信頭像和暱稱,怎麼做到的?
sequenceDiagram
使用者->>x站點: 請求微信登入
x站點->>微信: 請求 oauth token
微信->>使用者: x站點請求基本資料許可權,是否同意?
使用者->>微信: 同意
微信->>x站點: token
x站點->>微信: 請求user基本資料(token)
微信->微信: 校驗token
微信->>x站點: user基本資料
大概就這麼個意思,OAuth可以讓第三方獲取有限的授權去獲取資源。
入門的看部落格
https://www.cnblogs.com/linianhui/p/oauth2-authorization.html
英文好有基礎的直接看協議
https://tools.ietf.org/html/rfc6749
依賴注入
配置類:OAuthOptions
處理器類: 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 - 配置類
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下面是校驗邏輯,這些配置是必需的。
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 - 配置處理
- DataProtectionProvider沒有配置的話則使用預設實現
- Backchannel沒有配置的話則處理構造預設配置
- StateDataFormat沒有配置的話則使用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);
}
}
這個StateDataFormat就是處理state欄位的加密解密的,state在認證過程中用於防止跨站偽造攻擊和存放一些狀態資訊,我們看一下協議的定義
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.
比如,認證之後的回跳地址就是存放在這裡。所以如果希望從state欄位中解密得到資訊的話,就需要使用到PropertiesDataFormat。PropertiesDataFormat沒有任何程式碼,繼承自SecureDataFormat。 為什麼這裡介紹這麼多呢,因為實際專案中用到過這個。
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和AddShema的差別就是做了下面的處理,確認始終有不是遠端schema的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
- 解密state
- 校驗CorrelationId,防跨站偽造攻擊
- 如果error不為空說明失敗返回錯誤
- 拿到授權碼code,換取token
- 如果SaveTokens設定為true,將access_token,refresh_token,token_type存放到properties中
- 建立憑據,返回成功
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);
}
}