+
+
+ +

This sample demonstrates how to take advantage of ADAL JS for adding Azure AD authentication to your AngularJS apps.

+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs new file mode 100644 index 0000000000..7d9b391213 --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs @@ -0,0 +1,97 @@ +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace OpenIdConnect.AzureAdSample +{ + public class AuthPropertiesTokenCache : TokenCache + { + private const string TokenCacheKey = ".TokenCache"; + + private HttpContext _httpContext; + private ClaimsPrincipal _principal; + private AuthenticationProperties _authProperties; + private string _signInScheme; + + private AuthPropertiesTokenCache(AuthenticationProperties authProperties) : base() + { + _authProperties = authProperties; + BeforeAccess = BeforeAccessNotificationWithProperties; + AfterAccess = AfterAccessNotificationWithProperties; + BeforeWrite = BeforeWriteNotification; + } + + private AuthPropertiesTokenCache(HttpContext httpContext, string signInScheme) : base() + { + _httpContext = httpContext; + _signInScheme = signInScheme; + BeforeAccess = BeforeAccessNotificationWithContext; + AfterAccess = AfterAccessNotificationWithContext; + BeforeWrite = BeforeWriteNotification; + } + + public static TokenCache ForCodeRedemption(AuthenticationProperties authProperties) + { + return new AuthPropertiesTokenCache(authProperties); + } + + public static TokenCache ForApiCalls(HttpContext httpContext, + string signInScheme = CookieAuthenticationDefaults.AuthenticationScheme) + { + return new AuthPropertiesTokenCache(httpContext, signInScheme); + } + + private void BeforeAccessNotificationWithProperties(TokenCacheNotificationArgs args) + { + string cachedTokensText; + if (_authProperties.Items.TryGetValue(TokenCacheKey, out cachedTokensText)) + { + var cachedTokens = Convert.FromBase64String(cachedTokensText); + Deserialize(cachedTokens); + } + } + + private void BeforeAccessNotificationWithContext(TokenCacheNotificationArgs args) + { + // Retrieve the auth session with the cached tokens + var result = _httpContext.AuthenticateAsync(_signInScheme).Result; + _authProperties = result.Ticket.Properties; + _principal = result.Ticket.Principal; + + BeforeAccessNotificationWithProperties(args); + } + + private void AfterAccessNotificationWithProperties(TokenCacheNotificationArgs args) + { + // if state changed + if (HasStateChanged) + { + var cachedTokens = Serialize(); + var cachedTokensText = Convert.ToBase64String(cachedTokens); + _authProperties.Items[TokenCacheKey] = cachedTokensText; + } + } + + private void AfterAccessNotificationWithContext(TokenCacheNotificationArgs args) + { + // if state changed + if (HasStateChanged) + { + AfterAccessNotificationWithProperties(args); + + var cachedTokens = Serialize(); + var cachedTokensText = Convert.ToBase64String(cachedTokens); + _authProperties.Items[TokenCacheKey] = cachedTokensText; + _httpContext.SignInAsync(_signInScheme, _principal, _authProperties).Wait(); + } + } + + private void BeforeWriteNotification(TokenCacheNotificationArgs args) + { + // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry + } + } +} diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj b/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj new file mode 100644 index 0000000000..b14b9590f5 --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj @@ -0,0 +1,23 @@ + + + + net461;netcoreapp2.1 + aspnet5-OpenIdConnectSample-20151210110318 + + + + + + + + + + + + + + + + + + diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs new file mode 100644 index 0000000000..0e1285a9c6 --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs @@ -0,0 +1,27 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace OpenIdConnect.AzureAdSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel() + .UseUrls("http://localhost:42023") + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json b/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json new file mode 100644 index 0000000000..e6436fee2a --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42023", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "OpenIdConnect": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:42023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md b/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md new file mode 100644 index 0000000000..767e336ac6 --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md @@ -0,0 +1,20 @@ +# How to set up the sample locally + +## Set up [Azure Active Directory](https://azure.microsoft.com/en-us/documentation/services/active-directory/) + +1. Create your own Azure Active Directory (AD). Save the "tenent name". +2. Add a new Application: in the Azure AD portal, select Application, and click Add in the drawer. +3. Set the sign-on url to `http://localhost:42023`. +4. Select the newly created Application, navigate to the Configure tab. +5. Find and save the "Client Id" +8. In the keys section add a new key. A key value will be generated. Save the value as "Client Secret" + +## Configure the local environment +1. Set environment ASPNETCORE_ENVIRONMENT to DEVELOPMENT. ([Working with Multiple Environments](https://docs.asp.net/en/latest/fundamentals/environments.html)) +2. Set up user secrets: +``` +dotnet user-secrets set oidc:clientid +dotnet user-secrets set oidc:clientsecret +dotnet user-secrets set oidc:authority https://login.windows.net/.onmicrosoft.com +``` + diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs new file mode 100644 index 0000000000..c3fa3c719b --- /dev/null +++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace OpenIdConnect.AzureAdSample +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; set; } + + private string ClientId => Configuration["oidc:clientid"]; + private string ClientSecret => Configuration["oidc:clientsecret"]; + private string Authority => Configuration["oidc:authority"]; + private string Resource => "https://graph.windows.net"; + + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "AAD", o => + { + o.ClientId = ClientId; + o.ClientSecret = ClientSecret; // for code flow + o.Authority = Authority; + o.ResponseType = OpenIdConnectResponseType.CodeIdToken; + o.SignedOutRedirectUri = "/signed-out"; + // GetClaimsFromUserInfoEndpoint = true, + o.Events = new OpenIdConnectEvents() + { + OnAuthorizationCodeReceived = async context => + { + var request = context.HttpContext.Request; + var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); + var credential = new ClientCredential(ClientId, ClientSecret); + var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForCodeRedemption(context.Properties)); + + var result = await authContext.AcquireTokenByAuthorizationCodeAsync( + context.ProtocolMessage.Code, new Uri(currentUri), credential, Resource); + + context.HandleCodeRedemption(result.AccessToken, result.IdToken); + } + }; + }); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseAuthentication(); + + app.Run(async context => + { + if (context.Request.Path.Equals("/signin")) + { + if (context.User.Identities.Any(identity => identity.IsAuthenticated)) + { + // User has already signed in + context.Response.Redirect("/"); + return; + } + + await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" }); + } + else if (context.Request.Path.Equals("/signout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

Signed out locally: {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign In"); + }); + } + else if (context.Request.Path.Equals("/signout-remote")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (context.Request.Path.Equals("/signed-out")) + { + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

You have been signed out.

"); + await response.WriteAsync("Sign In"); + }); + } + else if (context.Request.Path.Equals("/remote-signedout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

Signed out remotely: {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign In"); + }); + } + else + { + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) + { + await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" }); + return; + } + + await WriteHtmlAsync(context.Response, async response => + { + await response.WriteAsync($"

Hello Authenticated User {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign Out Locally"); + await response.WriteAsync("Sign Out Remotely"); + + await response.WriteAsync("

Claims:

"); + await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + + await response.WriteAsync("

Tokens:

"); + try + { + // Use ADAL to get the right token + var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForApiCalls(context, CookieAuthenticationDefaults.AuthenticationScheme)); + var credential = new ClientCredential(ClientId, ClientSecret); + string userObjectID = context.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var result = await authContext.AcquireTokenSilentAsync(Resource, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)); + + await response.WriteAsync($"

access_token

{HtmlEncode(result.AccessToken)}
"); + } + catch (Exception ex) + { + await response.WriteAsync($"AquireToken error: {ex.Message}"); + } + }); + } + }); + } + + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) + { + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); + } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); + } +} + diff --git a/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj b/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj new file mode 100644 index 0000000000..23e87d4f2a --- /dev/null +++ b/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj @@ -0,0 +1,33 @@ + + + + net461;netcoreapp2.1 + aspnet5-OpenIdConnectSample-20151210110318 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Security/samples/OpenIdConnectSample/Program.cs b/src/Security/samples/OpenIdConnectSample/Program.cs new file mode 100644 index 0000000000..87e7755084 --- /dev/null +++ b/src/Security/samples/OpenIdConnectSample/Program.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace OpenIdConnectSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddDebug(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + factory.AddFilter("Debug", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44318, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var assembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(assembly, "OpenIdConnectSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json b/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json new file mode 100644 index 0000000000..058fa4c5dd --- /dev/null +++ b/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42023", + "sslPort": 44318 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "https://localhost:44318/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "OpenIdConnectSample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44318/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Security/samples/OpenIdConnectSample/Readme.md b/src/Security/samples/OpenIdConnectSample/Readme.md new file mode 100644 index 0000000000..846e3f8e6a --- /dev/null +++ b/src/Security/samples/OpenIdConnectSample/Readme.md @@ -0,0 +1,44 @@ +# How to set up the sample locally + +The OpenIdConnect sample supports multilpe authentication providers. In these instruction, we will explore how to set up this sample with both Azure Active Directory and Google Identity Platform. + +## Determine your development environment and a few key variables + +This sample is configured to run on port __44318__ locally. In Visual Studio, the setting is carried out in `.\properties\launchSettings.json`. When the application is run from command line, the URL is coded in `Program.cs`. + +If the application is run from command line or terminal, environment variable ASPNETCORE_ENVIRONMENT should be set to DEVELOPMENT to enable user secret. + +## Configure the Authorization server + +### Configure with Azure Active Directory + +1. Set up a new Azure Active Directory (AAD) in your Azure Subscription. +2. Open the newly created AAD in Azure web portal. +3. Navigate to the Applications tab. +4. Add a new Application to the AAD. Set the "Sign-on URL" to sample application's URL. +5. Naigate to the Application, and click the Configure tab. +6. Find and save the "Client Id". +7. Add a new key in the "Keys" section. Save value of the key, which is the "Client Secret". +8. Click the "View Endpoints" on the drawer, a dialog will shows six endpoint URLs. Copy the "OAuth 2.0 Authorization Endpoint" to a text editor and remove the "/oauth2/authorize" from the string. The remaining part is the __authority URL__. It looks like `https://login.microsoftonline.com/`. + +### Configure with Google Identity Platform + +1. Create a new project through [Google APIs](https://console.developers.google.com). +2. In the sidebar choose "Credentials". +3. Navigate to "OAuth consent screen" tab, fill in the project name and save. +4. Navigate to "Credentials" tab. Click "Create credentials". Choose "OAuth client ID". +5. Select "Web application" as the application type. Fill in the "Authorized redirect URIs" with `https://localhost:44318/signin-oidc`. +6. Save the "Client ID" and "Client Secret" shown in the dialog. +7. The "Authority URL" for Google Authentication is `https://accounts.google.com/`. + +## Configure the sample application + +1. Restore the application. +2. Set user secrets: + + ``` +dotnet user-secrets set oidc:clientid +dotnet user-secrets set oidc:clientsecret +dotnet user-secrets set oidc:authority +``` + diff --git a/src/Security/samples/OpenIdConnectSample/Startup.cs b/src/Security/samples/OpenIdConnectSample/Startup.cs new file mode 100644 index 0000000000..1aa7625cb0 --- /dev/null +++ b/src/Security/samples/OpenIdConnectSample/Startup.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Newtonsoft.Json.Linq; + +namespace OpenIdConnectSample +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + Environment = env; + + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; set; } + + public IHostingEnvironment Environment { get; set; } + + public void ConfigureServices(IServiceCollection services) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(o => + { + o.ClientId = Configuration["oidc:clientid"]; + o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow + o.Authority = Configuration["oidc:authority"]; + + o.ResponseType = OpenIdConnectResponseType.CodeIdToken; + o.SaveTokens = true; + o.GetClaimsFromUserInfoEndpoint = true; + + o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce"); + + o.Events = new OpenIdConnectEvents() + { + OnAuthenticationFailed = c => + { + c.HandleResponse(); + + c.Response.StatusCode = 500; + c.Response.ContentType = "text/plain"; + if (Environment.IsDevelopment()) + { + // Debug only, in production do not share exceptions with the remote host. + return c.Response.WriteAsync(c.Exception.ToString()); + } + return c.Response.WriteAsync("An error occurred processing your authentication."); + } + }; + }); + } + + public void Configure(IApplicationBuilder app, IOptionsMonitor optionsMonitor) + { + app.UseDeveloperExceptionPage(); + app.UseAuthentication(); + + app.Run(async context => + { + var response = context.Response; + + if (context.Request.Path.Equals("/signedout")) + { + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

You have been signed out.

"); + await res.WriteAsync("Home"); + }); + return; + } + + if (context.Request.Path.Equals("/signout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Signed out {HtmlEncode(context.User.Identity.Name)}

"); + await res.WriteAsync("Home"); + }); + return; + } + + if (context.Request.Path.Equals("/signout-remote")) + { + // Redirects + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() + { + RedirectUri = "/signedout" + }); + return; + } + + if (context.Request.Path.Equals("/Account/AccessDenied")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'

"); + await res.WriteAsync("Sign Out"); + await res.WriteAsync("Home"); + }); + return; + } + + // DefaultAuthenticateScheme causes User to be set + // var user = context.User; + + // This is what [Authorize] calls + var userResult = await context.AuthenticateAsync(); + var user = userResult.Principal; + var props = userResult.Properties; + + // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme); + + // Not authenticated + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); + + return; + } + + // Authenticated, but not authorized + if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true"))) + { + await context.ForbidAsync(); + return; + } + + if (context.Request.Path.Equals("/refresh")) + { + var refreshToken = props.GetTokenValue("refresh_token"); + + if (string.IsNullOrEmpty(refreshToken)) + { + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"No refresh_token is available.
"); + await res.WriteAsync("Sign Out"); + }); + + return; + } + + var options = optionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme); + var metadata = await options.ConfigurationManager.GetConfigurationAsync(context.RequestAborted); + + var pairs = new Dictionary() + { + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "grant_type", "refresh_token" }, + { "refresh_token", refreshToken } + }; + var content = new FormUrlEncodedContent(pairs); + var tokenResponse = await options.Backchannel.PostAsync(metadata.TokenEndpoint, content, context.RequestAborted); + tokenResponse.EnsureSuccessStatusCode(); + + var payload = JObject.Parse(await tokenResponse.Content.ReadAsStringAsync()); + + // Persist the new acess token + props.UpdateTokenValue("access_token", payload.Value("access_token")); + props.UpdateTokenValue("refresh_token", payload.Value("refresh_token")); + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, props); + + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Refreshed.

"); + await res.WriteAsync("Refresh tokens"); + await res.WriteAsync("Home"); + + await res.WriteAsync("

Tokens:

"); + await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); + + await res.WriteAsync("

Payload:

"); + await res.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",
") + "
"); + }); + + return; + } + + if (context.Request.Path.Equals("/login-challenge")) + { + // Challenge the user authentication, and force a login prompt by overwriting the + // "prompt". This could be used for example to require the user to re-enter their + // credentials at the authentication provider, to add an extra confirmation layer. + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties() + { + Prompt = "login", + + // it is also possible to specify different scopes, e.g. + // Scope = new string[] { "openid", "profile", "other" } + }); + + return; + } + + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Hello Authenticated User {HtmlEncode(user.Identity.Name)}

"); + await res.WriteAsync("Refresh tokens"); + await res.WriteAsync("Restricted"); + await res.WriteAsync("Login challenge"); + await res.WriteAsync("Sign Out"); + await res.WriteAsync("Sign Out Remote"); + + await res.WriteAsync("

Claims:

"); + await WriteTableHeader(res, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + + await res.WriteAsync("

Tokens:

"); + await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); + }); + }); + } + + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) + { + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); + } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); + } +} + diff --git a/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx b/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx differ diff --git a/src/Security/samples/SocialSample/Program.cs b/src/Security/samples/SocialSample/Program.cs new file mode 100644 index 0000000000..a712b6c03f --- /dev/null +++ b/src/Security/samples/SocialSample/Program.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace SocialSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44318, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var socialSampleAssembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(socialSampleAssembly, "SocialSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/src/Security/samples/SocialSample/Properties/launchSettings.json b/src/Security/samples/SocialSample/Properties/launchSettings.json new file mode 100644 index 0000000000..30bf2e5f6a --- /dev/null +++ b/src/Security/samples/SocialSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54540", + "sslPort": 44318 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "https://localhost:44318/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "SocialSample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44318/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Security/samples/SocialSample/SocialSample.csproj b/src/Security/samples/SocialSample/SocialSample.csproj new file mode 100644 index 0000000000..a423ae21a3 --- /dev/null +++ b/src/Security/samples/SocialSample/SocialSample.csproj @@ -0,0 +1,36 @@ + + + + net461;netcoreapp2.1 + aspnet5-SocialSample-20151210111056 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Security/samples/SocialSample/Startup.cs b/src/Security/samples/SocialSample/Startup.cs new file mode 100644 index 0000000000..35896e84b1 --- /dev/null +++ b/src/Security/samples/SocialSample/Startup.cs @@ -0,0 +1,502 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Facebook; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.Twitter; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + +namespace SocialSample +{ + /* Note all servers must use the same address and port because these are pre-registered with the various providers. */ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfiguration Configuration { get; set; } + + public void ConfigureServices(IServiceCollection services) + { + if (string.IsNullOrEmpty(Configuration["facebook:appid"])) + { + // User-Secrets: https://docs.asp.net/en/latest/security/app-secrets.html + // See below for registration instructions for each provider. + throw new InvalidOperationException("User secrets must be configured for each authentication provider."); + } + + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(o => o.LoginPath = new PathString("/login")) + // You must first create an app with Facebook and add its ID and Secret to your user-secrets. + // https://developers.facebook.com/apps/ + .AddFacebook(o => + { + o.AppId = Configuration["facebook:appid"]; + o.AppSecret = Configuration["facebook:appsecret"]; + o.Scope.Add("email"); + o.Fields.Add("name"); + o.Fields.Add("email"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Google and add its ID and Secret to your user-secrets. + // https://console.developers.google.com/project + .AddOAuth("Google-AccessToken", "Google AccessToken only", o => + { + o.ClientId = Configuration["google:clientid"]; + o.ClientSecret = Configuration["google:clientsecret"]; + o.CallbackPath = new PathString("/signin-google-token"); + o.AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint; + o.TokenEndpoint = GoogleDefaults.TokenEndpoint; + o.Scope.Add("openid"); + o.Scope.Add("profile"); + o.Scope.Add("email"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Google and add its ID and Secret to your user-secrets. + // https://console.developers.google.com/project + .AddGoogle(o => + { + o.ClientId = Configuration["google:clientid"]; + o.ClientSecret = Configuration["google:clientsecret"]; + o.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response + o.AccessType = "offline"; + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + o.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url"); + o.ClaimActions.Remove(ClaimTypes.GivenName); + }) + // You must first create an app with Twitter and add its key and Secret to your user-secrets. + // https://apps.twitter.com/ + .AddTwitter(o => + { + o.ConsumerKey = Configuration["twitter:consumerkey"]; + o.ConsumerSecret = Configuration["twitter:consumersecret"]; + // http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370 + // http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1 + o.RetrieveUserDetails = true; + o.SaveTokens = true; + o.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri); + o.Events = new TwitterEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + /* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs. + Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL: + https://localhost:44318/ + */ + // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets. + // https://apps.dev.microsoft.com/ + .AddOAuth("Microsoft-AccessToken", "Microsoft AccessToken only", o => + { + o.ClientId = Configuration["microsoftaccount:clientid"]; + o.ClientSecret = Configuration["microsoftaccount:clientsecret"]; + o.CallbackPath = new PathString("/signin-microsoft-token"); + o.AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint; + o.TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; + o.Scope.Add("https://graph.microsoft.com/user.read"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets. + // https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-app-registration/ + .AddMicrosoftAccount(o => + { + o.ClientId = Configuration["microsoftaccount:clientid"]; + o.ClientSecret = Configuration["microsoftaccount:clientsecret"]; + o.SaveTokens = true; + o.Scope.Add("offline_access"); + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with GitHub and add its ID and Secret to your user-secrets. + // https://github.com/settings/applications/ + .AddOAuth("GitHub-AccessToken", "GitHub AccessToken only", o => + { + o.ClientId = Configuration["github-token:clientid"]; + o.ClientSecret = Configuration["github-token:clientsecret"]; + o.CallbackPath = new PathString("/signin-github-token"); + o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + o.TokenEndpoint = "https://github.com/login/oauth/access_token"; + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with GitHub and add its ID and Secret to your user-secrets. + // https://github.com/settings/applications/ + .AddOAuth("GitHub", "Github", o => + { + o.ClientId = Configuration["github:clientid"]; + o.ClientSecret = Configuration["github:clientsecret"]; + o.CallbackPath = new PathString("/signin-github"); + o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + o.TokenEndpoint = "https://github.com/login/oauth/access_token"; + o.UserInformationEndpoint = "https://api.github.com/user"; + o.ClaimsIssuer = "OAuth2-Github"; + o.SaveTokens = true; + // Retrieving user information is unique to each provider. + o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + o.ClaimActions.MapJsonKey(ClaimTypes.Name, "login"); + o.ClaimActions.MapJsonKey("urn:github:name", "name"); + o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + o.ClaimActions.MapJsonKey("urn:github:url", "url"); + o.Events = new OAuthEvents + { + OnRemoteFailure = HandleOnRemoteFailure, + OnCreatingTicket = async context => + { + // Get the GitHub user + var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted); + response.EnsureSuccessStatusCode(); + + var user = JObject.Parse(await response.Content.ReadAsStringAsync()); + + context.RunClaimActions(user); + } + }; + }); + } + + private async Task HandleOnRemoteFailure(RemoteFailureContext context) + { + context.Response.StatusCode = 500; + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("A remote failure has occurred: " + UrlEncoder.Default.Encode(context.Failure.Message) + "
"); + + if (context.Properties != null) + { + await context.Response.WriteAsync("Properties:
"); + foreach (var pair in context.Properties.Items) + { + await context.Response.WriteAsync($"-{ UrlEncoder.Default.Encode(pair.Key)}={ UrlEncoder.Default.Encode(pair.Value)}
"); + } + } + + await context.Response.WriteAsync("Home"); + await context.Response.WriteAsync(""); + + // context.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(context.Failure.Message)); + + context.HandleResponse(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseAuthentication(); + + // Choose an authentication type + app.Map("/login", signinApp => + { + signinApp.Run(async context => + { + var authType = context.Request.Query["authscheme"]; + if (!string.IsNullOrEmpty(authType)) + { + // By default the client will be redirect back to the URL that issued the challenge (/login?authtype=foo), + // send them to the home page instead (/). + await context.ChallengeAsync(authType, new AuthenticationProperties() { RedirectUri = "/" }); + return; + } + + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Choose an authentication scheme:
"); + var schemeProvider = context.RequestServices.GetRequiredService(); + foreach (var provider in await schemeProvider.GetAllSchemesAsync()) + { + await response.WriteAsync("" + (provider.DisplayName ?? "(suppressed)") + "
"); + } + await response.WriteAsync(""); + }); + }); + + // Refresh the access token + app.Map("/refresh_token", signinApp => + { + signinApp.Run(async context => + { + var response = context.Response; + + // Setting DefaultAuthenticateScheme causes User to be set + // var user = context.User; + + // This is what [Authorize] calls + var userResult = await context.AuthenticateAsync(); + var user = userResult.Principal; + var authProperties = userResult.Properties; + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + // Deny anonymous request beyond this point. + if (!userResult.Succeeded || user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + // The cookie middleware will handle this and redirect to /login + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + return; + } + + var currentAuthType = user.Identities.First().AuthenticationType; + if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType) + || string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType)) + { + var refreshToken = authProperties.GetTokenValue("refresh_token"); + + if (string.IsNullOrEmpty(refreshToken)) + { + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("No refresh_token is available.
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); + return; + } + + var options = await GetOAuthOptionsAsync(context, currentAuthType); + + var pairs = new Dictionary() + { + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "grant_type", "refresh_token" }, + { "refresh_token", refreshToken } + }; + var content = new FormUrlEncodedContent(pairs); + var refreshResponse = await options.Backchannel.PostAsync(options.TokenEndpoint, content, context.RequestAborted); + refreshResponse.EnsureSuccessStatusCode(); + + var payload = JObject.Parse(await refreshResponse.Content.ReadAsStringAsync()); + + // Persist the new acess token + authProperties.UpdateTokenValue("access_token", payload.Value("access_token")); + refreshToken = payload.Value("refresh_token"); + if (!string.IsNullOrEmpty(refreshToken)) + { + authProperties.UpdateTokenValue("refresh_token", refreshToken); + } + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, authProperties); + + await PrintRefreshedTokensAsync(response, payload, authProperties); + + return; + } + // https://developers.facebook.com/docs/facebook-login/access-tokens/expiration-and-extension + else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType)) + { + var options = await GetOAuthOptionsAsync(context, currentAuthType); + + var accessToken = authProperties.GetTokenValue("access_token"); + + var query = new QueryBuilder() + { + { "grant_type", "fb_exchange_token" }, + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "fb_exchange_token", accessToken }, + }.ToQueryString(); + + var refreshResponse = await options.Backchannel.GetStringAsync(options.TokenEndpoint + query); + var payload = JObject.Parse(refreshResponse); + + authProperties.UpdateTokenValue("access_token", payload.Value("access_token")); + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, authProperties); + + await PrintRefreshedTokensAsync(response, payload, authProperties); + + return; + } + + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Refresh has not been implemented for this provider.
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); + }); + }); + + // Sign-out to remove the user cookie. + app.Map("/logout", signoutApp => + { + signoutApp.Run(async context => + { + var response = context.Response; + response.ContentType = "text/html"; + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await response.WriteAsync(""); + await response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); + }); + }); + + // Display the remote error + app.Map("/error", errorApp => + { + errorApp.Run(async context => + { + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); + }); + }); + + + app.Run(async context => + { + // Setting DefaultAuthenticateScheme causes User to be set + var user = context.User; + + // This is what [Authorize] calls + // var user = await context.AuthenticateAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + // Deny anonymous request beyond this point. + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + // The cookie middleware will handle this and redirect to /login + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + return; + } + + // Display user information + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Hello " + (context.User.Identity.Name ?? "anonymous") + "
"); + foreach (var claim in context.User.Claims) + { + await response.WriteAsync(claim.Type + ": " + claim.Value + "
"); + } + + await response.WriteAsync("Tokens:
"); + + await response.WriteAsync("Access Token: " + await context.GetTokenAsync("access_token") + "
"); + await response.WriteAsync("Refresh Token: " + await context.GetTokenAsync("refresh_token") + "
"); + await response.WriteAsync("Token Type: " + await context.GetTokenAsync("token_type") + "
"); + await response.WriteAsync("expires_at: " + await context.GetTokenAsync("expires_at") + "
"); + await response.WriteAsync("Logout
"); + await response.WriteAsync("Refresh Token
"); + await response.WriteAsync(""); + }); + } + + private Task GetOAuthOptionsAsync(HttpContext context, string currentAuthType) + { + if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + else if (string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + + throw new NotImplementedException(currentAuthType); + } + + private async Task PrintRefreshedTokensAsync(HttpResponse response, JObject payload, AuthenticationProperties authProperties) + { + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Refreshed.
"); + await response.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",
") + "
"); + + await response.WriteAsync("
Tokens:
"); + + await response.WriteAsync("Access Token: " + authProperties.GetTokenValue("access_token") + "
"); + await response.WriteAsync("Refresh Token: " + authProperties.GetTokenValue("refresh_token") + "
"); + await response.WriteAsync("Token Type: " + authProperties.GetTokenValue("token_type") + "
"); + await response.WriteAsync("expires_at: " + authProperties.GetTokenValue("expires_at") + "
"); + + await response.WriteAsync("Home
"); + await response.WriteAsync("Refresh Token
"); + await response.WriteAsync(""); + } + } +} + diff --git a/src/Security/samples/SocialSample/compiler/resources/cert.pfx b/src/Security/samples/SocialSample/compiler/resources/cert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/src/Security/samples/SocialSample/compiler/resources/cert.pfx differ diff --git a/src/Security/samples/SocialSample/web.config b/src/Security/samples/SocialSample/web.config new file mode 100644 index 0000000000..f7ac679334 --- /dev/null +++ b/src/Security/samples/SocialSample/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Security/samples/WsFedSample/Program.cs b/src/Security/samples/WsFedSample/Program.cs new file mode 100644 index 0000000000..40e1945c69 --- /dev/null +++ b/src/Security/samples/WsFedSample/Program.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace WsFedSample +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddDebug(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + factory.AddFilter("Debug", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44307, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var assembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(assembly, "WsFedSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/src/Security/samples/WsFedSample/Properties/launchSettings.json b/src/Security/samples/WsFedSample/Properties/launchSettings.json new file mode 100644 index 0000000000..bdf80e2481 --- /dev/null +++ b/src/Security/samples/WsFedSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44307/", + "sslPort": 44318 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "https://localhost:44307/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WsFedSample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44307/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Security/samples/WsFedSample/Startup.cs b/src/Security/samples/WsFedSample/Startup.cs new file mode 100644 index 0000000000..0fc32769e9 --- /dev/null +++ b/src/Security/samples/WsFedSample/Startup.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.WsFederation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace WsFedSample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddWsFederation(options => + { + options.Wtrealm = "https://Tratcheroutlook.onmicrosoft.com/WsFedSample"; + options.MetadataAddress = "https://login.windows.net/cdc690f9-b6b8-4023-813a-bae7143d1f87/FederationMetadata/2007-06/FederationMetadata.xml"; + // options.CallbackPath = "/"; + // options.SkipUnrecognizedRequests = true; + }) + .AddCookie(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseAuthentication(); + + app.Run(async context => + { + if (context.Request.Path.Equals("/signedout")) + { + await WriteHtmlAsync(context.Response, async res => + { + await res.WriteAsync($"

You have been signed out.

"); + await res.WriteAsync("Sign In"); + }); + return; + } + + if (context.Request.Path.Equals("/signout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, async res => + { + await context.Response.WriteAsync($"

Signed out {HtmlEncode(context.User.Identity.Name)}

"); + await context.Response.WriteAsync("Sign In"); + }); + return; + } + + if (context.Request.Path.Equals("/signout-remote")) + { + // Redirects + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, new AuthenticationProperties() + { + RedirectUri = "/signedout" + }); + return; + } + + if (context.Request.Path.Equals("/Account/AccessDenied")) + { + await WriteHtmlAsync(context.Response, async res => + { + await context.Response.WriteAsync($"

Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'

"); + await context.Response.WriteAsync("Sign Out"); + }); + return; + } + + // DefaultAuthenticateScheme causes User to be set + var user = context.User; + + // This is what [Authorize] calls + // var user = await context.AuthenticateAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(WsFederationDefaults.AuthenticationScheme); + + // Not authenticated + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + + return; + } + + // Authenticated, but not authorized + if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true"))) + { + await context.ForbidAsync(); + return; + } + + await WriteHtmlAsync(context.Response, async response => + { + await response.WriteAsync($"

Hello Authenticated User {HtmlEncode(user.Identity.Name)}

"); + await response.WriteAsync("Restricted"); + await response.WriteAsync("Sign Out"); + await response.WriteAsync("Sign Out Remote"); + + await response.WriteAsync("

Claims:

"); + await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + }); + }); + } + + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) + { + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); + } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); + } +} diff --git a/src/Security/samples/WsFedSample/WsFedSample.csproj b/src/Security/samples/WsFedSample/WsFedSample.csproj new file mode 100644 index 0000000000..bc3a59f10e --- /dev/null +++ b/src/Security/samples/WsFedSample/WsFedSample.csproj @@ -0,0 +1,27 @@ + + + + net461;netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Security/samples/WsFedSample/compiler/resources/cert.pfx b/src/Security/samples/WsFedSample/compiler/resources/cert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/src/Security/samples/WsFedSample/compiler/resources/cert.pfx differ diff --git a/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs b/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs new file mode 100644 index 0000000000..42cc4e2f0f --- /dev/null +++ b/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs @@ -0,0 +1,309 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +// Keep the type public for Security repo as it would be a breaking change to change the accessor now. +// Make this type internal for other repos as it could be used by multiple projects and having it public causes type conflicts. +#if SECURITY +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + public class ChunkingCookieManager : ICookieManager + { +#else +namespace Microsoft.AspNetCore.Internal +{ + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + internal class ChunkingCookieManager + { +#endif + /// + /// The default maximum size of characters in a cookie to send back to the client. + /// + public const int DefaultChunkSize = 4050; + + private const string ChunkKeySuffix = "C"; + private const string ChunkCountPrefix = "chunks-"; + + public ChunkingCookieManager() + { + // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case. + // See http://browsercookielimits.x64.me/. + // Leave at least 40 in case CookiePolicy tries to add 'secure', 'samesite=strict' and/or 'httponly'. + ChunkSize = DefaultChunkSize; + } + + /// + /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple + /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all + /// common browsers. + /// + /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain. + /// + public int? ChunkSize { get; set; } + + /// + /// Throw if not all chunks of a cookie are available on a request for re-assembly. + /// + public bool ThrowForPartialCookies { get; set; } + + // Parse the "chunks-XX" to determine how many chunks there should be. + private static int ParseChunksCount(string value) + { + if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal)) + { + var chunksCountString = value.Substring(ChunkCountPrefix.Length); + int chunksCount; + if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount)) + { + return chunksCount; + } + } + return 0; + } + + /// + /// Get the reassembled cookie. Non chunked cookies are returned normally. + /// Cookies with missing chunks just have their "chunks-XX" header returned. + /// + /// + /// + /// The reassembled cookie, if any, or null. + public string GetRequestCookie(HttpContext context, string key) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var requestCookies = context.Request.Cookies; + var value = requestCookies[key]; + var chunksCount = ParseChunksCount(value); + if (chunksCount > 0) + { + var chunks = new string[chunksCount]; + for (var chunkId = 1; chunkId <= chunksCount; chunkId++) + { + var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)]; + if (string.IsNullOrEmpty(chunk)) + { + if (ThrowForPartialCookies) + { + var totalSize = 0; + for (int i = 0; i < chunkId - 1; i++) + { + totalSize += chunks[i].Length; + } + throw new FormatException( + string.Format( + CultureInfo.CurrentCulture, + "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.", + chunkId - 1, + chunksCount, + totalSize)); + } + // Missing chunk, abort by returning the original cookie value. It may have been a false positive? + return value; + } + + chunks[chunkId - 1] = chunk; + } + + return string.Join(string.Empty, chunks); + } + return value; + } + + /// + /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit + /// then it will be broken down into multiple cookies as follows: + /// Set-Cookie: CookieName=chunks-3; path=/ + /// Set-Cookie: CookieNameC1=Segment1; path=/ + /// Set-Cookie: CookieNameC2=Segment2; path=/ + /// Set-Cookie: CookieNameC3=Segment3; path=/ + /// + /// + /// + /// + /// + public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var template = new SetCookieHeaderValue(key) + { + Domain = options.Domain, + Expires = options.Expires, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly, + Path = options.Path, + Secure = options.Secure, + }; + + var templateLength = template.ToString().Length; + + value = value ?? string.Empty; + + // Normal cookie + var responseCookies = context.Response.Cookies; + if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length) + { + responseCookies.Append(key, value, options); + } + else if (ChunkSize.Value < templateLength + 10) + { + // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX". + // No room for data, we can't chunk the options and name + throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data."); + } + else + { + // Break the cookie down into multiple cookies. + // Key = CookieName, value = "Segment1Segment2Segment2" + // Set-Cookie: CookieName=chunks-3; path=/ + // Set-Cookie: CookieNameC1="Segment1"; path=/ + // Set-Cookie: CookieNameC2="Segment2"; path=/ + // Set-Cookie: CookieNameC3="Segment3"; path=/ + var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid. + var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie); + + responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options); + + var offset = 0; + for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++) + { + var remainingLength = value.Length - offset; + var length = Math.Min(dataSizePerCookie, remainingLength); + var segment = value.Substring(offset, length); + offset += length; + + responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options); + } + } + } + + /// + /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on + /// the request, delete each chunk. + /// + /// + /// + /// + public void DeleteCookie(HttpContext context, string key, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var keys = new List(); + keys.Add(key + "="); + + var requestCookie = context.Request.Cookies[key]; + var chunks = ParseChunksCount(requestCookie); + if (chunks > 0) + { + for (int i = 1; i <= chunks + 1; i++) + { + var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture); + keys.Add(subkey + "="); + } + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + Func predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase)); + if (domainHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => predicate(value); + } + + var responseHeaders = context.Response.Headers; + var existingValues = responseHeaders[HeaderNames.SetCookie]; + if (!StringValues.IsNullOrEmpty(existingValues)) + { + responseHeaders[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray(); + } + + AppendResponseCookie( + context, + key, + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + SameSite = options.SameSite, + IsEssential = options.IsEssential, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + for (int i = 1; i <= chunks; i++) + { + AppendResponseCookie( + context, + key + "C" + i.ToString(CultureInfo.InvariantCulture), + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + SameSite = options.SameSite, + IsEssential = options.IsEssential, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } + } +} diff --git a/src/Security/src/Directory.Build.props b/src/Security/src/Directory.Build.props new file mode 100644 index 0000000000..1e0980f663 --- /dev/null +++ b/src/Security/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs new file mode 100644 index 0000000000..3aabf94c15 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + internal static class Constants + { + internal static class Headers + { + internal const string SetCookie = "Set-Cookie"; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs new file mode 100644 index 0000000000..bdfd43c796 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add cookie authentication capabilities to an HTTP application pipeline. + /// + public static class CookieAppBuilderExtensions + { + /// + /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app, CookieAuthenticationOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs new file mode 100644 index 0000000000..700b607976 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Default values related to cookie-based authentication handler + /// + public static class CookieAuthenticationDefaults + { + /// + /// The default value used for CookieAuthenticationOptions.AuthenticationScheme + /// + public const string AuthenticationScheme = "Cookies"; + + /// + /// The prefix used to provide a default CookieAuthenticationOptions.CookieName + /// + public static readonly string CookiePrefix = ".AspNetCore."; + + /// + /// The default value used by CookieAuthenticationMiddleware for the + /// CookieAuthenticationOptions.LoginPath + /// + public static readonly PathString LoginPath = new PathString("/Account/Login"); + + /// + /// The default value used by CookieAuthenticationMiddleware for the + /// CookieAuthenticationOptions.LogoutPath + /// + public static readonly PathString LogoutPath = new PathString("/Account/Logout"); + + /// + /// The default value used by CookieAuthenticationMiddleware for the + /// CookieAuthenticationOptions.AccessDeniedPath + /// + public static readonly PathString AccessDeniedPath = new PathString("/Account/AccessDenied"); + + /// + /// The default value of the CookieAuthenticationOptions.ReturnUrlParameter + /// + public static readonly string ReturnUrlParameter = "ReturnUrl"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs new file mode 100644 index 0000000000..b77a51ef4f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -0,0 +1,451 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + public class CookieAuthenticationHandler : SignInAuthenticationHandler + { + private const string HeaderValueNoCache = "no-cache"; + private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT"; + private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId"; + + private bool _shouldRefresh; + private bool _signInCalled; + private bool _signOutCalled; + + private DateTimeOffset? _refreshIssuedUtc; + private DateTimeOffset? _refreshExpiresUtc; + private string _sessionKey; + private Task _readCookieTask; + private AuthenticationTicket _refreshTicket; + + public CookieAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new CookieAuthenticationEvents Events + { + get { return (CookieAuthenticationEvents)base.Events; } + set { base.Events = value; } + } + + protected override Task InitializeHandlerAsync() + { + // Cookies needs to finish the response + Context.Response.OnStarting(FinishResponseAsync); + return Task.CompletedTask; + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new CookieAuthenticationEvents()); + + private Task EnsureCookieTicket() + { + // We only need to read the ticket once + if (_readCookieTask == null) + { + _readCookieTask = ReadCookieTicket(); + } + return _readCookieTask; + } + + private void CheckForRefresh(AuthenticationTicket ticket) + { + var currentUtc = Clock.UtcNow; + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + var allowRefresh = ticket.Properties.AllowRefresh ?? true; + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + RequestRefresh(ticket); + } + } + } + + private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null) + { + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (issuedUtc != null && expiresUtc != null) + { + _shouldRefresh = true; + var currentUtc = Clock.UtcNow; + _refreshIssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _refreshExpiresUtc = currentUtc.Add(timeSpan); + _refreshTicket = CloneTicket(ticket, replacedPrincipal); + } + } + + private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal) + { + var principal = replacedPrincipal ?? ticket.Principal; + var newPrincipal = new ClaimsPrincipal(); + foreach (var identity in principal.Identities) + { + newPrincipal.AddIdentity(identity.Clone()); + } + + var newProperties = new AuthenticationProperties(); + foreach (var item in ticket.Properties.Items) + { + newProperties.Items[item.Key] = item.Value; + } + + return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme); + } + + private async Task ReadCookieTicket() + { + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name); + if (string.IsNullOrEmpty(cookie)) + { + return AuthenticateResult.NoResult(); + } + + var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding()); + if (ticket == null) + { + return AuthenticateResult.Fail("Unprotect ticket failed"); + } + + if (Options.SessionStore != null) + { + var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); + if (claim == null) + { + return AuthenticateResult.Fail("SessionId missing"); + } + _sessionKey = claim.Value; + ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); + if (ticket == null) + { + return AuthenticateResult.Fail("Identity missing in session store"); + } + } + + var currentUtc = Clock.UtcNow; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc != null && expiresUtc.Value < currentUtc) + { + if (Options.SessionStore != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + return AuthenticateResult.Fail("Ticket expired"); + } + + CheckForRefresh(ticket); + + // Finally we have a valid ticket + return AuthenticateResult.Success(ticket); + } + + protected override async Task HandleAuthenticateAsync() + { + var result = await EnsureCookieTicket(); + if (!result.Succeeded) + { + return result; + } + + var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket); + await Events.ValidatePrincipal(context); + + if (context.Principal == null) + { + return AuthenticateResult.Fail("No principal."); + } + + if (context.ShouldRenew) + { + RequestRefresh(result.Ticket, context.Principal); + } + + return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name)); + } + + private CookieOptions BuildCookieOptions() + { + var cookieOptions = Options.Cookie.Build(Context); + // ignore the 'Expires' value as this will be computed elsewhere + cookieOptions.Expires = null; + + return cookieOptions; + } + + protected virtual async Task FinishResponseAsync() + { + // Only renew if requested, and neither sign in or sign out was called + if (!_shouldRefresh || _signInCalled || _signOutCalled) + { + return; + } + + var ticket = _refreshTicket; + if (ticket != null) + { + var properties = ticket.Properties; + + if (_refreshIssuedUtc.HasValue) + { + properties.IssuedUtc = _refreshIssuedUtc; + } + + if (_refreshExpiresUtc.HasValue) + { + properties.ExpiresUtc = _refreshExpiresUtc; + } + + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RenewAsync(_sessionKey, ticket); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Scheme.Name)); + ticket = new AuthenticationTicket(principal, null, Scheme.Name); + } + + var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); + + var cookieOptions = BuildCookieOptions(); + if (properties.IsPersistent && _refreshExpiresUtc.HasValue) + { + cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime(); + } + + Options.CookieManager.AppendResponseCookie( + Context, + Options.Cookie.Name, + cookieValue, + cookieOptions); + + await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties); + } + } + + protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + properties = properties ?? new AuthenticationProperties(); + + _signInCalled = true; + + // Process the request cookie to initialize members like _sessionKey. + await EnsureCookieTicket(); + var cookieOptions = BuildCookieOptions(); + + var signInContext = new CookieSigningInContext( + Context, + Scheme, + Options, + user, + properties, + cookieOptions); + + DateTimeOffset issuedUtc; + if (signInContext.Properties.IssuedUtc.HasValue) + { + issuedUtc = signInContext.Properties.IssuedUtc.Value; + } + else + { + issuedUtc = Clock.UtcNow; + signInContext.Properties.IssuedUtc = issuedUtc; + } + + if (!signInContext.Properties.ExpiresUtc.HasValue) + { + signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); + } + + await Events.SigningIn(signInContext); + + if (signInContext.Properties.IsPersistent) + { + var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); + signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime(); + } + + var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name); + + if (Options.SessionStore != null) + { + if (_sessionKey != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + _sessionKey = await Options.SessionStore.StoreAsync(ticket); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.ClaimsIssuer)); + ticket = new AuthenticationTicket(principal, null, Scheme.Name); + } + + var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); + + Options.CookieManager.AppendResponseCookie( + Context, + Options.Cookie.Name, + cookieValue, + signInContext.CookieOptions); + + var signedInContext = new CookieSignedInContext( + Context, + Scheme, + signInContext.Principal, + signInContext.Properties, + Options); + + await Events.SignedIn(signedInContext); + + // Only redirect on the login path + var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; + await ApplyHeaders(shouldRedirect, signedInContext.Properties); + + Logger.SignedIn(Scheme.Name); + } + + protected async override Task HandleSignOutAsync(AuthenticationProperties properties) + { + properties = properties ?? new AuthenticationProperties(); + + _signOutCalled = true; + + // Process the request cookie to initialize members like _sessionKey. + await EnsureCookieTicket(); + var cookieOptions = BuildCookieOptions(); + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + + var context = new CookieSigningOutContext( + Context, + Scheme, + Options, + properties, + cookieOptions); + + await Events.SigningOut(context); + + Options.CookieManager.DeleteCookie( + Context, + Options.Cookie.Name, + context.CookieOptions); + + // Only redirect on the logout path + var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath; + await ApplyHeaders(shouldRedirect, context.Properties); + + Logger.SignedOut(Scheme.Name); + } + + private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties) + { + Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache; + Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; + + if (shouldRedirectToReturnUrl && Response.StatusCode == 200) + { + // set redirect uri in order: + // 1. properties.RedirectUri + // 2. query parameter ReturnUrlParameter + // + // Absolute uri is not allowed if it is from query string as query string is not + // a trusted source. + var redirectUri = properties.RedirectUri; + if (string.IsNullOrEmpty(redirectUri)) + { + redirectUri = Request.Query[Options.ReturnUrlParameter]; + if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri)) + { + redirectUri = null; + } + } + + if (redirectUri != null) + { + await Events.RedirectToReturnUrl( + new RedirectContext(Context, Scheme, Options, properties, redirectUri)); + } + } + } + + private static bool IsHostRelative(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + if (path.Length == 1) + { + return path[0] == '/'; + } + return path[0] == '/' && path[1] != '/' && path[1] != '\\'; + } + + protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) + { + var returnUrl = properties.RedirectUri; + if (string.IsNullOrEmpty(returnUrl)) + { + returnUrl = OriginalPathBase + Request.Path + Request.QueryString; + } + var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl); + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri)); + await Events.RedirectToAccessDenied(redirectContext); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + var redirectUri = properties.RedirectUri; + if (string.IsNullOrEmpty(redirectUri)) + { + redirectUri = OriginalPathBase + Request.Path + Request.QueryString; + } + + var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, BuildRedirectUri(loginUri)); + await Events.RedirectToLogin(redirectContext); + } + + private string GetTlsTokenBinding() + { + var binding = Context.Features.Get()?.GetProvidedTokenBindingId(); + return binding == null ? null : Convert.ToBase64String(binding); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs new file mode 100644 index 0000000000..35017f9c4d --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -0,0 +1,214 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.Internal; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Configuration options for . + /// + public class CookieAuthenticationOptions : AuthenticationSchemeOptions + { + private CookieBuilder _cookieBuilder = new RequestPathBaseCookieBuilder + { + // the default name is configured in PostConfigureCookieAuthenticationOptions + + // To support OAuth authentication, a lax mode is required, see https://github.com/aspnet/Security/issues/1231. + SameSite = SameSiteMode.Lax, + HttpOnly = true, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; + + /// + /// Create an instance of the options initialized with the default values + /// + public CookieAuthenticationOptions() + { + ExpireTimeSpan = TimeSpan.FromDays(14); + ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; + SlidingExpiration = true; + Events = new CookieAuthenticationEvents(); + } + + /// + /// + /// Determines the settings used to create the cookie. + /// + /// + /// defaults to . + /// defaults to true. + /// defaults to . + /// + /// + /// + /// + /// The default value for cookie name is ".AspNetCore.Cookies". + /// This value should be changed if you change the name of the AuthenticationScheme, especially if your + /// system uses the cookie authentication handler multiple times. + /// + /// + /// determines if the browser should allow the cookie to be attached to same-site or cross-site requests. + /// The default is Lax, which means the cookie is only allowed to be attached to cross-site requests using safe HTTP methods and same-site requests. + /// + /// + /// determines if the browser should allow the cookie to be accessed by client-side javascript. + /// The default is true, which means the cookie will only be passed to http requests and is not made available to script on the page. + /// + /// + /// is currently ignored. Use to control lifetime of cookie authentication. + /// + /// + public CookieBuilder Cookie + { + get => _cookieBuilder; + set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// If set this will be used by the CookieAuthenticationHandler for data protection. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } + + /// + /// The SlidingExpiration is set to true to instruct the handler to re-issue a new cookie with a new + /// expiration time any time it processes a request which is more than halfway through the expiration window. + /// + public bool SlidingExpiration { get; set; } + + /// + /// The LoginPath property is used by the handler for the redirection target when handling ChallengeAsync. + /// The current url which is added to the LoginPath as a query string parameter named by the ReturnUrlParameter. + /// Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect + /// the browser back to the original url. + /// + public PathString LoginPath { get; set; } + + /// + /// If the LogoutPath is provided the handler then a request to that path will redirect based on the ReturnUrlParameter. + /// + public PathString LogoutPath { get; set; } + + /// + /// The AccessDeniedPath property is used by the handler for the redirection target when handling ForbidAsync. + /// + public PathString AccessDeniedPath { get; set; } + + /// + /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the handler + /// when during a Challenge. This is also the query string parameter looked for when a request arrives on the + /// login path or logout path, in order to return to the original url after the action is performed. + /// + public string ReturnUrlParameter { get; set; } + + /// + /// The Provider may be assigned to an instance of an object created by the application at startup time. The handler + /// calls methods on the provider which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + public new CookieAuthenticationEvents Events + { + get => (CookieAuthenticationEvents)base.Events; + set => base.Events = value; + } + + /// + /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the + /// cookie value. If not provided one will be created using . + /// + public ISecureDataFormat TicketDataFormat { get; set; } + + /// + /// The component used to get cookies from the request or set them on the response. + /// + /// ChunkingCookieManager will be used by default. + /// + public ICookieManager CookieManager { get; set; } + + /// + /// An optional container in which to store the identity across requests. When used, only a session identifier is sent + /// to the client. This can be used to mitigate potential problems with very large identities. + /// + public ITicketStore SessionStore { get; set; } + + /// + /// + /// Controls how much time the authentication ticket stored in the cookie will remain valid from the point it is created + /// The expiration information is stored in the protected cookie ticket. Because of that an expired cookie will be ignored + /// even if it is passed to the server after the browser should have purged it. + /// + /// + /// This is separate from the value of , which specifies + /// how long the browser will keep the cookie. + /// + /// + public TimeSpan ExpireTimeSpan { get; set; } + + #region Obsolete API + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Determines the cookie name used to persist the identity. The default value is ".AspNetCore.Cookies". + /// This value should be changed if you change the name of the AuthenticationScheme, especially if your + /// system uses the cookie authentication handler multiple times. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Name) + ".")] + public string CookieName { get => Cookie.Name; set => Cookie.Name = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Determines the domain used to create the cookie. Is not provided by default. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Domain) + ".")] + public string CookieDomain { get => Cookie.Domain; set => Cookie.Domain = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Determines the path used to create the cookie. The default value is "/" for highest browser compatibility. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")] + public string CookiePath { get => Cookie.Path; set => Cookie.Path = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Determines if the browser should allow the cookie to be accessed by client-side javascript. The + /// default is true, which means the cookie will only be passed to http requests and is not made available + /// to script on the page. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.HttpOnly) + ".")] + public bool CookieHttpOnly { get => Cookie.HttpOnly; set => Cookie.HttpOnly = value; } + + /// + /// + /// This property is obsolete and will be removed in a future version. The recommended alternative is on . + /// + /// + /// Determines if the cookie should only be transmitted on HTTPS request. The default is to limit the cookie + /// to HTTPS requests if the page which is doing the SignIn is also HTTPS. If you have an HTTPS sign in page + /// and portions of your site are HTTP you may need to change this value. + /// + /// + [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")] + public CookieSecurePolicy CookieSecure { get => Cookie.SecurePolicy; set => Cookie.SecurePolicy = value; } + #endregion + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs new file mode 100644 index 0000000000..4c41f54a9c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CookieExtensions + { + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder) + => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddCookie(authenticationScheme, configureOptions: null); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureCookieAuthenticationOptions>()); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs new file mode 100644 index 0000000000..2b8b0416b3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs @@ -0,0 +1,158 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// This default implementation of the ICookieAuthenticationEvents may be used if the + /// application only needs to override a few of the interface methods. This may be used as a base class + /// or may be instantiated directly. + /// + public class CookieAuthenticationEvents + { + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func OnValidatePrincipal { get; set; } = context => Task.CompletedTask; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func OnSigningIn { get; set; } = context => Task.CompletedTask; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func OnSignedIn { get; set; } = context => Task.CompletedTask; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func OnSigningOut { get; set; } = context => Task.CompletedTask; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func, Task> OnRedirectToLogin { get; set; } = context => + { + if (IsAjaxRequest(context.Request)) + { + context.Response.Headers["Location"] = context.RedirectUri; + context.Response.StatusCode = 401; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func, Task> OnRedirectToAccessDenied { get; set; } = context => + { + if (IsAjaxRequest(context.Request)) + { + context.Response.Headers["Location"] = context.RedirectUri; + context.Response.StatusCode = 403; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func, Task> OnRedirectToLogout { get; set; } = context => + { + if (IsAjaxRequest(context.Request)) + { + context.Response.Headers["Location"] = context.RedirectUri; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; + + /// + /// A delegate assigned to this property will be invoked when the related method is called. + /// + public Func, Task> OnRedirectToReturnUrl { get; set; } = context => + { + if (IsAjaxRequest(context.Request)) + { + context.Response.Headers["Location"] = context.RedirectUri; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; + + private static bool IsAjaxRequest(HttpRequest request) + { + return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) || + string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal); + } + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// + /// + public virtual Task ValidatePrincipal(CookieValidatePrincipalContext context) => OnValidatePrincipal(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// + public virtual Task SigningIn(CookieSigningInContext context) => OnSigningIn(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// + public virtual Task SignedIn(CookieSignedInContext context) => OnSignedIn(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// + public virtual Task SigningOut(CookieSigningOutContext context) => OnSigningOut(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// Contains information about the event + public virtual Task RedirectToLogout(RedirectContext context) => OnRedirectToLogout(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// Contains information about the event + public virtual Task RedirectToLogin(RedirectContext context) => OnRedirectToLogin(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// Contains information about the event + public virtual Task RedirectToReturnUrl(RedirectContext context) => OnRedirectToReturnUrl(context); + + /// + /// Implements the interface method by invoking the related delegate method. + /// + /// Contains information about the event + public virtual Task RedirectToAccessDenied(RedirectContext context) => OnRedirectToAccessDenied(context); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs new file mode 100644 index 0000000000..98c31dd190 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Context object passed to the ICookieAuthenticationEvents method SignedIn. + /// + public class CookieSignedInContext : PrincipalContext + { + /// + /// Creates a new instance of the context object. + /// + /// The HTTP request context + /// The scheme data + /// Initializes Principal property + /// Initializes Properties property + /// The handler options + public CookieSignedInContext( + HttpContext context, + AuthenticationScheme scheme, + ClaimsPrincipal principal, + AuthenticationProperties properties, + CookieAuthenticationOptions options) + : base(context, scheme, options, properties) + { + Principal = principal; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs new file mode 100644 index 0000000000..41d7b4f6ae --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Context object passed to the . + /// + public class CookieSigningInContext : PrincipalContext + { + /// + /// Creates a new instance of the context object. + /// + /// The HTTP request context + /// The scheme data + /// The handler options + /// Initializes Principal property + /// The authentication properties. + /// Initializes options for the authentication cookie. + public CookieSigningInContext( + HttpContext context, + AuthenticationScheme scheme, + CookieAuthenticationOptions options, + ClaimsPrincipal principal, + AuthenticationProperties properties, + CookieOptions cookieOptions) + : base(context, scheme, options, properties) + { + CookieOptions = cookieOptions; + Principal = principal; + } + + /// + /// The options for creating the outgoing cookie. + /// May be replace or altered during the SigningIn call. + /// + public CookieOptions CookieOptions { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs new file mode 100644 index 0000000000..34f6e49ab6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Context object passed to the + /// + public class CookieSigningOutContext : PropertiesContext + { + /// + /// + /// + /// + /// + /// + /// + /// + public CookieSigningOutContext( + HttpContext context, + AuthenticationScheme scheme, + CookieAuthenticationOptions options, + AuthenticationProperties properties, + CookieOptions cookieOptions) + : base(context, scheme, options, properties) + => CookieOptions = cookieOptions; + + /// + /// The options for creating the outgoing cookie. + /// May be replace or altered during the SigningOut call. + /// + public CookieOptions CookieOptions { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs new file mode 100644 index 0000000000..d2161e42a1 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Context object passed to the CookieAuthenticationEvents ValidatePrincipal method. + /// + public class CookieValidatePrincipalContext : PrincipalContext + { + /// + /// Creates a new instance of the context object. + /// + /// + /// + /// Contains the initial values for identity and extra data + /// + public CookieValidatePrincipalContext(HttpContext context, AuthenticationScheme scheme, CookieAuthenticationOptions options, AuthenticationTicket ticket) + : base(context, scheme, options, ticket?.Properties) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + + Principal = ticket.Principal; + } + + /// + /// If true, the cookie will be renewed + /// + public bool ShouldRenew { get; set; } + + /// + /// Called to replace the claims principal. The supplied principal will replace the value of the + /// Principal property, which determines the identity of the authenticated request. + /// + /// The used as the replacement + public void ReplacePrincipal(ClaimsPrincipal principal) => Principal = principal; + + /// + /// Called to reject the incoming principal. This may be done if the application has determined the + /// account is no longer active, and the request should be treated as if it was anonymous. + /// + public void RejectPrincipal() => Principal = null; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs new file mode 100644 index 0000000000..4514fefa97 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// This is used by the CookieAuthenticationMiddleware to process request and response cookies. + /// It is abstracted from the normal cookie APIs to allow for complex operations like chunking. + /// + public interface ICookieManager + { + /// + /// Retrieve a cookie of the given name from the request. + /// + /// + /// + /// + string GetRequestCookie(HttpContext context, string key); + + /// + /// Append the given cookie to the response. + /// + /// + /// + /// + /// + void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options); + + /// + /// Append a delete cookie to the response. + /// + /// + /// + /// + void DeleteCookie(HttpContext context, string key, CookieOptions options); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs new file mode 100644 index 0000000000..cff11a8929 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// This provides an abstract storage mechanic to preserve identity information on the server + /// while only sending a simple identifier key to the client. This is most commonly used to mitigate + /// issues with serializing large identities into cookies. + /// + public interface ITicketStore + { + /// + /// Store the identity ticket and return the associated key. + /// + /// The identity information to store. + /// The key that can be used to retrieve the identity later. + Task StoreAsync(AuthenticationTicket ticket); + + /// + /// Tells the store that the given identity should be updated. + /// + /// + /// + /// + Task RenewAsync(string key, AuthenticationTicket ticket); + + /// + /// Retrieves an identity from the store for the given key. + /// + /// The key associated with the identity. + /// The identity associated with the given key, or if not found. + Task RetrieveAsync(string key); + + /// + /// Remove the identity associated with the given key. + /// + /// The key associated with the identity. + /// + Task RemoveAsync(string key); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs new file mode 100644 index 0000000000..d12735443f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _authSchemeSignedIn; + private static Action _authSchemeSignedOut; + + static LoggingExtensions() + { + _authSchemeSignedIn = LoggerMessage.Define( + eventId: 10, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed in."); + _authSchemeSignedOut = LoggerMessage.Define( + eventId: 11, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed out."); + } + + public static void SignedIn(this ILogger logger, string authenticationScheme) + { + _authSchemeSignedIn(logger, authenticationScheme, null); + } + + public static void SignedOut(this ILogger logger, string authenticationScheme) + { + _authSchemeSignedOut(logger, authenticationScheme, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj new file mode 100644 index 0000000000..b188a58e08 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core middleware that enables an application to use cookie based authentication. + netstandard2.0 + $(DefineConstants);SECURITY + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs new file mode 100644 index 0000000000..48895072e9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Used to setup defaults for all . + /// + public class PostConfigureCookieAuthenticationOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public PostConfigureCookieAuthenticationOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, CookieAuthenticationOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.Cookie.Name)) + { + options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + name; + } + if (options.TicketDataFormat == null) + { + // Note: the purpose for the data protector must remain fixed for interop to work. + var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", name, "v2"); + options.TicketDataFormat = new TicketDataFormat(dataProtector); + } + if (options.CookieManager == null) + { + options.CookieManager = new ChunkingCookieManager(); + } + if (!options.LoginPath.HasValue) + { + options.LoginPath = CookieAuthenticationDefaults.LoginPath; + } + if (!options.LogoutPath.HasValue) + { + options.LogoutPath = CookieAuthenticationDefaults.LogoutPath; + } + if (!options.AccessDeniedPath.HasValue) + { + options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json new file mode 100644 index 0000000000..b218669b76 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json @@ -0,0 +1,1621 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Cookies, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.CookieExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.CookieAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseCookieAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCookieAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "CookiePrefix", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LoginPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LogoutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessDeniedPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ReturnUrlParameter", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cookies\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHandlerAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FinishResponseAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DataProtectionProvider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DataProtectionProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SlidingExpiration", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SlidingExpiration", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LoginPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LoginPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LogoutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LogoutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessDeniedPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessDeniedPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReturnUrlParameter", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReturnUrlParameter", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TicketDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TicketDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SessionStore", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SessionStore", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpireTimeSpan", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpireTimeSpan", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieDomain", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieDomain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookiePath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookiePath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieHttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieHttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieSecure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieSecure", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnValidatePrincipal", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnValidatePrincipal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSigningIn", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSigningIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSignedIn", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSignedIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSigningOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSigningOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToLogin", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToLogin", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAccessDenied", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAccessDenied", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToLogout", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToLogout", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToReturnUrl", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToReturnUrl", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidatePrincipal", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SigningIn", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignedIn", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SigningOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToLogout", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToLogin", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToReturnUrl", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAccessDenied", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieOptions", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "cookieOptions", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieOptions", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "cookieOptions", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ShouldRenew", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ShouldRenew", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReplacePrincipal", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RejectPrincipal", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StoreAsync", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RenewAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RetrieveAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.PostConfigureCookieAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ChunkSize", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ChunkSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ThrowForPartialCookies", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ThrowForPartialCookies", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultChunkSize", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "4050" + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs new file mode 100644 index 0000000000..a94dc7bc45 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.Facebook; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add Facebook authentication capabilities to an HTTP application pipeline. + /// + public static class FacebookAppBuilderExtensions + { + /// + /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app, FacebookOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs new file mode 100644 index 0000000000..92d1d003e6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + public static class FacebookDefaults + { + public const string AuthenticationScheme = "Facebook"; + + public static readonly string DisplayName = "Facebook"; + + public static readonly string AuthorizationEndpoint = "https://www.facebook.com/v2.12/dialog/oauth"; + + public static readonly string TokenEndpoint = "https://graph.facebook.com/v2.12/oauth/access_token"; + + public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v2.12/me"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs new file mode 100644 index 0000000000..2273724a42 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Facebook; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class FacebookAuthenticationOptionsExtensions + { + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder) + => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs new file mode 100644 index 0000000000..eb42511431 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + public class FacebookHandler : OAuthHandler + { + public FacebookHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + { + var endpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, "access_token", tokens.AccessToken); + if (Options.SendAppSecretProof) + { + endpoint = QueryHelpers.AddQueryString(endpoint, "appsecret_proof", GenerateAppSecretProof(tokens.AccessToken)); + } + if (Options.Fields.Count > 0) + { + endpoint = QueryHelpers.AddQueryString(endpoint, "fields", string.Join(",", Options.Fields)); + } + + var response = await Backchannel.GetAsync(endpoint, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"An error occurred when retrieving Facebook user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Facebook Graph API is enabled."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + + } + + private string GenerateAppSecretProof(string accessToken) + { + using (var algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret))) + { + var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); + var builder = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } + } + + protected override string FormatScope(IEnumerable scopes) + { + // Facebook deviates from the OAuth spec here. They require comma separated instead of space separated. + // https://developers.facebook.com/docs/reference/dialogs/oauth + // http://tools.ietf.org/html/rfc6749#section-3.3 + return string.Join(",", scopes); + } + + protected override string FormatScope() + => base.FormatScope(); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs new file mode 100644 index 0000000000..7010bb20aa --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using System.Globalization; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + /// + /// Configuration options for . + /// + public class FacebookOptions : OAuthOptions + { + /// + /// Initializes a new . + /// + public FacebookOptions() + { + CallbackPath = new PathString("/signin-facebook"); + SendAppSecretProof = true; + AuthorizationEndpoint = FacebookDefaults.AuthorizationEndpoint; + TokenEndpoint = FacebookDefaults.TokenEndpoint; + UserInformationEndpoint = FacebookDefaults.UserInformationEndpoint; + Scope.Add("public_profile"); + Scope.Add("email"); + Fields.Add("name"); + Fields.Add("email"); + Fields.Add("first_name"); + Fields.Add("last_name"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max"); + ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name"); + ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name"); + ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender"); + ClaimActions.MapJsonKey("urn:facebook:link", "link"); + ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name"); + ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale"); + ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone"); + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + if (string.IsNullOrEmpty(AppId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppId)), nameof(AppId)); + } + + if (string.IsNullOrEmpty(AppSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppSecret)), nameof(AppSecret)); + } + + base.Validate(); + } + + // Facebook uses a non-standard term for this field. + /// + /// Gets or sets the Facebook-assigned appId. + /// + public string AppId + { + get { return ClientId; } + set { ClientId = value; } + } + + // Facebook uses a non-standard term for this field. + /// + /// Gets or sets the Facebook-assigned app secret. + /// + public string AppSecret + { + get { return ClientSecret; } + set { ClientSecret = value; } + } + + /// + /// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls. + /// This is enabled by default. + /// + public bool SendAppSecretProof { get; set; } + + /// + /// The list of fields to retrieve from the UserInformationEndpoint. + /// https://developers.facebook.com/docs/graph-api/reference/user + /// + public ICollection Fields { get; } = new HashSet(); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj new file mode 100644 index 0000000000..62aee1367f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..655da24a30 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs @@ -0,0 +1,44 @@ +// +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.Facebook.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx new file mode 100644 index 0000000000..56ef7f56bd --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json new file mode 100644 index 0000000000..5d95efca6f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json @@ -0,0 +1,390 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Facebook, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.FacebookAuthenticationOptionsExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Facebook\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AppId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AppId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AppSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AppSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SendAppSecretProof", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SendAppSecretProof", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Fields", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.FacebookAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseFacebookAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseFacebookAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs new file mode 100644 index 0000000000..4302d20db1 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.Google; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add Google authentication capabilities to an HTTP application pipeline. + /// + public static class GoogleAppBuilderExtensions + { + /// + /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app, GoogleOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs new file mode 100644 index 0000000000..714df45655 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + public class GoogleChallengeProperties : OAuthChallengeProperties + { + /// + /// The parameter key for the "access_type" argument being used for a challenge request. + /// + public static readonly string AccessTypeKey = "access_type"; + + /// + /// The parameter key for the "approval_prompt" argument being used for a challenge request. + /// + public static readonly string ApprovalPromptKey = "approval_prompt"; + + /// + /// The parameter key for the "include_granted_scopes" argument being used for a challenge request. + /// + public static readonly string IncludeGrantedScopesKey = "include_granted_scopes"; + + /// + /// The parameter key for the "login_hint" argument being used for a challenge request. + /// + public static readonly string LoginHintKey = "login_hint"; + + /// + /// The parameter key for the "prompt" argument being used for a challenge request. + /// + public static readonly string PromptParameterKey = "prompt"; + + public GoogleChallengeProperties() + { } + + public GoogleChallengeProperties(IDictionary items) + : base(items) + { } + + public GoogleChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "access_type" parameter value being used for a challenge request. + /// + public string AccessType + { + get => GetParameter(AccessTypeKey); + set => SetParameter(AccessTypeKey, value); + } + + /// + /// The "approval_prompt" parameter value being used for a challenge request. + /// + public string ApprovalPrompt + { + get => GetParameter(ApprovalPromptKey); + set => SetParameter(ApprovalPromptKey, value); + } + + /// + /// The "include_granted_scopes" parameter value being used for a challenge request. + /// + public bool? IncludeGrantedScopes + { + get => GetParameter(IncludeGrantedScopesKey); + set => SetParameter(IncludeGrantedScopesKey, value); + } + + /// + /// The "login_hint" parameter value being used for a challenge request. + /// + public string LoginHint + { + get => GetParameter(LoginHintKey); + set => SetParameter(LoginHintKey, value); + } + + /// + /// The "prompt" parameter value being used for a challenge request. + /// + public string Prompt + { + get => GetParameter(PromptParameterKey); + set => SetParameter(PromptParameterKey, value); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs new file mode 100644 index 0000000000..0428703180 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Google +{ + /// + /// Default values for Google authentication + /// + public static class GoogleDefaults + { + public const string AuthenticationScheme = "Google"; + + public static readonly string DisplayName = "Google"; + + public static readonly string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + + public static readonly string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; + + public static readonly string UserInformationEndpoint = "https://www.googleapis.com/plus/v1/people/me"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs new file mode 100644 index 0000000000..95547014ca --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Google; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class GoogleExtensions + { + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder) + => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs new file mode 100644 index 0000000000..88d48d4467 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + public class GoogleHandler : OAuthHandler + { + public GoogleHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override async Task CreateTicketAsync( + ClaimsIdentity identity, + AuthenticationProperties properties, + OAuthTokenResponse tokens) + { + // Get the Google user + var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"An error occurred when retrieving Google user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Google+ API is enabled."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + } + + // TODO: Abstract this properties override pattern into the base class? + protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) + { + // Google Identity Platform Manual: + // https://developers.google.com/identity/protocols/OAuth2WebServer + + var queryStrings = new Dictionary(StringComparer.OrdinalIgnoreCase); + queryStrings.Add("response_type", "code"); + queryStrings.Add("client_id", Options.ClientId); + queryStrings.Add("redirect_uri", redirectUri); + + AddQueryString(queryStrings, properties, GoogleChallengeProperties.ScopeKey, FormatScope, Options.Scope); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.AccessTypeKey, Options.AccessType); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.ApprovalPromptKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.PromptParameterKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.LoginHintKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.IncludeGrantedScopesKey, v => v?.ToString().ToLower(), (bool?)null); + + var state = Options.StateDataFormat.Protect(properties); + queryStrings.Add("state", state); + + var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings); + return authorizationEndpoint; + } + + private void AddQueryString( + IDictionary queryStrings, + AuthenticationProperties properties, + string name, + Func formatter, + T defaultValue) + { + string value = null; + var parameterValue = properties.GetParameter(name); + if (parameterValue != null) + { + value = formatter(parameterValue); + } + else if (!properties.Items.TryGetValue(name, out value)) + { + value = formatter(defaultValue); + } + + // Remove the parameter from AuthenticationProperties so it won't be serialized into the state + properties.Items.Remove(name); + + if (value != null) + { + queryStrings[name] = value; + } + } + + private void AddQueryString( + IDictionary queryStrings, + AuthenticationProperties properties, + string name, + string defaultValue = null) + => AddQueryString(queryStrings, properties, name, x => x, defaultValue); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs new file mode 100644 index 0000000000..2cac949a03 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + /// + /// Contains static methods that allow to extract user's information from a + /// instance retrieved from Google after a successful authentication process. + /// + public static class GoogleHelper + { + /// + /// Gets the user's email. + /// + public static string GetEmail(JObject user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + return TryGetFirstValue(user, "emails", "value"); + } + + // Get the given subProperty from a list property. + private static string TryGetFirstValue(JObject user, string propertyName, string subProperty) + { + JToken value; + if (user.TryGetValue(propertyName, out value)) + { + var array = JArray.Parse(value.ToString()); + if (array != null && array.Count > 0) + { + var subObject = JObject.Parse(array.First.ToString()); + if (subObject != null) + { + if (subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + } + } + return null; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs new file mode 100644 index 0000000000..34028bc52b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + /// + /// Configuration options for . + /// + public class GoogleOptions : OAuthOptions + { + /// + /// Initializes a new . + /// + public GoogleOptions() + { + CallbackPath = new PathString("/signin-google"); + AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint; + TokenEndpoint = GoogleDefaults.TokenEndpoint; + UserInformationEndpoint = GoogleDefaults.UserInformationEndpoint; + Scope.Add("openid"); + Scope.Add("profile"); + Scope.Add("email"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonSubKey(ClaimTypes.GivenName, "name", "givenName"); + ClaimActions.MapJsonSubKey(ClaimTypes.Surname, "name", "familyName"); + ClaimActions.MapJsonKey("urn:google:profile", "url"); + ClaimActions.MapCustomJson(ClaimTypes.Email, GoogleHelper.GetEmail); + } + + /// + /// access_type. Set to 'offline' to request a refresh token. + /// + public string AccessType { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj new file mode 100644 index 0000000000..de8867f91a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core contains middleware to support Google's OpenId and OAuth 2.0 authentication workflows. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..03448b408c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// +namespace Microsoft.AspNetCore.Authentication.Google +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.Google.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json new file mode 100644 index 0000000000..0a623b3b85 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json @@ -0,0 +1,550 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Google, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.GoogleExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AccessType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApprovalPrompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApprovalPrompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IncludeGrantedScopes", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IncludeGrantedScopes", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LoginHint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LoginHint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessTypeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ApprovalPromptKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "IncludeGrantedScopesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LoginHintKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PromptParameterKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Google\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildChallengeUrl", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHelper", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetEmail", + "Parameters": [ + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AccessType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.GoogleAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseGoogleAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseGoogleAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs new file mode 100644 index 0000000000..1c2efd6c73 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class AuthenticationFailedContext : ResultContext + { + public AuthenticationFailedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } + + public Exception Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs new file mode 100644 index 0000000000..6500e1e3f7 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class JwtBearerChallengeContext : PropertiesContext + { + public JwtBearerChallengeContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// Any failures encountered during the authentication process. + /// + public Exception AuthenticateFailure { get; set; } + + /// + /// Gets or sets the "error" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string Error { get; set; } + + /// + /// Gets or sets the "error_description" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string ErrorDescription { get; set; } + + /// + /// Gets or sets the "error_uri" value returned to the caller as part of the + /// WWW-Authenticate header. This property is always null unless explicitly set. + /// + public string ErrorUri { get; set; } + + /// + /// If true, will skip any default logic for this challenge. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this challenge. + /// + public void HandleResponse() => Handled = true; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs new file mode 100644 index 0000000000..a9b35c310f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. + /// + public class JwtBearerEvents + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a protocol message is first received. + /// + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public Func OnTokenValidated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before a challenge is sent back to the caller. + /// + public Func OnChallenge { get; set; } = context => Task.CompletedTask; + + public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); + + public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); + + public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context); + + public virtual Task Challenge(JwtBearerChallengeContext context) => OnChallenge(context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs new file mode 100644 index 0000000000..1850ad0492 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class MessageReceivedContext : ResultContext + { + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } + + /// + /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. + /// + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs new file mode 100644 index 0000000000..39b677b96d --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class TokenValidatedContext : ResultContext + { + public TokenValidatedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } + + public SecurityToken SecurityToken { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs new file mode 100644 index 0000000000..0cfc97573c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add OpenIdConnect Bearer authentication capabilities to an HTTP application pipeline. + /// + public static class JwtBearerAppBuilderExtensions + { + /// + /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app, JwtBearerOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs new file mode 100644 index 0000000000..649edf94bb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + /// + /// Default values used by bearer authentication. + /// + public static class JwtBearerDefaults + { + /// + /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions + /// + public const string AuthenticationScheme = "Bearer"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs new file mode 100644 index 0000000000..334407c0da --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class JwtBearerExtensions + { + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder) + => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs new file mode 100644 index 0000000000..6d5c7f5f5e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs @@ -0,0 +1,323 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class JwtBearerHandler : AuthenticationHandler + { + private OpenIdConnectConfiguration _configuration; + + public JwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IDataProtectionProvider dataProtection, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new JwtBearerEvents Events + { + get { return (JwtBearerEvents)base.Events; } + set { base.Events = value; } + } + + protected override Task CreateEventsAsync() => Task.FromResult(new JwtBearerEvents()); + + /// + /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. + /// + /// + protected override async Task HandleAuthenticateAsync() + { + string token = null; + try + { + // Give application opportunity to find from a different location, adjust, or reject token + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); + + // event can set the token + await Events.MessageReceived(messageReceivedContext); + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + + // If application retrieved token from somewhere else, use that. + token = messageReceivedContext.Token; + + if (string.IsNullOrEmpty(token)) + { + string authorization = Request.Headers["Authorization"]; + + // If no authorization header found, nothing to process further + if (string.IsNullOrEmpty(authorization)) + { + return AuthenticateResult.NoResult(); + } + + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + token = authorization.Substring("Bearer ".Length).Trim(); + } + + // If no token found, no further work possible + if (string.IsNullOrEmpty(token)) + { + return AuthenticateResult.NoResult(); + } + } + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var validationParameters = Options.TokenValidationParameters.Clone(); + if (_configuration != null) + { + var issuers = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; + } + + List validationFailures = null; + SecurityToken validatedToken; + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(token)) + { + ClaimsPrincipal principal; + try + { + principal = validator.ValidateToken(token, validationParameters, out validatedToken); + } + catch (Exception ex) + { + Logger.TokenValidationFailed(ex); + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. + if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null + && ex is SecurityTokenSignatureKeyNotFoundException) + { + Options.ConfigurationManager.RequestRefresh(); + } + + if (validationFailures == null) + { + validationFailures = new List(1); + } + validationFailures.Add(ex); + continue; + } + + Logger.TokenValidationSucceeded(); + + var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) + { + Principal = principal, + SecurityToken = validatedToken + }; + + await Events.TokenValidated(tokenValidatedContext); + if (tokenValidatedContext.Result != null) + { + return tokenValidatedContext.Result; + } + + if (Options.SaveToken) + { + tokenValidatedContext.Properties.StoreTokens(new[] + { + new AuthenticationToken { Name = "access_token", Value = token } + }); + } + + tokenValidatedContext.Success(); + return tokenValidatedContext.Result; + } + } + + if (validationFailures != null) + { + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures) + }; + + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + return AuthenticateResult.Fail(authenticationFailedContext.Exception); + } + + return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]"); + } + catch (Exception ex) + { + Logger.ErrorProcessingMessage(ex); + + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + Exception = ex + }; + + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + throw; + } + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + var authResult = await HandleAuthenticateOnceSafeAsync(); + var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties) + { + AuthenticateFailure = authResult?.Failure + }; + + // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token). + if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) + { + eventContext.Error = "invalid_token"; + eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); + } + + await Events.Challenge(eventContext); + if (eventContext.Handled) + { + return; + } + + Response.StatusCode = 401; + + if (string.IsNullOrEmpty(eventContext.Error) && + string.IsNullOrEmpty(eventContext.ErrorDescription) && + string.IsNullOrEmpty(eventContext.ErrorUri)) + { + Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); + } + else + { + // https://tools.ietf.org/html/rfc6750#section-3.1 + // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" + var builder = new StringBuilder(Options.Challenge); + if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0) + { + // Only add a comma after the first param, if any + builder.Append(','); + } + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(" error=\""); + builder.Append(eventContext.Error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(eventContext.ErrorDescription); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(eventContext.ErrorUri)) + { + if (!string.IsNullOrEmpty(eventContext.Error) || + !string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(eventContext.ErrorUri); + builder.Append('\"'); + } + + Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + } + } + + private static string CreateErrorDescription(Exception authFailure) + { + IEnumerable exceptions; + if (authFailure is AggregateException) + { + var agEx = authFailure as AggregateException; + exceptions = agEx.InnerExceptions; + } + else + { + exceptions = new[] { authFailure }; + } + + var messages = new List(); + + foreach (var ex in exceptions) + { + // Order sensitive, some of these exceptions derive from others + // and we want to display the most specific message possible. + if (ex is SecurityTokenInvalidAudienceException) + { + messages.Add("The audience is invalid"); + } + else if (ex is SecurityTokenInvalidIssuerException) + { + messages.Add("The issuer is invalid"); + } + else if (ex is SecurityTokenNoExpirationException) + { + messages.Add("The token has no expiration"); + } + else if (ex is SecurityTokenInvalidLifetimeException) + { + messages.Add("The token lifetime is invalid"); + } + else if (ex is SecurityTokenNotYetValidException) + { + messages.Add("The token is not valid yet"); + } + else if (ex is SecurityTokenExpiredException) + { + messages.Add("The token is expired"); + } + else if (ex is SecurityTokenSignatureKeyNotFoundException) + { + messages.Add("The signature key was not found"); + } + else if (ex is SecurityTokenInvalidSignatureException) + { + messages.Add("The signature is invalid"); + } + } + + return string.Join("; ", messages); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs new file mode 100644 index 0000000000..0d0a88e247 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + /// + /// Options class provides information needed to control Bearer Authentication handler behavior + /// + public class JwtBearerOptions : AuthenticationSchemeOptions + { + /// + /// Gets or sets if HTTPS is required for the metadata address or authority. + /// The default is true. This should be disabled only in development environments. + /// + public bool RequireHttpsMetadata { get; set; } = true; + + /// + /// Gets or sets the discovery endpoint for obtaining metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Gets or sets the Authority to use when making OpenIdConnect calls. + /// + public string Authority { get; set; } + + /// + /// Gets or sets the audience for any received OpenIdConnect token. + /// + /// + /// The expected audience for any received OpenIdConnect token. + /// + public string Audience { get; set; } + + /// + /// Gets or sets the challenge to put in the "WWW-Authenticate" header. + /// + public string Challenge { get; set; } = JwtBearerDefaults.AuthenticationScheme; + + /// + /// The object provided by the application to process events raised by the bearer authentication handler. + /// The application may implement the interface fully, or it may create an instance of JwtBearerEvents + /// and assign delegates only to the events it wants to process. + /// + public new JwtBearerEvents Events + { + get { return (JwtBearerEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// The HttpMessageHandler used to retrieve metadata. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// is a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets the timeout when using the backchannel to make an http call. + /// + public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties + /// will not be used. This information should not be updated during request processing. + /// + public OpenIdConnectConfiguration Configuration { get; set; } + + /// + /// Responsible for retrieving, caching, and refreshing the configuration from metadata. + /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. + /// + public IConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic + /// recovery in the event of a signature key rollover. This is enabled by default. + /// + public bool RefreshOnIssuerKeyNotFound { get; set; } = true; + + /// + /// Gets the ordered list of used to validate access tokens. + /// + public IList SecurityTokenValidators { get; } = new List { new JwtSecurityTokenHandler() }; + + /// + /// Gets or sets the parameters used to validate identity tokens. + /// + /// Contains the types and definitions required for validating a token. + /// if 'value' is null. + public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); + + /// + /// Defines whether the bearer token should be stored in the + /// after a successful authorization. + /// + public bool SaveToken { get; set; } = true; + + /// + /// Defines whether the token validation errors should be returned to the caller. + /// Enabled by default, this option can be disabled to prevent the JWT handler + /// from returning an error and an error_description in the WWW-Authenticate header. + /// + public bool IncludeErrorDetails { get; set; } = true; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs new file mode 100644 index 0000000000..8829bfac0f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + /// + /// Used to setup defaults for all . + /// + public class JwtBearerPostConfigureOptions : IPostConfigureOptions + { + /// + /// Invoked to post configure a JwtBearerOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, JwtBearerOptions options) + { + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.Audience)) + { + options.TokenValidationParameters.ValidAudience = options.Audience; + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority))) + { + if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority)) + { + options.MetadataAddress = options.Authority; + if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + options.MetadataAddress += "/"; + } + + options.MetadataAddress += ".well-known/openid-configuration"; + } + + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + httpClient.Timeout = options.BackchannelTimeout; + httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs new file mode 100644 index 0000000000..5c6ca088a8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _tokenValidationFailed; + private static Action _tokenValidationSucceeded; + private static Action _errorProcessingMessage; + + static LoggingExtensions() + { + _tokenValidationFailed = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Information, + formatString: "Failed to validate the token."); + _tokenValidationSucceeded = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Information, + formatString: "Successfully validated the token."); + _errorProcessingMessage = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Error, + formatString: "Exception occurred while processing message."); + } + + public static void TokenValidationFailed(this ILogger logger, Exception ex) + => _tokenValidationFailed(logger, ex); + + public static void TokenValidationSucceeded(this ILogger logger) + => _tokenValidationSucceeded(logger, null); + + public static void ErrorProcessingMessage(this ILogger logger, Exception ex) + => _errorProcessingMessage(logger, ex); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj new file mode 100644 index 0000000000..e5bae5a3da --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..e95b8e061b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.JwtBearer.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json new file mode 100644 index 0000000000..d3839022b5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json @@ -0,0 +1,1064 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.JwtBearer, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.JwtBearerAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseJwtBearerAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseJwtBearerAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticateFailure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticateFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Error", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ErrorDescription", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ErrorDescription", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ErrorUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ErrorUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnChallenge", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnChallenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Challenge", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.SecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Bearer\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authority", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Authority", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Audience", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Audience", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Challenge", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Challenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelHttpHandler", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpMessageHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelHttpHandler", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpMessageHandler" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenValidators", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SaveToken", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SaveToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IncludeErrorDetails", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IncludeErrorDetails", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj new file mode 100644 index 0000000000..0eddc6f764 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support the Microsoft Account authentication workflow. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs new file mode 100644 index 0000000000..7fd71d7a9b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add Microsoft Account authentication capabilities to an HTTP application pipeline. + /// + public static class MicrosoftAccountAppBuilderExtensions + { + /// + /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app, MicrosoftAccountOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs new file mode 100644 index 0000000000..1b0859c5b7 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount +{ + public static class MicrosoftAccountDefaults + { + public const string AuthenticationScheme = "Microsoft"; + + public static readonly string DisplayName = "Microsoft"; + + public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; + + public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + + public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs new file mode 100644 index 0000000000..7f24e5af77 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class MicrosoftAccountExtensions + { + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder) + => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs new file mode 100644 index 0000000000..bba5472774 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount +{ + public class MicrosoftAccountHandler : OAuthHandler + { + public MicrosoftAccountHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + { + var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"An error occurred when retrieving Microsoft user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs new file mode 100644 index 0000000000..dbca3507e9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount +{ + /// + /// Configuration options for . + /// + public class MicrosoftAccountOptions : OAuthOptions + { + /// + /// Initializes a new . + /// + public MicrosoftAccountOptions() + { + CallbackPath = new PathString("/signin-microsoft"); + AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint; + TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; + UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint; + Scope.Add("https://graph.microsoft.com/user.read"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname"); + ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value("mail") ?? user.Value("userPrincipalName")); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..7ef5acecb2 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +// +namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.MicrosoftAccount.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The user does not have an id. + /// + internal static string Exception_MissingId + { + get => GetString("Exception_MissingId"); + } + + /// + /// The user does not have an id. + /// + internal static string FormatException_MissingId() + => GetString("Exception_MissingId"); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx new file mode 100644 index 0000000000..26eb43888e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The user does not have an id. + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json new file mode 100644 index 0000000000..877e9035ac --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json @@ -0,0 +1,284 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.MicrosoftAccount, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.MicrosoftAccountExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Microsoft\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MicrosoftAccountAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseMicrosoftAccountAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseMicrosoftAccountAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs new file mode 100644 index 0000000000..78b63bb38e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// Infrastructure for mapping user data from a json structure to claims on the ClaimsIdentity. + /// + public abstract class ClaimAction + { + /// + /// Create a new claim manipulation action. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + public ClaimAction(string claimType, string valueType) + { + ClaimType = claimType; + ValueType = valueType; + } + + /// + /// The value to use for Claim.Type when creating a Claim. + /// + public string ClaimType { get; } + + // The value to use for Claim.ValueType when creating a Claim. + public string ValueType { get; } + + /// + /// Examine the given userData json, determine if the requisite data is present, and optionally add it + /// as a new Claim on the ClaimsIdentity. + /// + /// The source data to examine. This value may be null. + /// The identity to add Claims to. + /// The value to use for Claim.Issuer when creating a Claim. + public abstract void Run(JObject userData, ClaimsIdentity identity, string issuer); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs new file mode 100644 index 0000000000..63da155d7c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A collection of ClaimActions used when mapping user data to Claims. + /// + public class ClaimActionCollection : IEnumerable + { + private IList Actions { get; } = new List(); + + /// + /// Remove all claim actions. + /// + public void Clear() => Actions.Clear(); + + /// + /// Remove all claim actions for the given ClaimType. + /// + /// The ClaimType of maps to remove. + public void Remove(string claimType) + { + var itemsToRemove = Actions.Where(map => string.Equals(claimType, map.ClaimType, StringComparison.OrdinalIgnoreCase)).ToList(); + itemsToRemove.ForEach(map => Actions.Remove(map)); + } + + /// + /// Add a claim action to the collection. + /// + /// The claim action to add. + public void Add(ClaimAction action) + { + Actions.Add(action); + } + + public IEnumerator GetEnumerator() + { + return Actions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Actions.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs new file mode 100644 index 0000000000..5a178957a0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionMapExtensions + { + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new JsonKeyClaimAction(claimType, valueType, jsonKey)); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey) + { + collection.MapJsonSubKey(claimType, jsonKey, subKey, ClaimValueTypes.String); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey, string valueType) + { + collection.Add(new JsonSubKeyClaimAction(claimType, valueType, jsonKey, subKey)); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, Func resolver) + { + collection.MapCustomJson(claimType, ClaimValueTypes.String, resolver); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, string valueType, Func resolver) + { + collection.Add(new CustomJsonClaimAction(claimType, valueType, resolver)); + } + + /// + /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding duplicates. + /// + /// + public static void MapAll(this ClaimActionCollection collection) + { + collection.Clear(); + collection.Add(new MapAllClaimsAction()); + } + + /// + /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding the specified types. + /// + /// + /// + public static void MapAllExcept(this ClaimActionCollection collection, params string[] exclusions) + { + collection.MapAll(); + collection.DeleteClaims(exclusions); + } + + /// + /// Delete all claims from the given ClaimsIdentity with the given ClaimType. + /// + /// + /// + public static void DeleteClaim(this ClaimActionCollection collection, string claimType) + { + collection.Add(new DeleteClaimAction(claimType)); + } + + /// + /// Delete all claims from the ClaimsIdentity with the given claimTypes. + /// + /// + /// + public static void DeleteClaims(this ClaimActionCollection collection, params string[] claimTypes) + { + if (claimTypes == null) + { + throw new ArgumentNullException(nameof(claimTypes)); + } + + foreach (var claimType in claimTypes) + { + collection.Add(new DeleteClaimAction(claimType)); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs new file mode 100644 index 0000000000..21a4f70e12 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects the value from the json user data by running the given Func resolver. + /// + public class CustomJsonClaimAction : ClaimAction + { + /// + /// Creates a new CustomJsonClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public CustomJsonClaimAction(string claimType, string valueType, Func resolver) + : base(claimType, valueType) + { + Resolver = resolver; + } + + /// + /// The Func that will be called to select value from the given json user data. + /// + public Func Resolver { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + if (userData == null) + { + return; + } + var value = Resolver(userData); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs new file mode 100644 index 0000000000..75167cabcb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that deletes all claims from the given ClaimsIdentity with the given ClaimType. + /// + public class DeleteClaimAction : ClaimAction + { + /// + /// Creates a new DeleteClaimAction. + /// + /// The ClaimType of Claims to delete. + public DeleteClaimAction(string claimType) + : base(claimType, ClaimValueTypes.String) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + foreach (var claim in identity.FindAll(ClaimType).ToList()) + { + identity.TryRemoveClaim(claim); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs new file mode 100644 index 0000000000..ccd1a965dc --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + public class JsonKeyClaimAction : ClaimAction + { + /// + /// Creates a new JsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public JsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType) + { + JsonKey = jsonKey; + } + + /// + /// The top level key to look for in the json user data. + /// + public string JsonKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?[JsonKey]; + if (value is JValue) + { + AddClaim(value?.ToString(), identity, issuer); + } + else if (value is JArray) + { + foreach (var v in value) + { + AddClaim(v?.ToString(), identity, issuer); + } + } + } + + private void AddClaim(string value, ClaimsIdentity identity, string issuer) + { + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs new file mode 100644 index 0000000000..bc29672d0f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a second level value from the json user data with the given top level key + /// name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + public class JsonSubKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new JsonSubKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public JsonSubKeyClaimAction(string claimType, string valueType, string jsonKey, string subKey) + : base(claimType, valueType, jsonKey) + { + SubKey = subKey; + } + + /// + /// The second level key to look for in the json user data. + /// + public string SubKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = GetValue(userData, JsonKey, SubKey); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + + // Get the given subProperty from a property. + private static string GetValue(JObject userData, string propertyName, string subProperty) + { + if (userData != null && userData.TryGetValue(propertyName, out var value)) + { + var subObject = JObject.Parse(value.ToString()); + if (subObject != null && subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + return null; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs new file mode 100644 index 0000000000..b3bf5d99f1 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects all top level values from the json user data and adds them as Claims. + /// This excludes duplicate sets of names and values. + /// + public class MapAllClaimsAction : ClaimAction + { + public MapAllClaimsAction() : base("All", ClaimValueTypes.String) + { + } + + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + if (userData == null) + { + return; + } + foreach (var pair in userData) + { + var claimValue = userData.TryGetValue(pair.Key, out var value) ? value.ToString() : null; + + // Avoid adding a claim if there's a duplicate name and value. This often happens in OIDC when claims are + // retrieved both from the id_token and from the user-info endpoint. + var duplicate = identity.FindFirst(c => string.Equals(c.Type, pair.Key, StringComparison.OrdinalIgnoreCase) + && string.Equals(c.Value, claimValue, StringComparison.Ordinal)) != null; + + if (!duplicate) + { + identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer)); + } + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs new file mode 100644 index 0000000000..f660dd2247 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + /// + /// Contains information about the login session as well as the user . + /// + public class OAuthCreatingTicketContext : ResultContext + { + /// + /// Initializes a new . + /// + /// The . + /// The . + /// The HTTP environment. + /// The authentication scheme. + /// The options used by the authentication middleware. + /// The HTTP client used by the authentication middleware + /// The tokens returned from the token endpoint. + public OAuthCreatingTicketContext( + ClaimsPrincipal principal, + AuthenticationProperties properties, + HttpContext context, + AuthenticationScheme scheme, + OAuthOptions options, + HttpClient backchannel, + OAuthTokenResponse tokens) + : this(principal, properties, context, scheme, options, backchannel, tokens, user: new JObject()) + { } + + /// + /// Initializes a new . + /// + /// The . + /// The . + /// The HTTP environment. + /// The authentication scheme. + /// The options used by the authentication middleware. + /// The HTTP client used by the authentication middleware + /// The tokens returned from the token endpoint. + /// The JSON-serialized user. + public OAuthCreatingTicketContext( + ClaimsPrincipal principal, + AuthenticationProperties properties, + HttpContext context, + AuthenticationScheme scheme, + OAuthOptions options, + HttpClient backchannel, + OAuthTokenResponse tokens, + JObject user) + : base(context, scheme, options) + { + if (backchannel == null) + { + throw new ArgumentNullException(nameof(backchannel)); + } + + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + TokenResponse = tokens; + Backchannel = backchannel; + User = user; + Principal = principal; + Properties = properties; + } + + /// + /// Gets the JSON-serialized user or an empty + /// if it is not available. + /// + public JObject User { get; } + + /// + /// Gets the token response returned by the authentication service. + /// + public OAuthTokenResponse TokenResponse { get; } + + /// + /// Gets the access token provided by the authentication service. + /// + public string AccessToken => TokenResponse.AccessToken; + + /// + /// Gets the access token type provided by the authentication service. + /// + public string TokenType => TokenResponse.TokenType; + + /// + /// Gets the refresh token provided by the authentication service. + /// + public string RefreshToken => TokenResponse.RefreshToken; + + /// + /// Gets the access token expiration time. + /// + public TimeSpan? ExpiresIn + { + get + { + int value; + if (int.TryParse(TokenResponse.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + { + return TimeSpan.FromSeconds(value); + } + + return null; + } + } + + /// + /// Gets the backchannel used to communicate with the provider. + /// + public HttpClient Backchannel { get; } + + /// + /// Gets the main identity exposed by the authentication ticket. + /// This property returns null when the ticket is null. + /// + public ClaimsIdentity Identity => Principal?.Identity as ClaimsIdentity; + + public void RunClaimActions() => RunClaimActions(User); + + public void RunClaimActions(JObject userData) + { + if (userData == null) + { + throw new ArgumentNullException(nameof(userData)); + } + + foreach (var action in Options.ClaimActions) + { + action.Run(userData, Identity, Options.ClaimsIssuer ?? Scheme.Name); + } + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs new file mode 100644 index 0000000000..9e194491b9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + /// + /// Default implementation. + /// + public class OAuthEvents : RemoteAuthenticationEvents + { + /// + /// Gets or sets the function that is invoked when the CreatingTicket method is invoked. + /// + public Func OnCreatingTicket { get; set; } = context => Task.CompletedTask; + + /// + /// Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked. + /// + public Func, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context => + { + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; + + /// + /// Invoked after the provider successfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task CreatingTicket(OAuthCreatingTicketContext context) => OnCreatingTicket(context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth handler. + /// + /// Contains redirect URI and of the challenge. + public virtual Task RedirectToAuthorizationEndpoint(RedirectContext context) => OnRedirectToAuthorizationEndpoint(context); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj new file mode 100644 index 0000000000..5c8a5e3a96 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to support any standard OAuth 2.0 authentication workflow. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs new file mode 100644 index 0000000000..d55f311f7b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add OAuth 2.0 authentication capabilities to an HTTP application pipeline. + /// + public static class OAuthAppBuilderExtensions + { + /// + /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app, OAuthOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs new file mode 100644 index 0000000000..fc768a8ac8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthChallengeProperties : AuthenticationProperties + { + /// + /// The parameter key for the "scope" argument being used for a challenge request. + /// + public static readonly string ScopeKey = "scope"; + + public OAuthChallengeProperties() + { } + + public OAuthChallengeProperties(IDictionary items) + : base(items) + { } + + public OAuthChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "scope" parameter value being used for a challenge request. + /// + public ICollection Scope + { + get => GetParameter>(ScopeKey); + set => SetParameter(ScopeKey, value); + } + + /// + /// Set the "scope" parameter value. + /// + /// List of scopes. + public virtual void SetScope(params string[] scopes) + { + Scope = scopes; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs new file mode 100644 index 0000000000..376f8ab01a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public static class OAuthDefaults + { + public static readonly string DisplayName = "OAuth"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs new file mode 100644 index 0000000000..22c541a0ac --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OAuthExtensions + { + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddOAuth>(authenticationScheme, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth>(authenticationScheme, displayName, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + => builder.AddOAuth(authenticationScheme, OAuthDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OAuthPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs new file mode 100644 index 0000000000..808e0f9039 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs @@ -0,0 +1,243 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthHandler : RemoteAuthenticationHandler where TOptions : OAuthOptions, new() + { + protected HttpClient Backchannel => Options.Backchannel; + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new OAuthEvents Events + { + get { return (OAuthEvents)base.Events; } + set { base.Events = value; } + } + + public OAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new OAuthEvents()); + + protected override async Task 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)) + { + 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(); + + 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); + } + } + + protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) + { + var tokenRequestParameters = new Dictionary() + { + { "client_id", Options.ClientId }, + { "redirect_uri", redirectUri }, + { "client_secret", Options.ClientSecret }, + { "code", code }, + { "grant_type", "authorization_code" }, + }; + + var requestContent = new FormUrlEncodedContent(tokenRequestParameters); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + requestMessage.Content = requestContent; + var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); + if (response.IsSuccessStatusCode) + { + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + return OAuthTokenResponse.Success(payload); + } + else + { + var error = "OAuth token endpoint failure: " + await Display(response); + return OAuthTokenResponse.Failed(new Exception(error)); + } + } + + private static async Task Display(HttpResponseMessage response) + { + var output = new StringBuilder(); + output.Append("Status: " + response.StatusCode + ";"); + output.Append("Headers: " + response.Headers.ToString() + ";"); + output.Append("Body: " + await response.Content.ReadAsStringAsync() + ";"); + return output.ToString(); + } + + protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + { + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens); + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath)); + var redirectContext = new RedirectContext( + Context, Scheme, Options, + properties, authorizationEndpoint); + await Events.RedirectToAuthorizationEndpoint(redirectContext); + } + + protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) + { + var scopeParameter = properties.GetParameter>(OAuthChallengeProperties.ScopeKey); + var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope(); + + var state = Options.StateDataFormat.Protect(properties); + var parameters = new Dictionary + { + { "client_id", Options.ClientId }, + { "scope", scope }, + { "response_type", "code" }, + { "redirect_uri", redirectUri }, + { "state", state }, + }; + return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters); + } + + /// + /// Format a list of OAuth scopes. + /// + /// List of scopes. + /// Formatted scopes. + protected virtual string FormatScope(IEnumerable scopes) + => string.Join(" ", scopes); // OAuth2 3.3 space separated + + /// + /// Format the property. + /// + /// Formatted scopes. + /// Subclasses should rather override . + protected virtual string FormatScope() + => FormatScope(Options.Scope); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs new file mode 100644 index 0000000000..3c71f055f5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http.Authentication; +using System.Globalization; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + /// + /// Configuration options OAuth. + /// + public class OAuthOptions : RemoteAuthenticationOptions + { + public OAuthOptions() + { + Events = new OAuthEvents(); + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + 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)); + } + } + + /// + /// Gets or sets the provider-assigned client id. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the provider-assigned client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets the URI where the client will be redirected to authenticate. + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to exchange the OAuth token. + /// + public string TokenEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to obtain the user information. + /// This value is not used in the default implementation, it is for use in custom implementations of + /// IOAuthAuthenticationEvents.Authenticated or OAuthAuthenticationHandler.CreateTicketAsync. + /// + public string UserInformationEndpoint { get; set; } + + /// + /// Gets or sets the used to handle authentication events. + /// + public new OAuthEvents Events + { + get { return (OAuthEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + + /// + /// Gets the list of permissions to request. + /// + public ICollection Scope { get; } = new HashSet(); + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs new file mode 100644 index 0000000000..e97346413c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Used to setup defaults for the OAuthOptions. + /// + public class OAuthPostConfigureOptions : IPostConfigureOptions + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + { + private readonly IDataProtectionProvider _dp; + + public OAuthPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + 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); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs new file mode 100644 index 0000000000..aa4026b009 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthTokenResponse + { + private OAuthTokenResponse(JObject response) + { + Response = response; + AccessToken = response.Value("access_token"); + TokenType = response.Value("token_type"); + RefreshToken = response.Value("refresh_token"); + ExpiresIn = response.Value("expires_in"); + } + + private OAuthTokenResponse(Exception error) + { + Error = error; + } + + public static OAuthTokenResponse Success(JObject response) + { + return new OAuthTokenResponse(response); + } + + public static OAuthTokenResponse Failed(Exception error) + { + return new OAuthTokenResponse(error); + } + + public JObject Response { get; set; } + public string AccessToken { get; set; } + public string TokenType { get; set; } + public string RefreshToken { get; set; } + public string ExpiresIn { get; set; } + public Exception Error { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..5a38ade0b9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.OAuth.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json new file mode 100644 index 0000000000..9c23947049 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json @@ -0,0 +1,1810 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OAuth, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.OAuthExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.OAuthPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Builder.OAuthAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOAuthAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOAuthAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionMapExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonSubKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonSubKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapCustomJson", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapCustomJson", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapAll", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapAllExcept", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "exclusions", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteClaim", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteClaims", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimTypes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenResponse", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresIn", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Identity", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RunClaimActions", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RunClaimActions", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + }, + { + "Name": "backchannel", + "Type": "System.Net.Http.HttpClient" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + }, + { + "Name": "backchannel", + "Type": "System.Net.Http.HttpClient" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnCreatingTicket", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnCreatingTicket", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreatingTicket", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scope", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.ICollection" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ScopeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExchangeCodeAsync", + "Parameters": [ + { + "Name": "code", + "Type": "System.String" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildChallengeUrl", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UserInformationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "response", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "error", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Response", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresIn", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Error", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClaimType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "action", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.CustomJsonClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Resolver", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.DeleteClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_JsonKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonSubKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.MapAllClaimsAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs new file mode 100644 index 0000000000..4e349579f3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionUniqueExtensions + { + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapUniqueJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new UniqueJsonKeyClaimAction(claimType, valueType, jsonKey)); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs new file mode 100644 index 0000000000..132885b3ca --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + public class UniqueJsonKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new UniqueJsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public UniqueJsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType, jsonKey) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?.Value(JsonKey); + if (string.IsNullOrEmpty(value)) + { + // Not found + return; + } + + var claim = identity.FindFirst(c => string.Equals(c.Type, JsonKey, System.StringComparison.OrdinalIgnoreCase)); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate + return; + } + + claim = identity.FindFirst(c => + { + // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set + return c.Properties.TryGetValue(JwtSecurityTokenHandler.ShortClaimTypeProperty, out var shortType) + && string.Equals(shortType, JsonKey, System.StringComparison.OrdinalIgnoreCase); + }); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate with an alternate name. + return; + } + + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs new file mode 100644 index 0000000000..203da93c53 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class AuthenticationFailedContext : RemoteAuthenticationContext + { + public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options) + : base(context, scheme, options, new AuthenticationProperties()) + { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + public Exception Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs new file mode 100644 index 0000000000..bdf6e4a7ff --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// This Context can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol. + /// + public class AuthorizationCodeReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public AuthorizationCodeReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// Gets or sets the that was received in the authentication response, if any. + /// + public JwtSecurityToken JwtSecurityToken { get; set; } + + /// + /// The request that will be sent to the token endpoint and is available for customization. + /// + public OpenIdConnectMessage TokenEndpointRequest { get; set; } + + /// + /// The configured communication channel to the identity provider for use when making custom requests to the token endpoint. + /// + public HttpClient Backchannel { get; internal set; } + + /// + /// If the developer chooses to redeem the code themselves then they can provide the resulting tokens here. This is the + /// same as calling HandleCodeRedemption. If set then the handler will not attempt to redeem the code. An IdToken + /// is required if one had not been previously received in the authorization response. An access token is optional + /// if the handler is to contact the user-info endpoint. + /// + public OpenIdConnectMessage TokenEndpointResponse { get; set; } + + /// + /// Indicates if the developer choose to handle (or skip) the code redemption. If true then the handler will not attempt + /// to redeem the code. See HandleCodeRedemption and TokenEndpointResponse. + /// + public bool HandledCodeRedemption => TokenEndpointResponse != null; + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. An access token can optionally be provided for the handler to contact the + /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption() + { + TokenEndpointResponse = new OpenIdConnectMessage(); + } + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. An access token can optionally be provided for the handler to contact the + /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption(string accessToken, string idToken) + { + TokenEndpointResponse = new OpenIdConnectMessage() { AccessToken = accessToken, IdToken = idToken }; + } + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. An access token can optionally be provided for the handler to contact the + /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption(OpenIdConnectMessage tokenEndpointResponse) + { + TokenEndpointResponse = tokenEndpointResponse; + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs new file mode 100644 index 0000000000..7d06e44799 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class MessageReceivedContext : RemoteAuthenticationContext + { + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. + /// + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs new file mode 100644 index 0000000000..2a48d250bb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. + /// + public class OpenIdConnectEvents : RemoteAuthenticationEvents + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after security token validation if an authorization code is present in the protocol message. + /// + public Func OnAuthorizationCodeReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a protocol message is first received. + /// + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State + /// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize + /// parameters sent to the identity provider. + /// + public Func OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before redirecting to the identity provider to sign out. + /// + public Func OnRedirectToIdentityProviderForSignOut { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before redirecting to the at the end of a remote sign-out flow. + /// + public Func OnSignedOutCallbackRedirect { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a request is received on the RemoteSignOutPath. + /// + public Func OnRemoteSignOut { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. + /// + public Func OnTokenResponseReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when an IdToken has been validated and produced an AuthenticationTicket. + /// + public Func OnTokenValidated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when user information is retrieved from the UserInfoEndpoint. + /// + public Func OnUserInformationReceived { get; set; } = context => Task.CompletedTask; + + public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); + + public virtual Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) => OnAuthorizationCodeReceived(context); + + public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); + + public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context); + + public virtual Task RedirectToIdentityProviderForSignOut(RedirectContext context) => OnRedirectToIdentityProviderForSignOut(context); + + public virtual Task SignedOutCallbackRedirect(RemoteSignOutContext context) => OnSignedOutCallbackRedirect(context); + + public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context); + + public virtual Task TokenResponseReceived(TokenResponseReceivedContext context) => OnTokenResponseReceived(context); + + public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context); + + public virtual Task UserInformationReceived(UserInformationReceivedContext context) => OnUserInformationReceived(context); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs new file mode 100644 index 0000000000..9961c237d4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// an instance of is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events. + /// + public class RedirectContext : PropertiesContext + { + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// If true, will skip any default logic for this redirect. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this redirect. + /// + public void HandleResponse() => Handled = true; + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs new file mode 100644 index 0000000000..26720a58f8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class RemoteSignOutContext : RemoteAuthenticationContext + { + public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message) + : base(context, scheme, options, new AuthenticationProperties()) + => ProtocolMessage = message; + + public OpenIdConnectMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs new file mode 100644 index 0000000000..2bebdb8dc5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. + /// + public class TokenResponseReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public TokenResponseReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal user, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = user; + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// Gets or sets the that contains the tokens received after redeeming the code at the token endpoint. + /// + public OpenIdConnectMessage TokenEndpointResponse { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs new file mode 100644 index 0000000000..853857dc7b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class TokenValidatedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public TokenValidatedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + public JwtSecurityToken SecurityToken { get; set; } + + public OpenIdConnectMessage TokenEndpointResponse { get; set; } + + public string Nonce { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs new file mode 100644 index 0000000000..0b855eaf39 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class UserInformationReceivedContext : RemoteAuthenticationContext + { + public UserInformationReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + public OpenIdConnectMessage ProtocolMessage { get; set; } + + public JObject User { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs new file mode 100644 index 0000000000..224af87b6f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs @@ -0,0 +1,508 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _redirectToIdentityProviderForSignOutHandledResponse; + private static Action _redirectToIdentityProviderHandledResponse; + private static Action _signoutCallbackRedirectHandledResponse; + private static Action _signoutCallbackRedirectSkipped; + private static Action _updatingConfiguration; + private static Action _receivedIdToken; + private static Action _redeemingCodeForTokens; + private static Action _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync; + private static Action _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync; + private static Action _enteringOpenIdAuthenticationHandlerHandleSignOutAsync; + private static Action _messageReceived; + private static Action _messageReceivedContextHandledResponse; + private static Action _messageReceivedContextSkipped; + private static Action _authorizationCodeReceived; + private static Action _configurationManagerRequestRefreshCalled; + private static Action _tokenResponseReceived; + private static Action _tokenValidatedHandledResponse; + private static Action _tokenValidatedSkipped; + private static Action _authenticationFailedContextHandledResponse; + private static Action _authenticationFailedContextSkipped; + private static Action _authorizationCodeReceivedContextHandledResponse; + private static Action _authorizationCodeReceivedContextSkipped; + private static Action _tokenResponseReceivedHandledResponse; + private static Action _tokenResponseReceivedSkipped; + private static Action _userInformationReceived; + private static Action _userInformationReceivedHandledResponse; + private static Action _userInformationReceivedSkipped; + private static Action _invalidLogoutQueryStringRedirectUrl; + private static Action _nullOrEmptyAuthorizationResponseState; + private static Action _unableToReadAuthorizationResponseState; + private static Action _responseError; + private static Action _responseErrorWithStatusCode; + private static Action _exceptionProcessingMessage; + private static Action _accessTokenNotAvailable; + private static Action _retrievingClaims; + private static Action _userInfoEndpointNotSet; + private static Action _unableToProtectNonceCookie; + private static Action _invalidAuthenticationRequestUrl; + private static Action _unableToReadIdToken; + private static Action _invalidSecurityTokenType; + private static Action _unableToValidateIdToken; + private static Action _postAuthenticationLocalRedirect; + private static Action _postSignOutRedirect; + private static Action _remoteSignOutHandledResponse; + private static Action _remoteSignOutSkipped; + private static Action _remoteSignOut; + private static Action _remoteSignOutSessionIdMissing; + private static Action _remoteSignOutSessionIdInvalid; + private static Action _signOut; + + static LoggingExtensions() + { + // Final + _redirectToIdentityProviderForSignOutHandledResponse = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Debug, + formatString: "RedirectToIdentityProviderForSignOut.HandledResponse"); + _invalidLogoutQueryStringRedirectUrl = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Warning, + formatString: "The query string for Logout is not a well-formed URI. Redirect URI: '{RedirectUrl}'."); + _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Trace, + formatString: "Entering {OpenIdConnectHandlerType}'s HandleUnauthorizedAsync."); + _enteringOpenIdAuthenticationHandlerHandleSignOutAsync = LoggerMessage.Define( + eventId: 14, + logLevel: LogLevel.Trace, + formatString: "Entering {OpenIdConnectHandlerType}'s HandleSignOutAsync."); + _postAuthenticationLocalRedirect = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Trace, + formatString: "Using properties.RedirectUri for 'local redirect' post authentication: '{RedirectUri}'."); + _redirectToIdentityProviderHandledResponse = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "RedirectToIdentityProvider.HandledResponse"); + _invalidAuthenticationRequestUrl = LoggerMessage.Define( + eventId: 8, + logLevel: LogLevel.Warning, + formatString: "The redirect URI is not well-formed. The URI is: '{AuthenticationRequestUrl}'."); + _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync = LoggerMessage.Define( + eventId: 9, + logLevel: LogLevel.Trace, + formatString: "Entering {OpenIdConnectHandlerType}'s HandleRemoteAuthenticateAsync."); + _nullOrEmptyAuthorizationResponseState = LoggerMessage.Define( + eventId: 10, + logLevel: LogLevel.Debug, + formatString: "message.State is null or empty."); + _unableToReadAuthorizationResponseState = LoggerMessage.Define( + eventId: 11, + logLevel: LogLevel.Debug, + formatString: "Unable to read the message.State."); + _responseError = LoggerMessage.Define( + eventId: 12, + logLevel: LogLevel.Error, + formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}'."); + _responseErrorWithStatusCode = LoggerMessage.Define( + eventId: 49, + logLevel: LogLevel.Error, + formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}', status code '{StatusCode}'."); + _updatingConfiguration = LoggerMessage.Define( + eventId: 13, + logLevel: LogLevel.Debug, + formatString: "Updating configuration"); + _tokenValidatedHandledResponse = LoggerMessage.Define( + eventId: 15, + logLevel: LogLevel.Debug, + formatString: "TokenValidated.HandledResponse"); + _tokenValidatedSkipped = LoggerMessage.Define( + eventId: 16, + logLevel: LogLevel.Debug, + formatString: "TokenValidated.Skipped"); + _exceptionProcessingMessage = LoggerMessage.Define( + eventId: 17, + logLevel: LogLevel.Error, + formatString: "Exception occurred while processing message."); + _configurationManagerRequestRefreshCalled = LoggerMessage.Define( + eventId: 18, + logLevel: LogLevel.Debug, + formatString: "Exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called."); + _redeemingCodeForTokens = LoggerMessage.Define( + eventId: 19, + logLevel: LogLevel.Debug, + formatString: "Redeeming code for tokens."); + _retrievingClaims = LoggerMessage.Define( + eventId: 20, + logLevel: LogLevel.Trace, + formatString: "Retrieving claims from the user info endpoint."); + _receivedIdToken = LoggerMessage.Define( + eventId: 21, + logLevel: LogLevel.Debug, + formatString: "Received 'id_token'"); + _userInfoEndpointNotSet = LoggerMessage.Define( + eventId: 22, + logLevel: LogLevel.Debug, + formatString: "UserInfoEndpoint is not set. Claims cannot be retrieved."); + _unableToProtectNonceCookie = LoggerMessage.Define( + eventId: 23, + logLevel: LogLevel.Warning, + formatString: "Failed to un-protect the nonce cookie."); + _messageReceived = LoggerMessage.Define( + eventId: 24, + logLevel: LogLevel.Trace, + formatString: "MessageReceived: '{RedirectUrl}'."); + _messageReceivedContextHandledResponse = LoggerMessage.Define( + eventId: 25, + logLevel: LogLevel.Debug, + formatString: "MessageReceivedContext.HandledResponse"); + _messageReceivedContextSkipped = LoggerMessage.Define( + eventId: 26, + logLevel: LogLevel.Debug, + formatString: "MessageReceivedContext.Skipped"); + _authorizationCodeReceived = LoggerMessage.Define( + eventId: 27, + logLevel: LogLevel.Trace, + formatString: "Authorization code received."); + _authorizationCodeReceivedContextHandledResponse = LoggerMessage.Define( + eventId: 28, + logLevel: LogLevel.Debug, + formatString: "AuthorizationCodeReceivedContext.HandledResponse"); + _authorizationCodeReceivedContextSkipped = LoggerMessage.Define( + eventId: 29, + logLevel: LogLevel.Debug, + formatString: "AuthorizationCodeReceivedContext.Skipped"); + _tokenResponseReceived = LoggerMessage.Define( + eventId: 30, + logLevel: LogLevel.Trace, + formatString: "Token response received."); + _tokenResponseReceivedHandledResponse = LoggerMessage.Define( + eventId: 31, + logLevel: LogLevel.Debug, + formatString: "TokenResponseReceived.HandledResponse"); + _tokenResponseReceivedSkipped = LoggerMessage.Define( + eventId: 32, + logLevel: LogLevel.Debug, + formatString: "TokenResponseReceived.Skipped"); + _postSignOutRedirect = LoggerMessage.Define( + eventId: 33, + logLevel: LogLevel.Trace, + formatString: "Using properties.RedirectUri for redirect post authentication: '{RedirectUri}'."); + _userInformationReceived = LoggerMessage.Define( + eventId: 35, + logLevel: LogLevel.Trace, + formatString: "User information received: {User}"); + _userInformationReceivedHandledResponse = LoggerMessage.Define( + eventId: 36, + logLevel: LogLevel.Debug, + formatString: "The UserInformationReceived event returned Handled."); + _userInformationReceivedSkipped = LoggerMessage.Define( + eventId: 37, + logLevel: LogLevel.Debug, + formatString: "The UserInformationReceived event returned Skipped."); + _authenticationFailedContextHandledResponse = LoggerMessage.Define( + eventId: 38, + logLevel: LogLevel.Debug, + formatString: "AuthenticationFailedContext.HandledResponse"); + _authenticationFailedContextSkipped = LoggerMessage.Define( + eventId: 39, + logLevel: LogLevel.Debug, + formatString: "AuthenticationFailedContext.Skipped"); + _invalidSecurityTokenType = LoggerMessage.Define( + eventId: 40, + logLevel: LogLevel.Error, + formatString: "The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{SecurityTokenType}'"); + _unableToValidateIdToken = LoggerMessage.Define( + eventId: 41, + logLevel: LogLevel.Error, + formatString: "Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'."); + _accessTokenNotAvailable = LoggerMessage.Define( + eventId: 42, + logLevel: LogLevel.Debug, + formatString: "The access_token is not available. Claims cannot be retrieved."); + _unableToReadIdToken = LoggerMessage.Define( + eventId: 43, + logLevel: LogLevel.Error, + formatString: "Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'."); + _remoteSignOutHandledResponse = LoggerMessage.Define( + eventId: 44, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.HandledResponse"); + _remoteSignOutSkipped = LoggerMessage.Define( + eventId: 45, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.Skipped"); + _remoteSignOut = LoggerMessage.Define( + eventId: 46, + logLevel: LogLevel.Information, + formatString: "Remote signout request processed."); + _remoteSignOutSessionIdMissing = LoggerMessage.Define( + eventId: 47, + logLevel: LogLevel.Error, + formatString: "The remote signout request was ignored because the 'sid' parameter " + + "was missing, which may indicate an unsolicited logout."); + _remoteSignOutSessionIdInvalid = LoggerMessage.Define( + eventId: 48, + logLevel: LogLevel.Error, + formatString: "The remote signout request was ignored because the 'sid' parameter didn't match " + + "the expected value, which may indicate an unsolicited logout."); + _signOut = LoggerMessage.Define( + eventId: 49, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed out."); + _signoutCallbackRedirectHandledResponse = LoggerMessage.Define( + eventId: 50, + logLevel: LogLevel.Debug, + formatString: "RedirectToSignedOutRedirectUri.HandledResponse"); + _signoutCallbackRedirectSkipped = LoggerMessage.Define( + eventId: 51, + logLevel: LogLevel.Debug, + formatString: "RedirectToSignedOutRedirectUri.Skipped"); + } + + public static void UpdatingConfiguration(this ILogger logger) + { + _updatingConfiguration(logger, null); + } + + public static void ConfigurationManagerRequestRefreshCalled(this ILogger logger) + { + _configurationManagerRequestRefreshCalled(logger, null); + } + + public static void AuthorizationCodeReceived(this ILogger logger) + { + _authorizationCodeReceived(logger, null); + } + + public static void TokenResponseReceived(this ILogger logger) + { + _tokenResponseReceived(logger, null); + } + + public static void ReceivedIdToken(this ILogger logger) + { + _receivedIdToken(logger, null); + } + + public static void RedeemingCodeForTokens(this ILogger logger) + { + _redeemingCodeForTokens(logger, null); + } + + public static void TokenValidatedHandledResponse(this ILogger logger) + { + _tokenValidatedHandledResponse(logger, null); + } + + public static void TokenValidatedSkipped(this ILogger logger) + { + _tokenValidatedSkipped(logger, null); + } + + public static void AuthorizationCodeReceivedContextHandledResponse(this ILogger logger) + { + _authorizationCodeReceivedContextHandledResponse(logger, null); + } + + public static void AuthorizationCodeReceivedContextSkipped(this ILogger logger) + { + _authorizationCodeReceivedContextSkipped(logger, null); + } + + public static void TokenResponseReceivedHandledResponse(this ILogger logger) + { + _tokenResponseReceivedHandledResponse(logger, null); + } + + public static void TokenResponseReceivedSkipped(this ILogger logger) + { + _tokenResponseReceivedSkipped(logger, null); + } + + public static void AuthenticationFailedContextHandledResponse(this ILogger logger) + { + _authenticationFailedContextHandledResponse(logger, null); + } + + public static void AuthenticationFailedContextSkipped(this ILogger logger) + { + _authenticationFailedContextSkipped(logger, null); + } + + public static void MessageReceived(this ILogger logger, string redirectUrl) + { + _messageReceived(logger, redirectUrl, null); + } + + public static void MessageReceivedContextHandledResponse(this ILogger logger) + { + _messageReceivedContextHandledResponse(logger, null); + } + + public static void MessageReceivedContextSkipped(this ILogger logger) + { + _messageReceivedContextSkipped(logger, null); + } + + public static void RedirectToIdentityProviderForSignOutHandledResponse(this ILogger logger) + { + _redirectToIdentityProviderForSignOutHandledResponse(logger, null); + } + + public static void RedirectToIdentityProviderHandledResponse(this ILogger logger) + { + _redirectToIdentityProviderHandledResponse(logger, null); + } + + public static void SignoutCallbackRedirectHandledResponse(this ILogger logger) + { + _signoutCallbackRedirectHandledResponse(logger, null); + } + + public static void SignoutCallbackRedirectSkipped(this ILogger logger) + { + _signoutCallbackRedirectSkipped(logger, null); + } + + public static void UserInformationReceivedHandledResponse(this ILogger logger) + { + _userInformationReceivedHandledResponse(logger, null); + } + + public static void UserInformationReceivedSkipped(this ILogger logger) + { + _userInformationReceivedSkipped(logger, null); + } + + public static void InvalidLogoutQueryStringRedirectUrl(this ILogger logger, string redirectUrl) + { + _invalidLogoutQueryStringRedirectUrl(logger, redirectUrl, null); + } + + public static void NullOrEmptyAuthorizationResponseState(this ILogger logger) + { + _nullOrEmptyAuthorizationResponseState(logger, null); + } + + public static void UnableToReadAuthorizationResponseState(this ILogger logger) + { + _unableToReadAuthorizationResponseState(logger, null); + } + + public static void ResponseError(this ILogger logger, string error, string errorDescription, string errorUri) + { + _responseError(logger, error, errorDescription, errorUri, null); + } + + public static void ResponseErrorWithStatusCode(this ILogger logger, string error, string errorDescription, string errorUri, int statusCode) + { + _responseErrorWithStatusCode(logger, error, errorDescription, errorUri, statusCode, null); + } + + public static void ExceptionProcessingMessage(this ILogger logger, Exception ex) + { + _exceptionProcessingMessage(logger, ex); + } + + public static void AccessTokenNotAvailable(this ILogger logger) + { + _accessTokenNotAvailable(logger, null); + } + + public static void RetrievingClaims(this ILogger logger) + { + _retrievingClaims(logger, null); + } + + public static void UserInfoEndpointNotSet(this ILogger logger) + { + _userInfoEndpointNotSet(logger, null); + } + + public static void UnableToProtectNonceCookie(this ILogger logger, Exception ex) + { + _unableToProtectNonceCookie(logger, ex); + } + + public static void InvalidAuthenticationRequestUrl(this ILogger logger, string redirectUri) + { + _invalidAuthenticationRequestUrl(logger, redirectUri, null); + } + + public static void UnableToReadIdToken(this ILogger logger, string idToken) + { + _unableToReadIdToken(logger, idToken, null); + } + + public static void InvalidSecurityTokenType(this ILogger logger, string tokenType) + { + _invalidSecurityTokenType(logger, tokenType, null); + } + + public static void UnableToValidateIdToken(this ILogger logger, string idToken) + { + _unableToValidateIdToken(logger, idToken, null); + } + + public static void EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(this ILogger logger, string openIdConnectHandlerTypeName) + { + _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(logger, openIdConnectHandlerTypeName, null); + } + + public static void EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(this ILogger logger, string openIdConnectHandlerTypeName) + { + _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(logger, openIdConnectHandlerTypeName, null); + } + + public static void EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(this ILogger logger, string openIdConnectHandlerTypeName) + { + _enteringOpenIdAuthenticationHandlerHandleSignOutAsync(logger, openIdConnectHandlerTypeName, null); + } + + public static void UserInformationReceived(this ILogger logger, string user) + { + _userInformationReceived(logger, user, null); + } + + public static void PostAuthenticationLocalRedirect(this ILogger logger, string redirectUri) + { + _postAuthenticationLocalRedirect(logger, redirectUri, null); + } + + public static void PostSignOutRedirect(this ILogger logger, string redirectUri) + { + _postSignOutRedirect(logger, redirectUri, null); + } + + public static void RemoteSignOutHandledResponse(this ILogger logger) + { + _remoteSignOutHandledResponse(logger, null); + } + + public static void RemoteSignOutSkipped(this ILogger logger) + { + _remoteSignOutSkipped(logger, null); + } + + public static void RemoteSignOut(this ILogger logger) + { + _remoteSignOut(logger, null); + } + + public static void RemoteSignOutSessionIdMissing(this ILogger logger) + { + _remoteSignOutSessionIdMissing(logger, null); + } + + public static void RemoteSignOutSessionIdInvalid(this ILogger logger) + { + _remoteSignOutSessionIdInvalid(logger, null); + } + + public static void SignedOut(this ILogger logger, string authenticationScheme) + { + _signOut(logger, authenticationScheme, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj new file mode 100644 index 0000000000..b7f4c1704a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to support the OpenID Connect authentication workflow. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs new file mode 100644 index 0000000000..0746ae3fdb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add OpenID Connect authentication capabilities to an HTTP application pipeline. + /// + public static class OpenIdConnectAppBuilderExtensions + { + /// + /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, OpenIdConnectOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs new file mode 100644 index 0000000000..0ced488deb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class OpenIdConnectChallengeProperties : OAuthChallengeProperties + { + /// + /// The parameter key for the "max_age" argument being used for a challenge request. + /// + public static readonly string MaxAgeKey = OpenIdConnectParameterNames.MaxAge; + + /// + /// The parameter key for the "prompt" argument being used for a challenge request. + /// + public static readonly string PromptKey = OpenIdConnectParameterNames.Prompt; + + public OpenIdConnectChallengeProperties() + { } + + public OpenIdConnectChallengeProperties(IDictionary items) + : base(items) + { } + + public OpenIdConnectChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "max_age" parameter value being used for a challenge request. + /// + public TimeSpan? MaxAge + { + get => GetParameter(MaxAgeKey); + set => SetParameter(MaxAgeKey, value); + } + + /// + /// The "prompt" parameter value being used for a challenge request. + /// + public string Prompt + { + get => GetParameter(PromptKey); + set => SetParameter(PromptKey, value); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs new file mode 100644 index 0000000000..f98ba87e02 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Default values related to OpenIdConnect authentication handler + /// + public static class OpenIdConnectDefaults + { + /// + /// Constant used to identify state in openIdConnect protocol message. + /// + public static readonly string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; + + /// + /// The default value used for OpenIdConnectOptions.AuthenticationScheme. + /// + public const string AuthenticationScheme = "OpenIdConnect"; + + /// + /// The default value for the display name. + /// + public static readonly string DisplayName = "OpenIdConnect"; + + /// + /// The prefix used to for the nonce in the cookie. + /// + public static readonly string CookieNoncePrefix = ".AspNetCore.OpenIdConnect.Nonce."; + + /// + /// The property for the RedirectUri that was used when asking for a 'authorizationCode'. + /// + public static readonly string RedirectUriForCodePropertiesKey = "OpenIdConnect.Code.RedirectUri"; + + /// + /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'state' parameter. + /// + public static readonly string UserstatePropertiesKey = "OpenIdConnect.Userstate"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs new file mode 100644 index 0000000000..f427bebaff --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OpenIdConnectExtensions + { + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder) + => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdConnectPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs new file mode 100644 index 0000000000..029cf541b7 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -0,0 +1,1240 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. + /// + public class OpenIdConnectHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler + { + private const string NonceProperty = "N"; + + private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT"; + + private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); + + private OpenIdConnectConfiguration _configuration; + + protected HttpClient Backchannel => Options.Backchannel; + + protected HtmlEncoder HtmlEncoder { get; } + + public OpenIdConnectHandler(IOptionsMonitor options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + HtmlEncoder = htmlEncoder; + } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new OpenIdConnectEvents Events + { + get { return (OpenIdConnectEvents)base.Events; } + set { base.Events = value; } + } + + protected override Task CreateEventsAsync() => Task.FromResult(new OpenIdConnectEvents()); + + public override Task HandleRequestAsync() + { + if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path) + { + return HandleRemoteSignOutAsync(); + } + else if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path) + { + return HandleSignOutCallbackAsync(); + } + + return base.HandleRequestAsync(); + } + + protected virtual async Task HandleRemoteSignOutAsync() + { + OpenIdConnectMessage message = null; + + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(Request.ContentType) + // May have media/type; charset=utf-8, allow partial match. + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.Body.CanRead) + { + var form = await Request.ReadFormAsync(); + message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message); + await Events.RemoteSignOut(remoteSignOutContext); + + if (remoteSignOutContext.Result != null) + { + if (remoteSignOutContext.Result.Handled) + { + Logger.RemoteSignOutHandledResponse(); + return true; + } + if (remoteSignOutContext.Result.Skipped) + { + Logger.RemoteSignOutSkipped(); + return false; + } + if (remoteSignOutContext.Result.Failure != null) + { + throw new InvalidOperationException("An error was returned from the RemoteSignOut event.", remoteSignOutContext.Result.Failure); + } + } + + if (message == null) + { + return false; + } + + // Try to extract the session identifier from the authentication ticket persisted by the sign-in handler. + // If the identifier cannot be found, bypass the session identifier checks: this may indicate that the + // authentication cookie was already cleared, that the session identifier was lost because of a lossy + // external/application cookie conversion or that the identity provider doesn't support sessions. + var sid = (await Context.AuthenticateAsync(Options.SignOutScheme)) + ?.Principal + ?.FindFirst(JwtRegisteredClaimNames.Sid) + ?.Value; + if (!string.IsNullOrEmpty(sid)) + { + // Ensure a 'sid' parameter was sent by the identity provider. + if (string.IsNullOrEmpty(message.Sid)) + { + Logger.RemoteSignOutSessionIdMissing(); + return true; + } + // Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket. + if (!string.Equals(sid, message.Sid, StringComparison.Ordinal)) + { + Logger.RemoteSignOutSessionIdInvalid(); + return true; + } + } + + Logger.RemoteSignOut(); + + // We've received a remote sign-out request + await Context.SignOutAsync(Options.SignOutScheme); + return true; + } + + /// + /// Redirect user to the identity provider for sign out + /// + /// A task executing the sign out procedure + public async virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + if (target != null) + { + await Context.SignOutAsync(target, properties); + return; + } + + properties = properties ?? new AuthenticationProperties(); + + Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName); + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var message = new OpenIdConnectMessage() + { + EnableTelemetryParameters = !Options.DisableTelemetry, + IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty, + + // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri + PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath) + }; + + // Get the post redirect URI. + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri); + if (string.IsNullOrWhiteSpace(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + } + Logger.PostSignOutRedirect(properties.RedirectUri); + + // Attach the identity token to the logout request when possible. + message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = message + }; + + await Events.RedirectToIdentityProviderForSignOut(redirectContext); + if (redirectContext.Handled) + { + Logger.RedirectToIdentityProviderForSignOutHandledResponse(); + return; + } + + message = redirectContext.ProtocolMessage; + + if (!string.IsNullOrEmpty(message.State)) + { + properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State; + } + + message.State = Options.StateDataFormat.Protect(properties); + + if (string.IsNullOrEmpty(message.IssuerAddress)) + { + throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid."); + } + + if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) + { + var redirectUri = message.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri); + } + + Response.Redirect(redirectUri); + } + else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) + { + var content = message.BuildFormPost(); + var buffer = Encoding.UTF8.GetBytes(content); + + Response.ContentLength = buffer.Length; + Response.ContentType = "text/html;charset=UTF-8"; + + // Emit Cache-Control=no-cache to prevent client caching. + Response.Headers[HeaderNames.CacheControl] = "no-cache"; + Response.Headers[HeaderNames.Pragma] = "no-cache"; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; + + await Response.Body.WriteAsync(buffer, 0, buffer.Length); + } + else + { + throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); + } + + Logger.SignedOut(Scheme.Name); + } + + /// + /// Response to the callback from OpenId provider after session ended. + /// + /// A task executing the callback procedure + protected async virtual Task HandleSignOutCallbackAsync() + { + var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + AuthenticationProperties properties = null; + if (!string.IsNullOrEmpty(message.State)) + { + properties = Options.StateDataFormat.Unprotect(message.State); + } + + var signOut = new RemoteSignOutContext(Context, Scheme, Options, message) + { + Properties = properties, + }; + + await Events.SignedOutCallbackRedirect(signOut); + if (signOut.Result != null) + { + if (signOut.Result.Handled) + { + Logger.SignoutCallbackRedirectHandledResponse(); + return true; + } + if (signOut.Result.Skipped) + { + Logger.SignoutCallbackRedirectSkipped(); + return false; + } + if (signOut.Result.Failure != null) + { + throw new InvalidOperationException("An error was returned from the SignedOutCallbackRedirect event.", signOut.Result.Failure); + } + } + + properties = signOut.Properties; + if (!string.IsNullOrEmpty(properties?.RedirectUri)) + { + Response.Redirect(properties.RedirectUri); + } + + return true; + } + + /// + /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. + /// + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName); + + // order for local RedirectUri + // 1. challenge.Properties.RedirectUri + // 2. CurrentUri if RedirectUri is not set) + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + Logger.PostAuthenticationLocalRedirect(properties.RedirectUri); + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var message = new OpenIdConnectMessage + { + ClientId = Options.ClientId, + EnableTelemetryParameters = !Options.DisableTelemetry, + IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty, + RedirectUri = BuildRedirectUri(Options.CallbackPath), + Resource = Options.Resource, + ResponseType = Options.ResponseType, + Prompt = properties.GetParameter(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt, + Scope = string.Join(" ", properties.GetParameter>(OpenIdConnectParameterNames.Scope) ?? Options.Scope), + }; + + // Add the 'max_age' parameter to the authentication request if MaxAge is not null. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + var maxAge = properties.GetParameter(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge; + if (maxAge.HasValue) + { + message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds)) + .ToString(CultureInfo.InvariantCulture); + } + + // Omitting the response_mode parameter when it already corresponds to the default + // response_mode used for the specified response_type is recommended by the specifications. + // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) || + !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal)) + { + message.ResponseMode = Options.ResponseMode; + } + + if (Options.ProtocolValidator.RequireNonce) + { + message.Nonce = Options.ProtocolValidator.GenerateNonce(); + WriteNonceCookie(message.Nonce); + } + + GenerateCorrelationId(properties); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = message + }; + + await Events.RedirectToIdentityProvider(redirectContext); + if (redirectContext.Handled) + { + Logger.RedirectToIdentityProviderHandledResponse(); + return; + } + + message = redirectContext.ProtocolMessage; + + if (!string.IsNullOrEmpty(message.State)) + { + properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State; + } + + // When redeeming a 'code' for an AccessToken, this value is needed + properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri); + + message.State = Options.StateDataFormat.Protect(properties); + + if (string.IsNullOrEmpty(message.IssuerAddress)) + { + throw new InvalidOperationException( + "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid."); + } + + if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) + { + var redirectUri = message.CreateAuthenticationRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.InvalidAuthenticationRequestUrl(redirectUri); + } + + Response.Redirect(redirectUri); + return; + } + else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) + { + var content = message.BuildFormPost(); + var buffer = Encoding.UTF8.GetBytes(content); + + Response.ContentLength = buffer.Length; + Response.ContentType = "text/html;charset=UTF-8"; + + // Emit Cache-Control=no-cache to prevent client caching. + Response.Headers[HeaderNames.CacheControl] = "no-cache"; + Response.Headers[HeaderNames.Pragma] = "no-cache"; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; + + await Response.Body.WriteAsync(buffer, 0, buffer.Length); + return; + } + + throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); + } + + /// + /// Invoked to process incoming OpenIdConnect messages. + /// + /// An . + protected override async Task HandleRemoteAuthenticateAsync() + { + Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName); + + OpenIdConnectMessage authorizationResponse = null; + + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + + // response_mode=query (explicit or not) and a response_type containing id_token + // or token are not considered as a safe combination and MUST be rejected. + // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security + if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken)) + { + if (Options.SkipUnrecognizedRequests) + { + // Not for us? + return HandleRequestResult.SkipHandler(); + } + return HandleRequestResult.Fail("An OpenID Connect response cannot contain an " + + "identity token or an access token when using response_mode=query"); + } + } + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(Request.ContentType) + // May have media/type; charset=utf-8, allow partial match. + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.Body.CanRead) + { + var form = await Request.ReadFormAsync(); + authorizationResponse = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + if (authorizationResponse == null) + { + if (Options.SkipUnrecognizedRequests) + { + // Not for us? + return HandleRequestResult.SkipHandler(); + } + return HandleRequestResult.Fail("No message."); + } + + AuthenticationProperties properties = null; + try + { + properties = ReadPropertiesAndClearState(authorizationResponse); + + var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties); + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + authorizationResponse = messageReceivedContext.ProtocolMessage; + properties = messageReceivedContext.Properties; + + if (properties == null) + { + // Fail if state is missing, it's required for the correlation id. + if (string.IsNullOrEmpty(authorizationResponse.State)) + { + // This wasn't a valid OIDC message, it may not have been intended for us. + Logger.NullOrEmptyAuthorizationResponseState(); + if (Options.SkipUnrecognizedRequests) + { + return HandleRequestResult.SkipHandler(); + } + return HandleRequestResult.Fail(Resources.MessageStateIsNullOrEmpty); + } + + properties = ReadPropertiesAndClearState(authorizationResponse); + } + + if (properties == null) + { + Logger.UnableToReadAuthorizationResponseState(); + if (Options.SkipUnrecognizedRequests) + { + // Not for us? + return HandleRequestResult.SkipHandler(); + } + + // if state exists and we failed to 'unprotect' this is not a message we should process. + return HandleRequestResult.Fail(Resources.MessageStateIsInvalid); + } + + if (!ValidateCorrelationId(properties)) + { + return HandleRequestResult.Fail("Correlation failed.", properties); + } + + // if any of the error fields are set, throw error null + if (!string.IsNullOrEmpty(authorizationResponse.Error)) + { + return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties); + } + + if (_configuration == null && Options.ConfigurationManager != null) + { + Logger.UpdatingConfiguration(); + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + PopulateSessionProperties(authorizationResponse, properties); + + ClaimsPrincipal user = null; + JwtSecurityToken jwt = null; + string nonce = null; + var validationParameters = Options.TokenValidationParameters.Clone(); + + // Hybrid or Implicit flow + if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) + { + Logger.ReceivedIdToken(); + user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + + nonce = jwt.Payload.Nonce; + if (!string.IsNullOrEmpty(nonce)) + { + nonce = ReadNonceCookie(nonce); + } + + var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, user, properties, jwt, nonce); + if (tokenValidatedContext.Result != null) + { + return tokenValidatedContext.Result; + } + authorizationResponse = tokenValidatedContext.ProtocolMessage; + user = tokenValidatedContext.Principal; + properties = tokenValidatedContext.Properties; + jwt = tokenValidatedContext.SecurityToken; + nonce = tokenValidatedContext.Nonce; + } + + Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() + { + ClientId = Options.ClientId, + ProtocolMessage = authorizationResponse, + ValidatedIdToken = jwt, + Nonce = nonce + }); + + OpenIdConnectMessage tokenEndpointResponse = null; + + // Authorization Code or Hybrid flow + if (!string.IsNullOrEmpty(authorizationResponse.Code)) + { + var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties, jwt); + if (authorizationCodeReceivedContext.Result != null) + { + return authorizationCodeReceivedContext.Result; + } + authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage; + user = authorizationCodeReceivedContext.Principal; + properties = authorizationCodeReceivedContext.Properties; + var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest; + // If the developer redeemed the code themselves... + tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse; + jwt = authorizationCodeReceivedContext.JwtSecurityToken; + + if (!authorizationCodeReceivedContext.HandledCodeRedemption) + { + tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest); + } + + var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, user, properties); + if (tokenResponseReceivedContext.Result != null) + { + return tokenResponseReceivedContext.Result; + } + + authorizationResponse = tokenResponseReceivedContext.ProtocolMessage; + tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse; + user = tokenResponseReceivedContext.Principal; + properties = tokenResponseReceivedContext.Properties; + + // no need to validate signature when token is received using "code flow" as per spec + // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. + validationParameters.RequireSignedTokens = false; + + // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. + // And we'll want to validate the new JWT in ValidateTokenResponse. + var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt); + + // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation. + if (user == null) + { + nonce = tokenEndpointJwt.Payload.Nonce; + if (!string.IsNullOrEmpty(nonce)) + { + nonce = ReadNonceCookie(nonce); + } + + var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, tokenEndpointUser, properties, tokenEndpointJwt, nonce); + if (tokenValidatedContext.Result != null) + { + return tokenValidatedContext.Result; + } + authorizationResponse = tokenValidatedContext.ProtocolMessage; + tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse; + user = tokenValidatedContext.Principal; + properties = tokenValidatedContext.Properties; + jwt = tokenValidatedContext.SecurityToken; + nonce = tokenValidatedContext.Nonce; + } + else + { + if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal)) + { + throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints."); + } + + jwt = tokenEndpointJwt; + } + + // Validate the token response if it wasn't provided manually + if (!authorizationCodeReceivedContext.HandledCodeRedemption) + { + Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() + { + ClientId = Options.ClientId, + ProtocolMessage = tokenEndpointResponse, + ValidatedIdToken = jwt, + Nonce = nonce + }); + } + } + + if (Options.SaveTokens) + { + SaveTokens(properties, tokenEndpointResponse ?? authorizationResponse); + } + + if (Options.GetClaimsFromUserInfoEndpoint) + { + return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, user, properties); + } + else + { + var identity = (ClaimsIdentity)user.Identity; + foreach (var action in Options.ClaimActions) + { + action.Run(null, identity, ClaimsIssuer); + } + } + + return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name)); + } + catch (Exception exception) + { + Logger.ExceptionProcessingMessage(exception); + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. + if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException) + { + if (Options.ConfigurationManager != null) + { + Logger.ConfigurationManagerRequestRefreshCalled(); + Options.ConfigurationManager.RequestRefresh(); + } + } + + var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + return HandleRequestResult.Fail(exception, properties); + } + } + + private AuthenticationProperties ReadPropertiesAndClearState(OpenIdConnectMessage message) + { + AuthenticationProperties properties = null; + if (!string.IsNullOrEmpty(message.State)) + { + properties = Options.StateDataFormat.Unprotect(message.State); + + if (properties != null) + { + // If properties can be decoded from state, clear the message state. + properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out var userstate); + message.State = userstate; + } + } + return properties; + } + + private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties) + { + if (!string.IsNullOrEmpty(message.SessionState)) + { + properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; + } + + if (!string.IsNullOrEmpty(_configuration.CheckSessionIframe)) + { + properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; + } + } + + /// + /// Redeems the authorization code for tokens at the token endpoint. + /// + /// The request that will be sent to the token endpoint and is available for customization. + /// OpenIdConnect message that has tokens inside it. + protected virtual async Task RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest) + { + Logger.RedeemingCodeForTokens(); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); + requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters); + + var responseMessage = await Backchannel.SendAsync(requestMessage); + + var contentMediaType = responseMessage.Content.Headers.ContentType?.MediaType; + if (string.IsNullOrEmpty(contentMediaType)) + { + Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type header is missing."); + } + else if (!string.Equals(contentMediaType, "application/json", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type {responseMessage.Content.Headers.ContentType}."); + } + + // Error handling: + // 1. If the response body can't be parsed as json, throws. + // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed, + // pass the error information from body to the exception. + OpenIdConnectMessage message; + try + { + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + message = new OpenIdConnectMessage(responseContent); + } + catch (Exception ex) + { + throw new OpenIdConnectProtocolException($"Failed to parse token response body as JSON. Status Code: {(int)responseMessage.StatusCode}. Content-Type: {responseMessage.Content.Headers.ContentType}", ex); + } + + if (!responseMessage.IsSuccessStatusCode) + { + throw CreateOpenIdConnectProtocolException(message, responseMessage); + } + + return message; + } + + /// + /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity. + /// + /// message that is being processed + /// The . + /// The claims principal and identities. + /// The authentication properties. + /// which is used to determine if the remote authentication was successful. + protected virtual async Task GetUserInformationAsync( + OpenIdConnectMessage message, JwtSecurityToken jwt, + ClaimsPrincipal principal, AuthenticationProperties properties) + { + var userInfoEndpoint = _configuration?.UserInfoEndpoint; + + if (string.IsNullOrEmpty(userInfoEndpoint)) + { + Logger.UserInfoEndpointNotSet(); + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); + } + if (string.IsNullOrEmpty(message.AccessToken)) + { + Logger.AccessTokenNotAvailable(); + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); + } + Logger.RetrievingClaims(); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken); + var responseMessage = await Backchannel.SendAsync(requestMessage); + responseMessage.EnsureSuccessStatusCode(); + var userInfoResponse = await responseMessage.Content.ReadAsStringAsync(); + + JObject user; + var contentType = responseMessage.Content.Headers.ContentType; + if (contentType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase)) + { + user = JObject.Parse(userInfoResponse); + } + else if (contentType.MediaType.Equals("application/jwt", StringComparison.OrdinalIgnoreCase)) + { + var userInfoEndpointJwt = new JwtSecurityToken(userInfoResponse); + user = JObject.FromObject(userInfoEndpointJwt.Payload); + } + else + { + return HandleRequestResult.Fail("Unknown response type: " + contentType.MediaType, properties); + } + + var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(principal, properties, message, user); + if (userInformationReceivedContext.Result != null) + { + return userInformationReceivedContext.Result; + } + principal = userInformationReceivedContext.Principal; + properties = userInformationReceivedContext.Properties; + user = userInformationReceivedContext.User; + + Options.ProtocolValidator.ValidateUserInfoResponse(new OpenIdConnectProtocolValidationContext() + { + UserInfoEndpointResponse = userInfoResponse, + ValidatedIdToken = jwt, + }); + + var identity = (ClaimsIdentity)principal.Identity; + + foreach (var action in Options.ClaimActions) + { + action.Run(user, identity, ClaimsIssuer); + } + + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); + } + + /// + /// Save the tokens contained in the in the . + /// + /// The in which tokens are saved. + /// The OpenID Connect response. + private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) + { + var tokens = new List(); + + if (!string.IsNullOrEmpty(message.AccessToken)) + { + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken }); + } + + if (!string.IsNullOrEmpty(message.IdToken)) + { + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken }); + } + + if (!string.IsNullOrEmpty(message.RefreshToken)) + { + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken }); + } + + if (!string.IsNullOrEmpty(message.TokenType)) + { + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType }); + } + + if (!string.IsNullOrEmpty(message.ExpiresIn)) + { + if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) + { + var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value); + // https://www.w3.org/TR/xmlschema-2/#dateTime + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx + tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }); + } + } + + properties.StoreTokens(tokens); + } + + /// + /// Adds the nonce to . + /// + /// the nonce to remember. + /// of is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)' of . + /// The value of the cookie is: "N". + private void WriteNonceCookie(string nonce) + { + if (string.IsNullOrEmpty(nonce)) + { + throw new ArgumentNullException(nameof(nonce)); + } + + var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow); + + Response.Cookies.Append( + Options.NonceCookie.Name + Options.StringDataFormat.Protect(nonce), + NonceProperty, + cookieOptions); + } + + /// + /// Searches for a matching nonce. + /// + /// the nonce that we are looking for. + /// echos 'nonce' if a cookie is found that matches, null otherwise. + /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. + /// of is used to obtain the actual 'nonce'. If the nonce is found, then of is called. + private string ReadNonceCookie(string nonce) + { + if (nonce == null) + { + return null; + } + + foreach (var nonceKey in Request.Cookies.Keys) + { + if (nonceKey.StartsWith(Options.NonceCookie.Name)) + { + try + { + var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(Options.NonceCookie.Name.Length, nonceKey.Length - Options.NonceCookie.Name.Length)); + if (nonceDecodedValue == nonce) + { + var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow); + Response.Cookies.Delete(nonceKey, cookieOptions); + return nonce; + } + } + catch (Exception ex) + { + Logger.UnableToProtectNonceCookie(ex); + } + } + } + + return null; + } + + private AuthenticationProperties GetPropertiesFromState(string state) + { + // assume a well formed query string: OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d> + var startIndex = 0; + if (string.IsNullOrEmpty(state) || (startIndex = state.IndexOf(OpenIdConnectDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1) + { + return null; + } + + var authenticationIndex = startIndex + OpenIdConnectDefaults.AuthenticationPropertiesKey.Length; + if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=') + { + return null; + } + + // scan rest of string looking for '&' + authenticationIndex++; + var endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal); + + // -1 => no other parameters are after the AuthenticationPropertiesKey + if (endIndex == -1) + { + return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' '))); + } + else + { + return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' '))); + } + } + + private async Task RunMessageReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties) + { + Logger.MessageReceived(message.BuildRedirectUrl()); + var context = new MessageReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = message, + }; + + await Events.MessageReceived(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.MessageReceivedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.MessageReceivedContextSkipped(); + } + } + + return context; + } + + private async Task RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, string nonce) + { + var context = new TokenValidatedContext(Context, Scheme, Options, user, properties) + { + ProtocolMessage = authorizationResponse, + TokenEndpointResponse = tokenEndpointResponse, + SecurityToken = jwt, + Nonce = nonce, + }; + + await Events.TokenValidated(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.TokenValidatedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.TokenValidatedSkipped(); + } + } + + return context; + } + + private async Task RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt) + { + Logger.AuthorizationCodeReceived(); + + var tokenEndpointRequest = new OpenIdConnectMessage() + { + ClientId = Options.ClientId, + ClientSecret = Options.ClientSecret, + Code = authorizationResponse.Code, + GrantType = OpenIdConnectGrantTypes.AuthorizationCode, + EnableTelemetryParameters = !Options.DisableTelemetry, + RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey] + }; + + var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = authorizationResponse, + TokenEndpointRequest = tokenEndpointRequest, + Principal = user, + JwtSecurityToken = jwt, + Backchannel = Backchannel + }; + + await Events.AuthorizationCodeReceived(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.AuthorizationCodeReceivedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.AuthorizationCodeReceivedContextSkipped(); + } + } + + return context; + } + + private async Task RunTokenResponseReceivedEventAsync( + OpenIdConnectMessage message, + OpenIdConnectMessage tokenEndpointResponse, + ClaimsPrincipal user, + AuthenticationProperties properties) + { + Logger.TokenResponseReceived(); + var context = new TokenResponseReceivedContext(Context, Scheme, Options, user, properties) + { + ProtocolMessage = message, + TokenEndpointResponse = tokenEndpointResponse, + }; + + await Events.TokenResponseReceived(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.TokenResponseReceivedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.TokenResponseReceivedSkipped(); + } + } + + return context; + } + + private async Task RunUserInformationReceivedEventAsync(ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage message, JObject user) + { + Logger.UserInformationReceived(user.ToString()); + + var context = new UserInformationReceivedContext(Context, Scheme, Options, principal, properties) + { + ProtocolMessage = message, + User = user, + }; + + await Events.UserInformationReceived(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.UserInformationReceivedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.UserInformationReceivedSkipped(); + } + } + + return context; + } + + private async Task RunAuthenticationFailedEventAsync(OpenIdConnectMessage message, Exception exception) + { + var context = new AuthenticationFailedContext(Context, Scheme, Options) + { + ProtocolMessage = message, + Exception = exception + }; + + await Events.AuthenticationFailed(context); + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.AuthenticationFailedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.AuthenticationFailedContextSkipped(); + } + } + + return context; + } + + // Note this modifies properties if Options.UseTokenLifetime + private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) + { + if (!Options.SecurityTokenValidator.CanReadToken(idToken)) + { + Logger.UnableToReadIdToken(idToken); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + } + + if (_configuration != null) + { + var issuer = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer; + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; + } + + var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken); + jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + Logger.InvalidSecurityTokenType(validatedToken?.GetType().ToString()); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); + } + + if (validatedToken == null) + { + Logger.UnableToValidateIdToken(idToken); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + } + + if (Options.UseTokenLifetime) + { + var issued = validatedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + properties.IssuedUtc = issued; + } + + var expires = validatedToken.ValidTo; + if (expires != DateTime.MinValue) + { + properties.ExpiresUtc = expires; + } + } + + return principal; + } + + /// + /// Build a redirect path if the given path is a relative path. + /// + private string BuildRedirectUriIfRelative(string uri) + { + if (string.IsNullOrEmpty(uri)) + { + return uri; + } + + if (!uri.StartsWith("/", StringComparison.Ordinal)) + { + return uri; + } + + return BuildRedirectUri(uri); + } + + private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message, HttpResponseMessage response) + { + var description = message.ErrorDescription ?? "error_description is null"; + var errorUri = message.ErrorUri ?? "error_uri is null"; + + if (response != null) + { + Logger.ResponseErrorWithStatusCode(message.Error, description, errorUri, (int)response.StatusCode); + } + else + { + Logger.ResponseError(message.Error, description, errorUri); + } + + return new OpenIdConnectProtocolException(string.Format( + CultureInfo.InvariantCulture, + Resources.MessageContainsError, + message.Error, + description, + errorUri)); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs new file mode 100644 index 0000000000..cbf6e8eab6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -0,0 +1,326 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Authentication.Internal; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Configuration options for + /// + public class OpenIdConnectOptions : RemoteAuthenticationOptions + { + private CookieBuilder _nonceCookieBuilder; + + /// + /// Initializes a new + /// + /// + /// Defaults: + /// AddNonceToRequest: true. + /// BackchannelTimeout: 1 minute. + /// ProtocolValidator: new . + /// RefreshOnIssuerKeyNotFound: true + /// ResponseType: + /// Scope: . + /// TokenValidationParameters: new with AuthenticationScheme = authenticationScheme. + /// UseTokenLifetime: false. + /// + public OpenIdConnectOptions() + { + CallbackPath = new PathString("/signin-oidc"); + SignedOutCallbackPath = new PathString("/signout-callback-oidc"); + RemoteSignOutPath = new PathString("/signout-oidc"); + + Events = new OpenIdConnectEvents(); + Scope.Add("openid"); + Scope.Add("profile"); + + ClaimActions.DeleteClaim("nonce"); + ClaimActions.DeleteClaim("aud"); + ClaimActions.DeleteClaim("azp"); + ClaimActions.DeleteClaim("acr"); + ClaimActions.DeleteClaim("amr"); + ClaimActions.DeleteClaim("iss"); + ClaimActions.DeleteClaim("iat"); + ClaimActions.DeleteClaim("nbf"); + ClaimActions.DeleteClaim("exp"); + ClaimActions.DeleteClaim("at_hash"); + ClaimActions.DeleteClaim("c_hash"); + ClaimActions.DeleteClaim("auth_time"); + ClaimActions.DeleteClaim("ipaddr"); + ClaimActions.DeleteClaim("platf"); + ClaimActions.DeleteClaim("ver"); + + // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + ClaimActions.MapUniqueJsonKey("sub", "sub"); + ClaimActions.MapUniqueJsonKey("name", "name"); + ClaimActions.MapUniqueJsonKey("given_name", "given_name"); + ClaimActions.MapUniqueJsonKey("family_name", "family_name"); + ClaimActions.MapUniqueJsonKey("profile", "profile"); + ClaimActions.MapUniqueJsonKey("email", "email"); + + _nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this) + { + Name = OpenIdConnectDefaults.CookieNoncePrefix, + HttpOnly = true, + SameSite = SameSiteMode.None, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + + if (MaxAge.HasValue && MaxAge.Value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(MaxAge), MaxAge.Value, "The value must not be a negative TimeSpan."); + } + + if (string.IsNullOrEmpty(ClientId)) + { + throw new ArgumentException("Options.ClientId must be provided", nameof(ClientId)); + } + + if (!CallbackPath.HasValue) + { + throw new ArgumentException("Options.CallbackPath must be provided.", nameof(CallbackPath)); + } + + if (ConfigurationManager == null) + { + throw new InvalidOperationException($"Provide {nameof(Authority)}, {nameof(MetadataAddress)}, " + + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(OpenIdConnectOptions)}"); + } + } + + /// + /// Gets or sets the Authority to use when making OpenIdConnect calls. + /// + public string Authority { get; set; } + + /// + /// Gets or sets the 'client_id'. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the 'client_secret'. + /// + public string ClientSecret { get; set; } + + /// + /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties + /// will not be used. This information should not be updated during request processing. + /// + public OpenIdConnectConfiguration Configuration { get; set; } + + /// + /// Responsible for retrieving, caching, and refreshing the configuration from metadata. + /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. + /// + public IConfigurationManager ConfigurationManager { get; set; } + + /// + /// Boolean to set whether the handler should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint. + /// The default is 'false'. + /// + public bool GetClaimsFromUserInfoEndpoint { get; set; } + + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + + /// + /// Gets or sets if HTTPS is required for the metadata address or authority. + /// The default is true. This should be disabled only in development environments. + /// + public bool RequireHttpsMetadata { get; set; } = true; + + /// + /// Gets or sets the discovery endpoint for obtaining metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Gets or sets the to notify when processing OpenIdConnect messages. + /// + public new OpenIdConnectEvents Events + { + get => (OpenIdConnectEvents)base.Events; + set => base.Events = value; + } + + /// + /// Gets or sets the 'max_age'. If set the 'max_age' parameter will be sent with the authentication request. If the identity + /// provider has not actively authenticated the user within the length of time specified, the user will be prompted to + /// re-authenticate. By default no max_age is specified. + /// + public TimeSpan? MaxAge { get; set; } + + /// + /// Gets or sets the that is used to ensure that the 'id_token' received + /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + /// + /// if 'value' is null. + public OpenIdConnectProtocolValidator ProtocolValidator { get; set; } = new OpenIdConnectProtocolValidator() + { + RequireStateValidation = false, + NonceLifetime = TimeSpan.FromMinutes(15) + }; + + /// + /// The request path within the application's base path where the user agent will be returned after sign out from the identity provider. + /// See post_logout_redirect_uri from http://openid.net/specs/openid-connect-session-1_0.html#RedirectionAfterLogout. + /// + public PathString SignedOutCallbackPath { get; set; } + + /// + /// The uri where the user agent will be redirected to after application is signed out from the identity provider. + /// The redirect will happen after the SignedOutCallbackPath is invoked. + /// + /// This URI can be out of the application's domain. By default it points to the root. + public string SignedOutRedirectUri { get; set; } = "/"; + + /// + /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic + /// recovery in the event of a signature key rollover. This is enabled by default. + /// + public bool RefreshOnIssuerKeyNotFound { get; set; } = true; + + /// + /// Gets or sets the method used to redirect the user agent to the identity provider. + /// + public OpenIdConnectRedirectBehavior AuthenticationMethod { get; set; } = OpenIdConnectRedirectBehavior.RedirectGet; + + /// + /// Gets or sets the 'resource'. + /// + public string Resource { get; set; } + + /// + /// Gets or sets the 'response_mode'. + /// + public string ResponseMode { get; set; } = OpenIdConnectResponseMode.FormPost; + + /// + /// Gets or sets the 'response_type'. + /// + public string ResponseType { get; set; } = OpenIdConnectResponseType.IdToken; + + /// + /// Gets or sets the 'prompt'. + /// + public string Prompt { get; set; } + + /// + /// Gets the list of permissions to request. + /// + public ICollection Scope { get; } = new HashSet(); + + /// + /// Requests received on this path will cause the handler to invoke SignOut using the SignInScheme. + /// + public PathString RemoteSignOutPath { get; set; } + + /// + /// The Authentication Scheme to use with SignOut on the SignOutPath. SignInScheme will be used if this + /// is not set. + /// + public string SignOutScheme { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the handler. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the type used to secure strings used by the handler. + /// + public ISecureDataFormat StringDataFormat { get; set; } + + /// + /// Gets or sets the used to validate identity tokens. + /// + public ISecurityTokenValidator SecurityTokenValidator { get; set; } = new JwtSecurityTokenHandler(); + + /// + /// Gets or sets the parameters used to validate identity tokens. + /// + /// Contains the types and definitions required for validating a token. + public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); + + /// + /// Indicates that the authentication session lifetime (e.g. cookies) should match that of the authentication token. + /// If the token does not provide lifetime information then normal session lifetimes will be used. + /// This is disabled by default. + /// + public bool UseTokenLifetime { get; set; } + + /// + /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass + /// requests through that do not contain OpenIdConnect authentication responses. Disabling this and setting the + /// CallbackPath to a dedicated endpoint may provide better error handling. + /// This is disabled by default. + /// + public bool SkipUnrecognizedRequests { get; set; } = false; + + /// + /// Indicates whether telemetry should be disabled. When this feature is enabled, + /// the assembly version of the Microsoft IdentityModel packages is sent to the + /// remote OpenID Connect provider as an authorization/logout request parameter. + /// + public bool DisableTelemetry { get; set; } + + /// + /// Determines the settings used to create the nonce cookie before the + /// cookie gets added to the response. + /// + /// + /// The value of is treated as the prefix to the cookie name, and defaults to . + /// + public CookieBuilder NonceCookie + { + get => _nonceCookieBuilder; + set => _nonceCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + private class OpenIdConnectNonceCookieBuilder : RequestPathBaseCookieBuilder + { + private readonly OpenIdConnectOptions _options; + + public OpenIdConnectNonceCookieBuilder(OpenIdConnectOptions oidcOptions) + { + _options = oidcOptions; + } + + protected override string AdditionalPath => _options.CallbackPath; + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var cookieOptions = base.Build(context, expiresFrom); + + if (!Expiration.HasValue || !cookieOptions.Expires.HasValue) + { + cookieOptions.Expires = expiresFrom.Add(_options.ProtocolValidator.NonceLifetime); + } + + return cookieOptions; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs new file mode 100644 index 0000000000..b79f1d1edf --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Used to setup defaults for all . + /// + public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, OpenIdConnectOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.SignOutScheme)) + { + options.SignOutScheme = options.SignInScheme; + } + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(OpenIdConnectHandler).FullName, name, "v1"); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (options.StringDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(OpenIdConnectHandler).FullName, + typeof(string).FullName, + name, + "v1"); + + options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector); + } + + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.ClientId)) + { + options.TokenValidationParameters.ValidAudience = options.ClientId; + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler"); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority))) + { + if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority)) + { + options.MetadataAddress = options.Authority; + if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + options.MetadataAddress += "/"; + } + + options.MetadataAddress += ".well-known/openid-configuration"; + } + + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + + private class StringSerializer : IDataSerializer + { + public string Deserialize(byte[] data) + { + return Encoding.UTF8.GetString(data); + } + + public byte[] Serialize(string model) + { + return Encoding.UTF8.GetBytes(model); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs new file mode 100644 index 0000000000..2f419df18a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs @@ -0,0 +1,21 @@ +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Lists the different authentication methods used to + /// redirect the user agent to the identity provider. + /// + public enum OpenIdConnectRedirectBehavior + { + /// + /// Emits a 302 response to redirect the user agent to + /// the OpenID Connect provider using a GET request. + /// + RedirectGet = 0, + + /// + /// Emits an HTML form to redirect the user agent to + /// the OpenID Connect provider using a POST request. + /// + FormPost = 1 + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..753373ece4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs @@ -0,0 +1,114 @@ +// +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.OpenIdConnect.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// OpenIdConnectAuthenticationHandler: message.State is null or empty. + /// + internal static string MessageStateIsNullOrEmpty + { + get => GetString("MessageStateIsNullOrEmpty"); + } + + /// + /// OpenIdConnectAuthenticationHandler: message.State is null or empty. + /// + internal static string FormatMessageStateIsNullOrEmpty() + => GetString("MessageStateIsNullOrEmpty"); + + /// + /// Unable to unprotect the message.State. + /// + internal static string MessageStateIsInvalid + { + get => GetString("MessageStateIsInvalid"); + } + + /// + /// Unable to unprotect the message.State. + /// + internal static string FormatMessageStateIsInvalid() + => GetString("MessageStateIsInvalid"); + + /// + /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. + /// + internal static string MessageContainsError + { + get => GetString("MessageContainsError"); + } + + /// + /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. + /// + internal static string FormatMessageContainsError(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("MessageContainsError"), p0, p1, p2); + + /// + /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. + /// + internal static string ValidatedSecurityTokenNotJwt + { + get => GetString("ValidatedSecurityTokenNotJwt"); + } + + /// + /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. + /// + internal static string FormatValidatedSecurityTokenNotJwt(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ValidatedSecurityTokenNotJwt"), p0); + + /// + /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." + /// + internal static string UnableToValidateToken + { + get => GetString("UnableToValidateToken"); + } + + /// + /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." + /// + internal static string FormatUnableToValidateToken(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("UnableToValidateToken"), p0); + + /// + /// Cannot process the message. Both id_token and code are missing. + /// + internal static string IdTokenCodeMissing + { + get => GetString("IdTokenCodeMissing"); + } + + /// + /// Cannot process the message. Both id_token and code are missing. + /// + internal static string FormatIdTokenCodeMissing() + => GetString("IdTokenCodeMissing"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx new file mode 100644 index 0000000000..7f790fef43 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OpenIdConnectAuthenticationHandler: message.State is null or empty. + + + Unable to unprotect the message.State. + + + Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. + + + The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. + + + Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." + + + Cannot process the message. Both id_token and code are missing. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json new file mode 100644 index 0000000000..d5c10d18db --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json @@ -0,0 +1,2452 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OpenIdConnect, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.OpenIdConnectAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOpenIdConnectAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOpenIdConnectAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionUniqueExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapUniqueJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapUniqueJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_JwtSecurityToken", + "Parameters": [], + "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_JwtSecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointRequest", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointRequest", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandledCodeRedemption", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [ + { + "Name": "accessToken", + "Type": "System.String" + }, + { + "Name": "idToken", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [ + { + "Name": "tokenEndpointResponse", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnAuthorizationCodeReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthorizationCodeReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProvider", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProvider", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProviderForSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProviderForSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSignedOutCallbackRedirect", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSignedOutCallbackRedirect", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRemoteSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenResponseReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenResponseReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnUserInformationReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnUserInformationReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizationCodeReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProvider", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProviderForSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignedOutCallbackRedirect", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenResponseReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UserInformationReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Nonce", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Nonce", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxAgeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PromptKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "AuthenticationPropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "CookieNoncePrefix", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RedirectUriForCodePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserstatePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"OpenIdConnect\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HtmlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.HtmlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteSignOutAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutCallbackAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedeemAuthorizationCodeAsync", + "Parameters": [ + { + "Name": "tokenEndpointRequest", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetUserInformationAsync", + "Parameters": [ + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + }, + { + "Name": "jwt", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "htmlEncoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authority", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Authority", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_GetClaimsFromUserInfoEndpoint", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_GetClaimsFromUserInfoEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ProtocolValidator", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolValidator", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignedOutCallbackPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignedOutCallbackPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignedOutRedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignedOutRedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationMethod", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationMethod", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Resource", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Resource", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ResponseMode", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ResponseMode", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ResponseType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ResponseType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteSignOutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteSignOutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StringDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StringDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenValidator", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityTokenValidator", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UseTokenLifetime", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UseTokenLifetime", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SkipUnrecognizedRequests", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SkipUnrecognizedRequests", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisableTelemetry", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisableTelemetry", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NonceCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NonceCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "RedirectGet", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "FormPost", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims.UniqueJsonKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs new file mode 100644 index 0000000000..67f28d5297 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Contains information about the login session as well as the user . + /// + public class TwitterCreatingTicketContext : ResultContext + { + /// + /// Initializes a + /// + /// The HTTP environment + /// The scheme data + /// The options for Twitter + /// The . + /// The . + /// Twitter user ID + /// Twitter screen name + /// Twitter access token + /// Twitter access token secret + /// User details + public TwitterCreatingTicketContext( + HttpContext context, + AuthenticationScheme scheme, + TwitterOptions options, + ClaimsPrincipal principal, + AuthenticationProperties properties, + string userId, + string screenName, + string accessToken, + string accessTokenSecret, + JObject user) + : base(context, scheme, options) + { + UserId = userId; + ScreenName = screenName; + AccessToken = accessToken; + AccessTokenSecret = accessTokenSecret; + User = user ?? new JObject(); + Principal = principal; + Properties = properties; + } + + /// + /// Gets the Twitter user ID + /// + public string UserId { get; } + + /// + /// Gets the Twitter screen name + /// + public string ScreenName { get; } + + /// + /// Gets the Twitter access token + /// + public string AccessToken { get; } + + /// + /// Gets the Twitter access token secret + /// + public string AccessTokenSecret { get; } + + /// + /// Gets the JSON-serialized user or an empty + /// if it is not available. + /// + public JObject User { get; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs new file mode 100644 index 0000000000..744c48c5fc --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Default implementation. + /// + public class TwitterEvents : RemoteAuthenticationEvents + { + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnCreatingTicket { get; set; } = context => Task.CompletedTask; + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Func, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context => + { + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; + + /// + /// Invoked whenever Twitter successfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task CreatingTicket(TwitterCreatingTicketContext context) => OnCreatingTicket(context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter handler + /// + /// Contains redirect URI and of the challenge + public virtual Task RedirectToAuthorizationEndpoint(RedirectContext context) => OnRedirectToAuthorizationEndpoint(context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs new file mode 100644 index 0000000000..2a2cd5da79 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _obtainRequestToken; + private static Action _obtainAccessToken; + private static Action _retrieveUserDetails; + + static LoggingExtensions() + { + _obtainRequestToken = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Debug, + formatString: "ObtainRequestToken"); + _obtainAccessToken = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Debug, + formatString: "ObtainAccessToken"); + _retrieveUserDetails = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Debug, + formatString: "RetrieveUserDetails"); + + } + + public static void ObtainAccessToken(this ILogger logger) + { + _obtainAccessToken(logger, null); + } + + public static void ObtainRequestToken(this ILogger logger) + { + _obtainRequestToken(logger, null); + } + + public static void RetrieveUserDetails(this ILogger logger) + { + _retrieveUserDetails(logger, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs new file mode 100644 index 0000000000..550163bec8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// The Twitter access token retrieved from the access token endpoint. + /// + public class AccessToken : RequestToken + { + /// + /// Gets or sets the Twitter User ID. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the Twitter screen name. + /// + public string ScreenName { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs new file mode 100644 index 0000000000..04c334e3d3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http.Authentication; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// The Twitter request token obtained from the request token endpoint. + /// + public class RequestToken + { + /// + /// Gets or sets the Twitter request token. + /// + public string Token { get; set; } + + /// + /// Gets or sets the Twitter token secret. + /// + public string TokenSecret { get; set; } + + public bool CallbackConfirmed { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs new file mode 100644 index 0000000000..88b10d3d60 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Http.Authentication; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Serializes and deserializes Twitter request and access tokens so that they can be used by other application components. + /// + public class RequestTokenSerializer : IDataSerializer + { + private const int FormatVersion = 1; + + /// + /// Serialize a request token. + /// + /// The token to serialize + /// A byte array containing the serialized token + public virtual byte[] Serialize(RequestToken model) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, model); + writer.Flush(); + return memory.ToArray(); + } + } + } + + /// + /// Deserializes a request token. + /// + /// A byte array containing the serialized token + /// The Twitter request token + public virtual RequestToken Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + /// + /// Writes a Twitter request token as a series of bytes. Used by the method. + /// + /// The writer to use in writing the token + /// The token to write + public static void Write(BinaryWriter writer, RequestToken token) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + writer.Write(FormatVersion); + writer.Write(token.Token); + writer.Write(token.TokenSecret); + writer.Write(token.CallbackConfirmed); + PropertiesSerializer.Default.Write(writer, token.Properties); + } + + /// + /// Reads a Twitter request token from a series of bytes. Used by the method. + /// + /// The reader to use in reading the token bytes + /// The token + public static RequestToken Read(BinaryReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + string token = reader.ReadString(); + string tokenSecret = reader.ReadString(); + bool callbackConfirmed = reader.ReadBoolean(); + AuthenticationProperties properties = PropertiesSerializer.Default.Read(reader); + if (properties == null) + { + return null; + } + + return new RequestToken { Token = token, TokenSecret = tokenSecret, CallbackConfirmed = callbackConfirmed, Properties = properties }; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj new file mode 100644 index 0000000000..f720d08f04 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support Twitter's OAuth 1.0 authentication workflow. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..2eabfff298 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.Twitter.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs new file mode 100644 index 0000000000..36e1111da6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication.Twitter; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add Twitter authentication capabilities to an HTTP application pipeline. + /// + public static class TwitterAppBuilderExtensions + { + /// + /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + + /// + /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. + /// + /// The to add the handler to. + /// An action delegate to configure the provided . + /// A reference to this instance after the operation has completed. + [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] + public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app, TwitterOptions options) + { + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs new file mode 100644 index 0000000000..a39a3f0367 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + public static class TwitterDefaults + { + public const string AuthenticationScheme = "Twitter"; + + public static readonly string DisplayName = "Twitter"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs new file mode 100644 index 0000000000..7243805692 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Twitter; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class TwitterExtensions + { + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder) + => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TwitterPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs new file mode 100644 index 0000000000..670e76f7e3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs @@ -0,0 +1,371 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + public class TwitterHandler : RemoteAuthenticationHandler + { + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; + private const string AuthenticationEndpoint = "https://api.twitter.com/oauth/authenticate?oauth_token="; + private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; + + private HttpClient Backchannel => Options.Backchannel; + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new TwitterEvents Events + { + get { return (TwitterEvents)base.Events; } + set { base.Events = value; } + } + + public TwitterHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override Task CreateEventsAsync() => Task.FromResult(new TwitterEvents()); + + protected override async Task HandleRemoteAuthenticateAsync() + { + var query = Request.Query; + var protectedRequestToken = Request.Cookies[Options.StateCookie.Name]; + + var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); + + if (requestToken == null) + { + return HandleRequestResult.Fail("Invalid state cookie."); + } + + var properties = requestToken.Properties; + + // REVIEW: see which of these are really errors + + var denied = query["denied"]; + if (!StringValues.IsNullOrEmpty(denied)) + { + return HandleRequestResult.Fail("The user denied permissions.", properties); + } + + var returnedToken = query["oauth_token"]; + if (StringValues.IsNullOrEmpty(returnedToken)) + { + return HandleRequestResult.Fail("Missing oauth_token", properties); + } + + if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) + { + return HandleRequestResult.Fail("Unmatched token", properties); + } + + var oauthVerifier = query["oauth_verifier"]; + if (StringValues.IsNullOrEmpty(oauthVerifier)) + { + return HandleRequestResult.Fail("Missing or blank oauth_verifier", properties); + } + + var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow); + + Response.Cookies.Delete(Options.StateCookie.Name, cookieOptions); + + var accessToken = await ObtainAccessTokenAsync(requestToken, oauthVerifier); + + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer), + new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer), + new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer), + new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer) + }, + ClaimsIssuer); + + JObject user = null; + if (Options.RetrieveUserDetails) + { + user = await RetrieveUserDetailsAsync(accessToken, identity); + } + + if (Options.SaveTokens) + { + properties.StoreTokens(new [] { + new AuthenticationToken { Name = "access_token", Value = accessToken.Token }, + new AuthenticationToken { Name = "access_token_secret", Value = accessToken.TokenSecret } + }); + } + + return HandleRequestResult.Success(await CreateTicketAsync(identity, properties, accessToken, user)); + } + + protected virtual async Task CreateTicketAsync( + ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token, JObject user) + { + foreach (var action in Options.ClaimActions) + { + action.Run(user, identity, ClaimsIssuer); + } + + var context = new TwitterCreatingTicketContext(Context, Scheme, Options, new ClaimsPrincipal(identity), properties, token.UserId, token.ScreenName, token.Token, token.TokenSecret, user); + await Events.CreatingTicket(context); + + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + + // If CallbackConfirmed is false, this will throw + var requestToken = await ObtainRequestTokenAsync(BuildRedirectUri(Options.CallbackPath), properties); + var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + + var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow); + + Response.Cookies.Append(Options.StateCookie.Name, Options.StateDataFormat.Protect(requestToken), cookieOptions); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, twitterAuthenticationEndpoint); + await Events.RedirectToAuthorizationEndpoint(redirectContext); + } + + private async Task ObtainRequestTokenAsync(string callBackUri, AuthenticationProperties properties) + { + Logger.ObtainRequestToken(); + + var nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_callback", callBackUri }, + { "oauth_consumer_key", Options.ConsumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_version", "1.0" } + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); + } + parameterBuilder.Length--; + var parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(RequestTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); + + var signature = ComputeSignature(Options.ConsumerSecret, null, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + response.EnsureSuccessStatusCode(); + var responseText = await response.Content.ReadAsStringAsync(); + + var responseParameters = new FormCollection(new FormReader(responseText).ReadForm()); + if (!string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) + { + throw new Exception("Twitter oauth_callback_confirmed is not true."); + } + + return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; + } + + private async Task ObtainAccessTokenAsync(RequestToken token, string verifier) + { + // https://dev.twitter.com/docs/api/1/post/oauth/access_token + + Logger.ObtainAccessToken(); + + var nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_consumer_key", Options.ConsumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_token", token.Token }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_verifier", verifier }, + { "oauth_version", "1.0" }, + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); + } + parameterBuilder.Length--; + var parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(AccessTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); + + var signature = ComputeSignature(Options.ConsumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + authorizationParts.Remove("oauth_verifier"); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var formPairs = new Dictionary() + { + { "oauth_verifier", verifier }, + }; + + request.Content = new FormUrlEncodedContent(formPairs); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + + if (!response.IsSuccessStatusCode) + { + Logger.LogError("AccessToken request failed with a status code of " + response.StatusCode); + response.EnsureSuccessStatusCode(); // throw + } + + var responseText = await response.Content.ReadAsStringAsync(); + var responseParameters = new FormCollection(new FormReader(responseText).ReadForm()); + + return new AccessToken + { + Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), + TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), + UserId = Uri.UnescapeDataString(responseParameters["user_id"]), + ScreenName = Uri.UnescapeDataString(responseParameters["screen_name"]) + }; + } + + // https://dev.twitter.com/rest/reference/get/account/verify_credentials + private async Task RetrieveUserDetailsAsync(AccessToken accessToken, ClaimsIdentity identity) + { + Logger.RetrieveUserDetails(); + + var nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_consumer_key", Options.ConsumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_token", accessToken.Token }, + { "oauth_version", "1.0" } + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); + } + parameterBuilder.Length--; + var parameterString = parameterBuilder.ToString(); + + var resource_url = "https://api.twitter.com/1.1/account/verify_credentials.json"; + var resource_query = "include_email=true"; + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Get.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_url)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_query)); + canonicalizedRequestBuilder.Append("%26"); + canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); + + var signature = ComputeSignature(Options.ConsumerSecret, accessToken.TokenSecret, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Get, resource_url + "?include_email=true"); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + Logger.LogError("Email request failed with a status code of " + response.StatusCode); + response.EnsureSuccessStatusCode(); // throw + } + var responseText = await response.Content.ReadAsStringAsync(); + + var result = JObject.Parse(responseText); + + return result; + } + + private static string GenerateTimeStamp() + { + var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch; + return Convert.ToInt64(secondsSinceUnixEpocStart.TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + + private string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData) + { + using (var algorithm = new HMACSHA1()) + { + algorithm.Key = Encoding.ASCII.GetBytes( + string.Format(CultureInfo.InvariantCulture, + "{0}&{1}", + UrlEncoder.Encode(consumerSecret), + string.IsNullOrEmpty(tokenSecret) ? string.Empty : UrlEncoder.Encode(tokenSecret))); + var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); + return Convert.ToBase64String(hash); + } + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs new file mode 100644 index 0000000000..03396807ee --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Globalization; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Options for the Twitter authentication handler. + /// + public class TwitterOptions : RemoteAuthenticationOptions + { + private const string DefaultStateCookieName = "__TwitterState"; + + private CookieBuilder _stateCookieBuilder; + + /// + /// Initializes a new instance of the class. + /// + public TwitterOptions() + { + CallbackPath = new PathString("/signin-twitter"); + BackchannelTimeout = TimeSpan.FromSeconds(60); + Events = new TwitterEvents(); + + ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + + _stateCookieBuilder = new TwitterCookieBuilder(this) + { + Name = DefaultStateCookieName, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + HttpOnly = true, + SameSite = SameSiteMode.Lax, + IsEssential = true, + }; + } + + /// + /// Gets or sets the consumer key used to communicate with Twitter. + /// + /// The consumer key used to communicate with Twitter. + public string ConsumerKey { get; set; } + + /// + /// Gets or sets the consumer secret used to sign requests to Twitter. + /// + /// The consumer secret used to sign requests to Twitter. + public string ConsumerSecret { get; set; } + + /// + /// Enables the retrieval user details during the authentication process, including + /// e-mail addresses. Retrieving e-mail addresses requires special permissions + /// from Twitter Support on a per application basis. The default is false. + /// See https://dev.twitter.com/rest/reference/get/account/verify_credentials + /// + public bool RetrieveUserDetails { get; set; } + + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + + /// + /// Gets or sets the type used to secure data handled by the handler. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the used to handle authentication events. + /// + public new TwitterEvents Events + { + get => (TwitterEvents)base.Events; + set => base.Events = value; + } + + /// + /// Determines the settings used to create the state cookie before the + /// cookie gets added to the response. + /// + public CookieBuilder StateCookie + { + get => _stateCookieBuilder; + set => _stateCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Added the validate method to ensure that the customer key and customer secret values are not not empty for the twitter authentication middleware + /// + public override void Validate() + { + base.Validate(); + if (string.IsNullOrEmpty(ConsumerKey)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerKey)), nameof(ConsumerKey)); + } + + if (string.IsNullOrEmpty(ConsumerSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerSecret)), nameof(ConsumerSecret)); + } + } + + private class TwitterCookieBuilder : CookieBuilder + { + private readonly TwitterOptions _twitterOptions; + + public TwitterCookieBuilder(TwitterOptions twitterOptions) + { + _twitterOptions = twitterOptions; + } + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var options = base.Build(context, expiresFrom); + if (!Expiration.HasValue) + { + options.Expires = expiresFrom.Add(_twitterOptions.RemoteAuthenticationTimeout); + } + return options; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs new file mode 100644 index 0000000000..09db5699f9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Used to setup defaults for all . + /// + public class TwitterPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public TwitterPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, TwitterOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(TwitterHandler).FullName, name, "v1"); + options.StateDataFormat = new SecureDataFormat( + new RequestTokenSerializer(), + dataProtector); + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + options.Backchannel.DefaultRequestHeaders.Accept.ParseAdd("*/*"); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core Twitter handler"); + options.Backchannel.DefaultRequestHeaders.ExpectContinue = false; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json new file mode 100644 index 0000000000..03ee645623 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json @@ -0,0 +1,924 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Twitter, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.TwitterExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.TwitterAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseTwitterAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseTwitterAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UserId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ScreenName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessTokenSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "userId", + "Type": "System.String" + }, + { + "Name": "screenName", + "Type": "System.String" + }, + { + "Name": "accessToken", + "Type": "System.String" + }, + { + "Name": "accessTokenSecret", + "Type": "System.String" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnCreatingTicket", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnCreatingTicket", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreatingTicket", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UserId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UserId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ScreenName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ScreenName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CallbackConfirmed", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CallbackConfirmed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestTokenSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "token", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Twitter\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "token", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConsumerKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsumerKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConsumerSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsumerSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RetrieveUserDetails", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RetrieveUserDetails", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs new file mode 100644 index 0000000000..f643fad97f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used in for . + /// + public class AuthenticationFailedContext : RemoteAuthenticationContext + { + /// + /// Creates a new context object + /// + /// + /// + /// + public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options) + : base(context, scheme, options, new AuthenticationProperties()) + { } + + /// + /// The from the request, if any. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// The that triggered this event. + /// + public Exception Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs new file mode 100644 index 0000000000..4028fa5e3c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used for . + /// + public class MessageReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a new context object. + /// + /// + /// + /// + /// + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + WsFederationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs new file mode 100644 index 0000000000..654037d0a8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// an instance of is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events. + /// + public class RedirectContext : PropertiesContext + { + /// + /// Creates a new context object. + /// + /// + /// + /// + /// + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + WsFederationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// The used to compose the redirect. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// If true, will skip any default logic for this redirect. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this redirect. + /// + public void HandleResponse() => Handled = true; + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs new file mode 100644 index 0000000000..8aec24a64e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// An event context for RemoteSignOut. + /// + public class RemoteSignOutContext : RemoteAuthenticationContext + { + /// + /// + /// + /// + /// + /// + /// + public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, WsFederationMessage message) + : base(context, scheme, options, new AuthenticationProperties()) + => ProtocolMessage = message; + + /// + /// The signout message. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs new file mode 100644 index 0000000000..311f41515f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. + /// + public class SecurityTokenReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public SecurityTokenReceivedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, AuthenticationProperties properties) + : base(context, scheme, options, properties) + { + } + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs new file mode 100644 index 0000000000..1f32014b6c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used for . + /// + public class SecurityTokenValidatedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public SecurityTokenValidatedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// The that was validated. + /// + public SecurityToken SecurityToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs new file mode 100644 index 0000000000..55c3936f9e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. /> + /// + public class WsFederationEvents : RemoteAuthenticationEvents + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a protocol message is first received. + /// + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. + /// + public Func OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint. + /// + public Func OnRemoteSignOut { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public Func OnSecurityTokenReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public Func OnSecurityTokenValidated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); + + /// + /// Invoked when a protocol message is first received. + /// + public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); + + /// + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. + /// + public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context); + + /// + /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint. + /// + public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context); + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public virtual Task SecurityTokenReceived(SecurityTokenReceivedContext context) => OnSecurityTokenReceived(context); + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public virtual Task SecurityTokenValidated(SecurityTokenValidatedContext context) => OnSecurityTokenValidated(context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs new file mode 100644 index 0000000000..e28b7e15b0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _signInWithoutWresult; + private static Action _signInWithoutToken; + private static Action _exceptionProcessingMessage; + private static Action _malformedRedirectUri; + private static Action _remoteSignOutHandledResponse; + private static Action _remoteSignOutSkipped; + private static Action _remoteSignOut; + + static LoggingExtensions() + { + _signInWithoutWresult = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Debug, + formatString: "Received a sign-in message without a WResult."); + _signInWithoutToken = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Debug, + formatString: "Received a sign-in message without a token."); + _exceptionProcessingMessage = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Error, + formatString: "Exception occurred while processing message."); + _malformedRedirectUri = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Warning, + formatString: "The sign-out redirect URI '{0}' is malformed."); + _remoteSignOutHandledResponse = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.HandledResponse"); + _remoteSignOutSkipped = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.Skipped"); + _remoteSignOut = LoggerMessage.Define( + eventId: 7, + logLevel: LogLevel.Information, + formatString: "Remote signout request processed."); + } + + public static void SignInWithoutWresult(this ILogger logger) + { + _signInWithoutWresult(logger, null); + } + + public static void SignInWithoutToken(this ILogger logger) + { + _signInWithoutToken(logger, null); + } + + public static void ExceptionProcessingMessage(this ILogger logger, Exception ex) + { + _exceptionProcessingMessage(logger, ex); + } + + public static void MalformedRedirectUri(this ILogger logger, string uri) + { + _malformedRedirectUri(logger, uri, null); + } + + public static void RemoteSignOutHandledResponse(this ILogger logger) + { + _remoteSignOutHandledResponse(logger, null); + } + + public static void RemoteSignOutSkipped(this ILogger logger) + { + _remoteSignOutSkipped(logger, null); + } + + public static void RemoteSignOut(this ILogger logger) + { + _remoteSignOut(logger, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj new file mode 100644 index 0000000000..4edb55cb35 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj @@ -0,0 +1,17 @@ + + + + ASP.NET Core middleware that enables an application to support the WsFederation authentication workflow. + netstandard2.0 + true + aspnetcore;authentication;security + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..564e826a78 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs @@ -0,0 +1,114 @@ +// +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.WsFederation.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The service descriptor is missing. + /// + internal static string Exception_MissingDescriptor + { + get => GetString("Exception_MissingDescriptor"); + } + + /// + /// The service descriptor is missing. + /// + internal static string FormatException_MissingDescriptor() + => GetString("Exception_MissingDescriptor"); + + /// + /// No token validator was found for the given token. + /// + internal static string Exception_NoTokenValidatorFound + { + get => GetString("Exception_NoTokenValidatorFound"); + } + + /// + /// No token validator was found for the given token. + /// + internal static string FormatException_NoTokenValidatorFound() + => GetString("Exception_NoTokenValidatorFound"); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + /// + /// The sign in message does not contain a required token. + /// + internal static string SignInMessageTokenIsMissing + { + get => GetString("SignInMessageTokenIsMissing"); + } + + /// + /// The sign in message does not contain a required token. + /// + internal static string FormatSignInMessageTokenIsMissing() + => GetString("SignInMessageTokenIsMissing"); + + /// + /// The sign in message does not contain a required wresult. + /// + internal static string SignInMessageWresultIsMissing + { + get => GetString("SignInMessageWresultIsMissing"); + } + + /// + /// The sign in message does not contain a required wresult. + /// + internal static string FormatSignInMessageWresultIsMissing() + => GetString("SignInMessageWresultIsMissing"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx new file mode 100644 index 0000000000..e2edafb671 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The service descriptor is missing. + + + No token validator was found for the given token. + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + + The sign in message does not contain a required token. + + + The sign in message does not contain a required wresult. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs new file mode 100644 index 0000000000..3b97d995b5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Default values related to WsFederation authentication handler + /// + public static class WsFederationDefaults + { + /// + /// The default authentication type used when registering the WsFederationHandler. + /// + public const string AuthenticationScheme = "WsFederation"; + + /// + /// The default display name used when registering the WsFederationHandler. + /// + public const string DisplayName = "WsFederation"; + + /// + /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'wctx' parameter. + /// + public static readonly string UserstatePropertiesKey = "WsFederation.Userstate"; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs new file mode 100644 index 0000000000..47091d58d5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.WsFederation; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extensions for registering the . + /// + public static class WsFederationExtensions + { + /// + /// Registers the using the default authentication scheme, display name, and options. + /// + /// + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder) + => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, _ => { }); + + /// + /// Registers the using the default authentication scheme, display name, and the given options configuration. + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions); + + /// + /// Registers the using the given authentication scheme, default display name, and the given options configuration. + /// + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions); + + /// + /// Registers the using the given authentication scheme, display name, and options configuration. + /// + /// + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, WsFederationPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs new file mode 100644 index 0000000000..e47f8431f9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs @@ -0,0 +1,425 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// A per-request authentication handler for the WsFederation. + /// + public class WsFederationHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler + { + private const string CorrelationProperty = ".xsrf"; + private WsFederationConfiguration _configuration; + + /// + /// Creates a new WsFederationAuthenticationHandler + /// + /// + /// + /// + /// + public WsFederationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new WsFederationEvents Events + { + get { return (WsFederationEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new WsFederationEvents()); + + /// + /// Overridden to handle remote signout requests + /// + /// + public override Task HandleRequestAsync() + { + // RemoteSignOutPath and CallbackPath may be the same, fall through if the message doesn't match. + if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path && HttpMethods.IsGet(Request.Method) + && string.Equals(Request.Query[WsFederationConstants.WsFederationParameterNames.Wa], + WsFederationConstants.WsFederationActions.SignOutCleanup, StringComparison.OrdinalIgnoreCase)) + { + // We've received a remote sign-out request + return HandleRemoteSignOutAsync(); + } + + return base.HandleRequestAsync(); + } + + /// + /// Handles Challenge + /// + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + // Save the original challenge URI so we can redirect back to it when we're done. + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + + var wsFederationMessage = new WsFederationMessage() + { + IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + Wtrealm = Options.Wtrealm, + Wa = WsFederationConstants.WsFederationActions.SignIn, + }; + + if (!string.IsNullOrEmpty(Options.Wreply)) + { + wsFederationMessage.Wreply = Options.Wreply; + } + else + { + wsFederationMessage.Wreply = BuildRedirectUri(Options.CallbackPath); + } + + GenerateCorrelationId(properties); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.RedirectToIdentityProvider(redirectContext); + + if (redirectContext.Handled) + { + return; + } + + wsFederationMessage = redirectContext.ProtocolMessage; + + if (!string.IsNullOrEmpty(wsFederationMessage.Wctx)) + { + properties.Items[WsFederationDefaults.UserstatePropertiesKey] = wsFederationMessage.Wctx; + } + + wsFederationMessage.Wctx = Uri.EscapeDataString(Options.StateDataFormat.Protect(properties)); + + var redirectUri = wsFederationMessage.CreateSignInUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.MalformedRedirectUri(redirectUri); + } + Response.Redirect(redirectUri); + } + + /// + /// Invoked to process incoming authentication messages. + /// + /// + protected override async Task HandleRemoteAuthenticateAsync() + { + WsFederationMessage wsFederationMessage = null; + AuthenticationProperties properties = null; + + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + if (HttpMethods.IsPost(Request.Method) + && !string.IsNullOrEmpty(Request.ContentType) + // May have media/type; charset=utf-8, allow partial match. + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.Body.CanRead) + { + var form = await Request.ReadFormAsync(); + + wsFederationMessage = new WsFederationMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + if (wsFederationMessage == null || !wsFederationMessage.IsSignInMessage) + { + if (Options.SkipUnrecognizedRequests) + { + // Not for us? + return HandleRequestResult.SkipHandler(); + } + + return HandleRequestResult.Fail("No message."); + } + + try + { + // Retrieve our cached redirect uri + var state = wsFederationMessage.Wctx; + // WsFed allows for uninitiated logins, state may be missing. See AllowUnsolicitedLogins. + properties = Options.StateDataFormat.Unprotect(state); + + if (properties == null) + { + if (!Options.AllowUnsolicitedLogins) + { + return HandleRequestResult.Fail("Unsolicited logins are not allowed."); + } + } + else + { + // Extract the user state from properties and reset. + properties.Items.TryGetValue(WsFederationDefaults.UserstatePropertiesKey, out var userState); + wsFederationMessage.Wctx = userState; + } + + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.MessageReceived(messageReceivedContext); + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + wsFederationMessage = messageReceivedContext.ProtocolMessage; + properties = messageReceivedContext.Properties; // Provides a new instance if not set. + + // If state did flow from the challenge then validate it. See AllowUnsolicitedLogins above. + if (properties.Items.TryGetValue(CorrelationProperty, out string correlationId) + && !ValidateCorrelationId(properties)) + { + return HandleRequestResult.Fail("Correlation failed.", properties); + } + + if (wsFederationMessage.Wresult == null) + { + Logger.SignInWithoutWresult(); + return HandleRequestResult.Fail(Resources.SignInMessageWresultIsMissing, properties); + } + + var token = wsFederationMessage.GetToken(); + if (string.IsNullOrEmpty(token)) + { + Logger.SignInWithoutToken(); + return HandleRequestResult.Fail(Resources.SignInMessageTokenIsMissing, properties); + } + + var securityTokenReceivedContext = new SecurityTokenReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.SecurityTokenReceived(securityTokenReceivedContext); + if (securityTokenReceivedContext.Result != null) + { + return securityTokenReceivedContext.Result; + } + wsFederationMessage = securityTokenReceivedContext.ProtocolMessage; + properties = messageReceivedContext.Properties; + + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + // Copy and augment to avoid cross request race conditions for updated configurations. + var tvp = Options.TokenValidationParameters.Clone(); + var issuers = new[] { _configuration.Issuer }; + tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); + tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + + ClaimsPrincipal principal = null; + SecurityToken parsedToken = null; + foreach (var validator in Options.SecurityTokenHandlers) + { + if (validator.CanReadToken(token)) + { + principal = validator.ValidateToken(token, tvp, out parsedToken); + break; + } + } + + if (principal == null) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + } + + if (Options.UseTokenLifetime && parsedToken != null) + { + // Override any session persistence to match the token lifetime. + var issued = parsedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + properties.IssuedUtc = issued.ToUniversalTime(); + } + var expires = parsedToken.ValidTo; + if (expires != DateTime.MinValue) + { + properties.ExpiresUtc = expires.ToUniversalTime(); + } + properties.AllowRefresh = false; + } + + var securityTokenValidatedContext = new SecurityTokenValidatedContext(Context, Scheme, Options, principal, properties) + { + ProtocolMessage = wsFederationMessage, + SecurityToken = parsedToken, + }; + + await Events.SecurityTokenValidated(securityTokenValidatedContext); + if (securityTokenValidatedContext.Result != null) + { + return securityTokenValidatedContext.Result; + } + + // Flow possible changes + principal = securityTokenValidatedContext.Principal; + properties = securityTokenValidatedContext.Properties; + + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); + } + catch (Exception exception) + { + Logger.ExceptionProcessingMessage(exception); + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. + if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + { + Options.ConfigurationManager.RequestRefresh(); + } + + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + ProtocolMessage = wsFederationMessage, + Exception = exception + }; + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + return HandleRequestResult.Fail(exception, properties); + } + } + + /// + /// Handles Signout + /// + /// + public async virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + if (target != null) + { + await Context.SignOutAsync(target, properties); + return; + } + + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var wsFederationMessage = new WsFederationMessage() + { + IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + Wtrealm = Options.Wtrealm, + Wa = WsFederationConstants.WsFederationActions.SignOut, + }; + + // Set Wreply in order: + // 1. properties.Redirect + // 2. Options.SignOutWreply + // 3. Options.Wreply + if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(properties.RedirectUri); + } + else if (!string.IsNullOrEmpty(Options.SignOutWreply)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.SignOutWreply); + } + else if (!string.IsNullOrEmpty(Options.Wreply)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.Wreply); + } + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.RedirectToIdentityProvider(redirectContext); + + if (!redirectContext.Handled) + { + var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.MalformedRedirectUri(redirectUri); + } + Response.Redirect(redirectUri); + } + } + + /// + /// Handles wsignoutcleanup1.0 messages sent to the RemoteSignOutPath + /// + /// + protected virtual async Task HandleRemoteSignOutAsync() + { + var message = new WsFederationMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message); + await Events.RemoteSignOut(remoteSignOutContext); + + if (remoteSignOutContext.Result != null) + { + if (remoteSignOutContext.Result.Handled) + { + Logger.RemoteSignOutHandledResponse(); + return true; + } + if (remoteSignOutContext.Result.Skipped) + { + Logger.RemoteSignOutSkipped(); + return false; + } + } + + Logger.RemoteSignOut(); + + await Context.SignOutAsync(Options.SignOutScheme); + return true; + } + + /// + /// Build a redirect path if the given path is a relative path. + /// + private string BuildRedirectUriIfRelative(string uri) + { + if (string.IsNullOrEmpty(uri)) + { + return uri; + } + + if (!uri.StartsWith("/", StringComparison.Ordinal)) + { + return uri; + } + + return BuildRedirectUri(uri); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs new file mode 100644 index 0000000000..4e06126773 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Saml; +using Microsoft.IdentityModel.Tokens.Saml2; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Configuration options for + /// + public class WsFederationOptions : RemoteAuthenticationOptions + { + private ICollection _securityTokenHandlers = new Collection() + { + new Saml2SecurityTokenHandler(), + new SamlSecurityTokenHandler(), + new JwtSecurityTokenHandler() + }; + private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); + + /// + /// Initializes a new + /// + public WsFederationOptions() + { + CallbackPath = "/signin-wsfed"; + // In ADFS the cleanup messages are sent to the same callback path as the initial login. + // In AAD it sends the cleanup message to a random Reply Url and there's no deterministic way to configure it. + // If you manage to get it configured, then you can set RemoteSignOutPath accordingly. + RemoteSignOutPath = "/signin-wsfed"; + Events = new WsFederationEvents(); + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + + if (ConfigurationManager == null) + { + throw new InvalidOperationException($"Provide {nameof(MetadataAddress)}, " + + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(WsFederationOptions)}"); + } + } + + /// + /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties + /// will not be used. This information should not be updated during request processing. + /// + public WsFederationConfiguration Configuration { get; set; } + + /// + /// Gets or sets the address to retrieve the wsFederation metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Responsible for retrieving, caching, and refreshing the configuration from metadata. + /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. + /// + public IConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic + /// recovery in the event of a signature key rollover. This is enabled by default. + /// + public bool RefreshOnIssuerKeyNotFound { get; set; } = true; + + /// + /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass + /// requests through that do not contain WsFederation authentication responses. Disabling this and setting the + /// CallbackPath to a dedicated endpoint may provide better error handling. + /// This is disabled by default. + /// + public bool SkipUnrecognizedRequests { get; set; } + + /// + /// Gets or sets the to call when processing WsFederation messages. + /// + public new WsFederationEvents Events + { + get => (WsFederationEvents)base.Events; + set => base.Events = value; + } + + /// + /// Gets or sets the collection of used to read and validate the s. + /// + public ICollection SecurityTokenHandlers + { + get + { + return _securityTokenHandlers; + } + set + { + _securityTokenHandlers = value ?? throw new ArgumentNullException(nameof(SecurityTokenHandlers)); + } + } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the + /// + /// if 'TokenValidationParameters' is null. + public TokenValidationParameters TokenValidationParameters + { + get + { + return _tokenValidationParameters; + } + set + { + _tokenValidationParameters = value ?? throw new ArgumentNullException(nameof(TokenValidationParameters)); + } + } + + /// + /// Gets or sets the 'wreply'. CallbackPath must be set to match or cleared so it can be generated dynamically. + /// This field is optional. If not set then it will be generated from the current request and the CallbackPath. + /// + public string Wreply { get; set; } + + /// + /// Gets or sets the 'wreply' value used during sign-out. + /// If none is specified then the value from the Wreply field is used. + /// + public string SignOutWreply { get; set; } + + /// + /// Gets or sets the 'wtrealm'. + /// + public string Wtrealm { get; set; } + + /// + /// Indicates that the authentication session lifetime (e.g. cookies) should match that of the authentication token. + /// If the token does not provide lifetime information then normal session lifetimes will be used. + /// This is enabled by default. + /// + public bool UseTokenLifetime { get; set; } = true; + + /// + /// Gets or sets if HTTPS is required for the metadata address or authority. + /// The default is true. This should be disabled only in development environments. + /// + public bool RequireHttpsMetadata { get; set; } = true; + + /// + /// The Ws-Federation protocol allows the user to initiate logins without contacting the application for a Challenge first. + /// However, that flow is susceptible to XSRF and other attacks so it is disabled here by default. + /// + public bool AllowUnsolicitedLogins { get; set; } + + /// + /// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme. + /// + public PathString RemoteSignOutPath { get; set; } + + /// + /// The Authentication Scheme to use with SignOutAsync from RemoteSignOutPath. SignInScheme will be used if this + /// is not set. + /// + public string SignOutScheme { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs new file mode 100644 index 0000000000..62647d4fcd --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Used to setup defaults for all . + /// + public class WsFederationPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + /// + /// + /// + /// + public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, WsFederationOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.SignOutScheme)) + { + options.SignOutScheme = options.SignInScheme; + } + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(WsFederationHandler).FullName, name, "v1"); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (!options.CallbackPath.HasValue && !string.IsNullOrEmpty(options.Wreply) && Uri.TryCreate(options.Wreply, UriKind.Absolute, out var wreply)) + { + // Wreply must be a very specific, case sensitive value, so we can't generate it. Instead we generate CallbackPath from it. + options.CallbackPath = PathString.FromUriComponent(wreply); + } + + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience)) + { + options.TokenValidationParameters.ValidAudience = options.Wtrealm; + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core WsFederation handler"); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!string.IsNullOrEmpty(options.MetadataAddress)) + { + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new WsFederationConfigurationRetriever(), + new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json new file mode 100644 index 0000000000..41150cbc09 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json @@ -0,0 +1,1314 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.WsFederation, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.WsFederationExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.SecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProvider", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProvider", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRemoteSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSecurityTokenReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSecurityTokenReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSecurityTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSecurityTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProvider", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SecurityTokenReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SecurityTokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "UserstatePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WsFederation\"" + }, + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WsFederation\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteSignOutAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SkipUnrecognizedRequests", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SkipUnrecognizedRequests", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenHandlers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityTokenHandlers", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.ICollection" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Wreply", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Wreply", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutWreply", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutWreply", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Wtrealm", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Wtrealm", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UseTokenLifetime", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UseTokenLifetime", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowUnsolicitedLogins", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowUnsolicitedLogins", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteSignOutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteSignOutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs new file mode 100644 index 0000000000..771601ed1a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add authentication capabilities to an HTTP application pipeline. + /// + public static class AuthAppBuilderExtensions + { + /// + /// Adds the to the specified , which enables authentication capabilities. + /// + /// The to add the middleware to. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs new file mode 100644 index 0000000000..401b1f488c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to configure authentication + /// + public class AuthenticationBuilder + { + /// + /// Constructor. + /// + /// The services being configured. + public AuthenticationBuilder(IServiceCollection services) + => Services = services; + + /// + /// The services being configured. + /// + public virtual IServiceCollection Services { get; } + + + private AuthenticationBuilder AddSchemeHelper(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : class, new() + where THandler : class, IAuthenticationHandler + { + Services.Configure(o => + { + o.AddScheme(authenticationScheme, scheme => { + scheme.HandlerType = typeof(THandler); + scheme.DisplayName = displayName; + }); + }); + if (configureOptions != null) + { + Services.Configure(authenticationScheme, configureOptions); + } + Services.AddTransient(); + return this; + } + + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddScheme(authenticationScheme, displayName: null, configureOptions: configureOptions); + + /// + /// Adds a based that supports remote authentication + /// which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : RemoteAuthenticationOptions, new() + where THandler : RemoteAuthenticationHandler + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureSignInScheme>()); + return AddScheme(authenticationScheme, displayName, configureOptions: configureOptions); + } + + /// + /// Adds a based authentication handler which can be used to + /// redirect to other authentication schemes. + /// + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action configureOptions) + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + + // Used to ensure that there's always a default sign in scheme that's not itself + private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + { + private readonly AuthenticationOptions _authOptions; + + public EnsureSignInScheme(IOptions authOptions) + { + _authOptions = authOptions.Value; + } + + public void PostConfigure(string name, TOptions options) + { + options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs new file mode 100644 index 0000000000..5c9a6473f1 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs @@ -0,0 +1,244 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new() + { + private Task _authenticateTask; + + public AuthenticationScheme Scheme { get; private set; } + public TOptions Options { get; private set; } + protected HttpContext Context { get; private set; } + + protected HttpRequest Request + { + get => Context.Request; + } + + protected HttpResponse Response + { + get => Context.Response; + } + + protected PathString OriginalPath => Context.Features.Get()?.OriginalPath ?? Request.Path; + + protected PathString OriginalPathBase => Context.Features.Get()?.OriginalPathBase ?? Request.PathBase; + + protected ILogger Logger { get; } + + protected UrlEncoder UrlEncoder { get; } + + protected ISystemClock Clock { get; } + + protected IOptionsMonitor OptionsMonitor { get; } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected virtual object Events { get; set; } + + protected virtual string ClaimsIssuer => Options.ClaimsIssuer ?? Scheme.Name; + + protected string CurrentUri + { + get => Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString; + } + + protected AuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + { + Logger = logger.CreateLogger(this.GetType().FullName); + UrlEncoder = encoder; + Clock = clock; + OptionsMonitor = options; + } + + /// + /// Initialize the handler, resolve the options and validate them. + /// + /// + /// + /// + public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + Scheme = scheme; + Context = context; + + Options = OptionsMonitor.Get(Scheme.Name) ?? new TOptions(); + Options.Validate(Scheme.Name); + + await InitializeEventsAsync(); + await InitializeHandlerAsync(); + } + + /// + /// Initializes the events object, called once per request by . + /// + protected virtual async Task InitializeEventsAsync() + { + Events = Options.Events; + if (Options.EventsType != null) + { + Events = Context.RequestServices.GetRequiredService(Options.EventsType); + } + Events = Events ?? await CreateEventsAsync(); + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected virtual Task CreateEventsAsync() => Task.FromResult(new object()); + + /// + /// Called after options/events have been initialized for the handler to finish initializing itself. + /// + /// A task + protected virtual Task InitializeHandlerAsync() => Task.CompletedTask; + + protected string BuildRedirectUri(string targetPath) + => Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; + + protected virtual string ResolveTarget(string scheme) + { + var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault; + + // Prevent self targetting + return string.Equals(target, Scheme.Name, StringComparison.Ordinal) + ? null + : target; + } + + public async Task AuthenticateAsync() + { + var target = ResolveTarget(Options.ForwardAuthenticate); + if (target != null) + { + return await Context.AuthenticateAsync(target); + } + + // Calling Authenticate more than once should always return the original value. + var result = await HandleAuthenticateOnceAsync(); + if (result?.Failure == null) + { + var ticket = result?.Ticket; + if (ticket?.Principal != null) + { + Logger.AuthenticationSchemeAuthenticated(Scheme.Name); + } + else + { + Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name); + } + } + else + { + Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message); + } + return result; + } + + /// + /// Used to ensure HandleAuthenticateAsync is only invoked once. The subsequent calls + /// will return the same authenticate result. + /// + protected Task HandleAuthenticateOnceAsync() + { + if (_authenticateTask == null) + { + _authenticateTask = HandleAuthenticateAsync(); + } + + return _authenticateTask; + } + + /// + /// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent + /// calls will return the same authentication result. Any exceptions will be converted + /// into a failed authentication result containing the exception. + /// + protected async Task HandleAuthenticateOnceSafeAsync() + { + try + { + return await HandleAuthenticateOnceAsync(); + } + catch (Exception ex) + { + return AuthenticateResult.Fail(ex); + } + } + + protected abstract Task HandleAuthenticateAsync(); + + /// + /// Override this method to handle Forbid. + /// + /// + /// A Task. + protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties) + { + Response.StatusCode = 403; + return Task.CompletedTask; + } + + /// + /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question + /// deals an authentication interaction as part of it's request flow. (like adding a response header, or + /// changing the 401 result to 302 of a login page or external sign-in location.) + /// + /// + /// A Task. + protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.StatusCode = 401; + return Task.CompletedTask; + } + + public async Task ChallengeAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardChallenge); + if (target != null) + { + await Context.ChallengeAsync(target, properties); + return; + } + + properties = properties ?? new AuthenticationProperties(); + await HandleChallengeAsync(properties); + Logger.AuthenticationSchemeChallenged(Scheme.Name); + } + + public async Task ForbidAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardForbid); + if (target != null) + { + await Context.ForbidAsync(target, properties); + return; + } + + properties = properties ?? new AuthenticationProperties(); + await HandleForbiddenAsync(properties); + Logger.AuthenticationSchemeForbidden(Scheme.Name); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs new file mode 100644 index 0000000000..0c62cc3c39 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationMiddleware + { + private readonly RequestDelegate _next; + + public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + if (schemes == null) + { + throw new ArgumentNullException(nameof(schemes)); + } + + _next = next; + Schemes = schemes; + } + + public IAuthenticationSchemeProvider Schemes { get; set; } + + public async Task Invoke(HttpContext context) + { + context.Features.Set(new AuthenticationFeature + { + OriginalPath = context.Request.Path, + OriginalPathBase = context.Request.PathBase + }); + + // Give any IAuthenticationRequestHandler schemes a chance to handle the request + var handlers = context.RequestServices.GetRequiredService(); + foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) + { + var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler; + if (handler != null && await handler.HandleRequestAsync()) + { + return; + } + } + + var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); + if (defaultAuthenticate != null) + { + var result = await context.AuthenticateAsync(defaultAuthenticate.Name); + if (result?.Principal != null) + { + context.User = result.Principal; + } + } + + await _next(context); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs new file mode 100644 index 0000000000..a547d203b4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the options used by the . + /// + public class AuthenticationSchemeOptions + { + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public virtual void Validate() { } + + /// + /// Checks that the options are valid for a specific scheme + /// + /// The scheme being validated. + public virtual void Validate(string scheme) + => Validate(); + + /// + /// Gets or sets the issuer that should be used for any claims that are created + /// + public string ClaimsIssuer { get; set; } + + /// + /// Instance used for events + /// + public object Events { get; set; } + + /// + /// If set, will be used as the service type to get the Events instance instead of the property. + /// + public Type EventsType { get; set; } + + /// + /// If set, this specifies a default scheme that authentication handlers should forward all authentication operations to + /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut + /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result + /// will be used as the target scheme to forward to. + /// + public string ForwardDefault { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward AuthenticateAsync calls to. + /// For example Context.AuthenticateAsync("ThisScheme") => Context.AuthenticateAsync("ForwardAuthenticateValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardAuthenticate { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward ChallengeAsync calls to. + /// For example Context.ChallengeAsync("ThisScheme") => Context.ChallengeAsync("ForwardChallengeValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardChallenge { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward ForbidAsync calls to. + /// For example Context.ForbidAsync("ThisScheme") => Context.ForbidAsync("ForwardForbidValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardForbid { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward SignInAsync calls to. + /// For example Context.SignInAsync("ThisScheme") => Context.SignInAsync("ForwardSignInValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardSignIn { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward SignOutAsync calls to. + /// For example Context.SignOutAsync("ThisScheme") => Context.SignInAsync("ForwardSignOutValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardSignOut { get; set; } + + /// + /// Used to select a default scheme for the current request that authentication handlers should forward all authentication operations to + /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut + /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result + /// will be used as the target scheme to forward to. + /// + public Func ForwardDefaultSelector { get; set; } + + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs new file mode 100644 index 0000000000..b274eaace4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up authentication services in an . + /// + public static class AuthenticationServiceCollectionExtensions + { + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddAuthenticationCore(); + services.AddDataProtection(); + services.AddWebEncoders(); + services.TryAddSingleton(); + return new AuthenticationBuilder(services); + } + + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, string defaultScheme) + => services.AddAuthentication(o => o.DefaultScheme = defaultScheme); + + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action configureOptions) { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + var builder = services.AddAuthentication(); + services.Configure(configureOptions); + return builder; + } + + [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] + public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureScheme, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + { + services.AddAuthentication(o => + { + o.AddScheme(authenticationScheme, scheme => { + scheme.HandlerType = typeof(THandler); + scheme.DisplayName = displayName; + configureScheme?.Invoke(scheme); + }); + }); + if (configureOptions != null) + { + services.Configure(authenticationScheme, configureOptions); + } + services.AddTransient(); + return services; + } + + [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] + public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => services.AddScheme(authenticationScheme, displayName: null, configureScheme: null, configureOptions: configureOptions); + + [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] + public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => services.AddScheme(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions); + + [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] + public static IServiceCollection AddRemoteScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureOptions) + where TOptions : RemoteAuthenticationOptions, new() + where THandler : RemoteAuthenticationHandler + { + services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureSignInScheme>()); + return services.AddScheme(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions); + } + + // Used to ensure that there's always a sign in scheme + private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + { + private readonly AuthenticationOptions _authOptions; + + public EnsureSignInScheme(IOptions authOptions) + { + _authOptions = authOptions.Value; + } + + public void PostConfigure(string name, TOptions options) + { + options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme; + } + } + + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs new file mode 100644 index 0000000000..ad9c523005 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + public interface IDataSerializer + { + byte[] Serialize(TModel model); + TModel Deserialize(byte[] data); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs new file mode 100644 index 0000000000..73b1b882b5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + public interface ISecureDataFormat + { + string Protect(TData data); + string Protect(TData data, string purpose); + TData Unprotect(string protectedText); + TData Unprotect(string protectedText, string purpose); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs new file mode 100644 index 0000000000..3d31e4bd2d --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http.Authentication; + +namespace Microsoft.AspNetCore.Authentication +{ + public class PropertiesDataFormat : SecureDataFormat + { + public PropertiesDataFormat(IDataProtector protector) + : base(new PropertiesSerializer(), protector) + { + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs new file mode 100644 index 0000000000..dd30b45ae0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Http.Authentication; + +namespace Microsoft.AspNetCore.Authentication +{ + public class PropertiesSerializer : IDataSerializer + { + private const int FormatVersion = 1; + + public static PropertiesSerializer Default { get; } = new PropertiesSerializer(); + + public virtual byte[] Serialize(AuthenticationProperties model) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, model); + writer.Flush(); + return memory.ToArray(); + } + } + } + + public virtual AuthenticationProperties Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + public virtual void Write(BinaryWriter writer, AuthenticationProperties properties) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + writer.Write(FormatVersion); + writer.Write(properties.Items.Count); + + foreach (var item in properties.Items) + { + writer.Write(item.Key ?? string.Empty); + writer.Write(item.Value ?? string.Empty); + } + } + + public virtual AuthenticationProperties Read(BinaryReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + var count = reader.ReadInt32(); + var extra = new Dictionary(count); + + for (var index = 0; index != count; ++index) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + extra.Add(key, value); + } + return new AuthenticationProperties(extra); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs new file mode 100644 index 0000000000..f35025d8bb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.DataProtection; + +namespace Microsoft.AspNetCore.Authentication +{ + public class SecureDataFormat : ISecureDataFormat + { + private readonly IDataSerializer _serializer; + private readonly IDataProtector _protector; + + public SecureDataFormat(IDataSerializer 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); + } + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs new file mode 100644 index 0000000000..1f7ecc7184 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + public static class Base64UrlTextEncoder + { + /// + /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable + /// characters. + /// + /// Data to be encoded. + /// Base64 encoded string modified with non-URL encodable characters + public static string Encode(byte[] data) + { + return WebUtilities.WebEncoders.Base64UrlEncode(data); + } + + /// + /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and + /// then decodes the Base64 string. + /// + /// The string to be decoded. + /// The decoded data. + public static byte[] Decode(string text) + { + return WebUtilities.WebEncoders.Base64UrlDecode(text); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs new file mode 100644 index 0000000000..e43943cfc8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.DataProtection; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TicketDataFormat : SecureDataFormat + { + public TicketDataFormat(IDataProtector protector) + : base(TicketSerializer.Default, protector) + { + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs new file mode 100644 index 0000000000..e33ec71725 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs @@ -0,0 +1,275 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + // This MUST be kept in sync with Microsoft.Owin.Security.Interop.AspNetTicketSerializer + public class TicketSerializer : IDataSerializer + { + private const string DefaultStringPlaceholder = "\0"; + private const int FormatVersion = 5; + + public static TicketSerializer Default { get; } = new TicketSerializer(); + + public virtual byte[] Serialize(AuthenticationTicket ticket) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, ticket); + } + return memory.ToArray(); + } + } + + public virtual AuthenticationTicket Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + public virtual void Write(BinaryWriter writer, AuthenticationTicket ticket) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + + writer.Write(FormatVersion); + writer.Write(ticket.AuthenticationScheme); + + // Write the number of identities contained in the principal. + var principal = ticket.Principal; + writer.Write(principal.Identities.Count()); + + foreach (var identity in principal.Identities) + { + WriteIdentity(writer, identity); + } + + PropertiesSerializer.Default.Write(writer, ticket.Properties); + } + + protected virtual void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + var authenticationType = identity.AuthenticationType ?? string.Empty; + + writer.Write(authenticationType); + WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); + WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); + + // Write the number of claims contained in the identity. + writer.Write(identity.Claims.Count()); + + foreach (var claim in identity.Claims) + { + WriteClaim(writer, claim); + } + + var bootstrap = identity.BootstrapContext as string; + if (!string.IsNullOrEmpty(bootstrap)) + { + writer.Write(true); + writer.Write(bootstrap); + } + else + { + writer.Write(false); + } + + if (identity.Actor != null) + { + writer.Write(true); + WriteIdentity(writer, identity.Actor); + } + else + { + writer.Write(false); + } + } + + protected virtual void WriteClaim(BinaryWriter writer, Claim claim) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); + writer.Write(claim.Value); + WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); + WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); + WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); + + // Write the number of properties contained in the claim. + writer.Write(claim.Properties.Count); + + foreach (var property in claim.Properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + public virtual AuthenticationTicket Read(BinaryReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + var scheme = reader.ReadString(); + + // Read the number of identities stored + // in the serialized payload. + var count = reader.ReadInt32(); + if (count < 0) + { + return null; + } + + var identities = new ClaimsIdentity[count]; + for (var index = 0; index != count; ++index) + { + identities[index] = ReadIdentity(reader); + } + + var properties = PropertiesSerializer.Default.Read(reader); + + return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, scheme); + } + + protected virtual ClaimsIdentity ReadIdentity(BinaryReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + var authenticationType = reader.ReadString(); + var nameClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType); + var roleClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType); + + // Read the number of claims contained + // in the serialized identity. + var count = reader.ReadInt32(); + + var identity = new ClaimsIdentity(authenticationType, nameClaimType, roleClaimType); + + for (int index = 0; index != count; ++index) + { + var claim = ReadClaim(reader, identity); + + identity.AddClaim(claim); + } + + // Determine whether the identity + // has a bootstrap context attached. + if (reader.ReadBoolean()) + { + identity.BootstrapContext = reader.ReadString(); + } + + // Determine whether the identity + // has an actor identity attached. + if (reader.ReadBoolean()) + { + identity.Actor = ReadIdentity(reader); + } + + return identity; + } + + protected virtual Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + var type = ReadWithDefault(reader, identity.NameClaimType); + var value = reader.ReadString(); + var valueType = ReadWithDefault(reader, ClaimValueTypes.String); + var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); + var originalIssuer = ReadWithDefault(reader, issuer); + + var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); + + // Read the number of properties stored in the claim. + var count = reader.ReadInt32(); + + for (var index = 0; index != count; ++index) + { + var key = reader.ReadString(); + var propertyValue = reader.ReadString(); + + claim.Properties.Add(key, propertyValue); + } + + return claim; + } + + private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + { + if (string.Equals(value, defaultValue, StringComparison.Ordinal)) + { + writer.Write(DefaultStringPlaceholder); + } + else + { + writer.Write(value); + } + } + + private static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + var value = reader.ReadString(); + if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal)) + { + return defaultValue; + } + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs new file mode 100644 index 0000000000..915fc2377f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base class used by other context classes. + /// + public abstract class BaseContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + protected BaseContext(HttpContext context, AuthenticationScheme scheme, TOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + HttpContext = context; + Scheme = scheme; + Options = options; + } + + /// + /// The authentication scheme. + /// + public AuthenticationScheme Scheme { get; } + + /// + /// Gets the authentication options associated with the scheme. + /// + public TOptions Options { get; } + + /// + /// The context. + /// + public HttpContext HttpContext { get; } + + /// + /// The request. + /// + public HttpRequest Request => HttpContext.Request; + + /// + /// The response. + /// + public HttpResponse Response => HttpContext.Response; + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs new file mode 100644 index 0000000000..52dd9ce12f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + public class HandleRequestContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + protected HandleRequestContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options) + : base(context, scheme, options) { } + + /// + /// The which is used by the handler. + /// + public HandleRequestResult Result { get; protected set; } + + /// + /// Discontinue all processing for this request and return to the client. + /// The caller is responsible for generating the full response. + /// + public void HandleResponse() => Result = HandleRequestResult.Handle(); + + /// + /// Discontinue processing the request in the current handler. + /// + public void SkipHandler() => Result = HandleRequestResult.SkipHandler(); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs new file mode 100644 index 0000000000..8bf40760a1 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for authentication events which deal with a ClaimsPrincipal. + /// + public abstract class PrincipalContext : PropertiesContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected PrincipalContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// Gets the containing the user claims. + /// + public virtual ClaimsPrincipal Principal { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs new file mode 100644 index 0000000000..f1730d0d7f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for authentication events which contain . + /// + public abstract class PropertiesContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected PropertiesContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties) + : base(context, scheme, options) + { + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Gets or sets the . + /// + public virtual AuthenticationProperties Properties { get; protected set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs new file mode 100644 index 0000000000..dac24cafa6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Context passed for redirect events. + /// + public class RedirectContext : PropertiesContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Creates a new context object. + /// + /// The HTTP request context + /// The scheme data + /// The handler options + /// The initial redirect URI + /// The . + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options, + AuthenticationProperties properties, + string redirectUri) + : base(context, scheme, options, properties) + { + Properties = properties; + RedirectUri = redirectUri; + } + + /// + /// Gets or Sets the URI used for the redirect operation. + /// + public string RedirectUri { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs new file mode 100644 index 0000000000..b7a0168798 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for remote authentication. + /// + public abstract class RemoteAuthenticationContext : HandleRequestContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected RemoteAuthenticationContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options, + AuthenticationProperties properties) + : base(context, scheme, options) + => Properties = properties ?? new AuthenticationProperties(); + + /// + /// Gets the containing the user claims. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the . + /// + public virtual AuthenticationProperties Properties { get; set; } + + /// + /// Calls success creating a ticket with the and . + /// + public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name)); + + public void Fail(Exception failure) => Result = HandleRequestResult.Fail(failure); + + public void Fail(string failureMessage) => Result = HandleRequestResult.Fail(failureMessage); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs new file mode 100644 index 0000000000..ca0f4a5c01 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + public class RemoteAuthenticationEvents + { + public Func OnRemoteFailure { get; set; } = context => Task.CompletedTask; + + public Func OnTicketReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when there is a remote failure + /// + public virtual Task RemoteFailure(RemoteFailureContext context) => OnRemoteFailure(context); + + /// + /// Invoked after the remote ticket has been received. + /// + public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs new file mode 100644 index 0000000000..6b3598f40a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides failure context information to handler providers. + /// + public class RemoteFailureContext : HandleRequestContext + { + public RemoteFailureContext( + HttpContext context, + AuthenticationScheme scheme, + RemoteAuthenticationOptions options, + Exception failure) + : base(context, scheme, options) + { + Failure = failure; + } + + /// + /// User friendly error message for the error. + /// + public Exception Failure { get; set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs new file mode 100644 index 0000000000..12b21f4bf6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for events that produce AuthenticateResults. + /// + public abstract class ResultContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + protected ResultContext(HttpContext context, AuthenticationScheme scheme, TOptions options) + : base(context, scheme, options) { } + + /// + /// Gets or sets the containing the user claims. + /// + public ClaimsPrincipal Principal { get; set; } + + private AuthenticationProperties _properties; + /// + /// Gets or sets the . + /// + public AuthenticationProperties Properties { + get => _properties ?? (_properties = new AuthenticationProperties()); + set => _properties = value; + } + + /// + /// Gets the result. + /// + public AuthenticateResult Result { get; private set; } + + /// + /// Calls success creating a ticket with the and . + /// + public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name)); + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + public void NoResult() => Result = AuthenticateResult.NoResult(); + + /// + /// Indicates that there was a failure during authentication. + /// + /// + public void Fail(Exception failure) => Result = AuthenticateResult.Fail(failure); + + /// + /// Indicates that there was a failure during authentication. + /// + /// + public void Fail(string failureMessage) => Result = AuthenticateResult.Fail(failureMessage); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs new file mode 100644 index 0000000000..51b77a37fa --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides context information to handler providers. + /// + public class TicketReceivedContext : RemoteAuthenticationContext + { + public TicketReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + RemoteAuthenticationOptions options, + AuthenticationTicket ticket) + : base(context, scheme, options, ticket?.Properties) + => Principal = ticket?.Principal; + + public string ReturnUri { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs new file mode 100644 index 0000000000..da9b6ea01c --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the result of an Authenticate call + /// + public class HandleRequestResult : AuthenticateResult + { + /// + /// Indicates that stage of authentication was directly handled by + /// user intervention and no further processing should be attempted. + /// + public bool Handled { get; private set; } + + /// + /// Indicates that the default authentication logic should be + /// skipped and that the rest of the pipeline should be invoked. + /// + public bool Skipped { get; private set; } + + /// + /// Indicates that authentication was successful. + /// + /// The ticket representing the authentication result. + /// The result. + public static new HandleRequestResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new HandleRequestResult() { Ticket = ticket, Properties = ticket.Properties }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// The result. + public static new HandleRequestResult Fail(Exception failure) + { + return new HandleRequestResult() { Failure = failure }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// Additional state values for the authentication session. + /// The result. + public static new HandleRequestResult Fail(Exception failure, AuthenticationProperties properties) + { + return new HandleRequestResult() { Failure = failure, Properties = properties }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// The result. + public static new HandleRequestResult Fail(string failureMessage) + => Fail(new Exception(failureMessage)); + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// Additional state values for the authentication session. + /// The result. + public static new HandleRequestResult Fail(string failureMessage, AuthenticationProperties properties) + => Fail(new Exception(failureMessage), properties); + + /// + /// Discontinue all processing for this request and return to the client. + /// The caller is responsible for generating the full response. + /// + /// The result. + public static HandleRequestResult Handle() + { + return new HandleRequestResult() { Handled = true }; + } + + /// + /// Discontinue processing the request in the current handler. + /// + /// The result. + public static HandleRequestResult SkipHandler() + { + return new HandleRequestResult() { Skipped = true }; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs new file mode 100644 index 0000000000..5582669861 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Abstracts the system clock to facilitate testing. + /// + public interface ISystemClock + { + /// + /// Retrieves the current system time in UTC. + /// + DateTimeOffset UtcNow { get; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs new file mode 100644 index 0000000000..f42617cb23 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Internal +{ + /// + /// A cookie builder that sets to the request path base. + /// + public class RequestPathBaseCookieBuilder : CookieBuilder + { + /// + /// Gets an optional value that is appended to the request path base. + /// + protected virtual string AdditionalPath { get; } + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + // check if the user has overridden the default value of path. If so, use that instead of our default value. + var path = Path; + if (path == null) + { + var originalPathBase = context.Features.Get()?.OriginalPathBase ?? context.Request.PathBase; + path = originalPathBase + AdditionalPath; + } + + var options = base.Build(context, expiresFrom); + + options.Path = !string.IsNullOrEmpty(path) + ? path + : "/"; + + return options; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs new file mode 100644 index 0000000000..8cba6c0d5e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _authSchemeAuthenticated; + private static Action _authSchemeNotAuthenticated; + private static Action _authSchemeNotAuthenticatedWithFailure; + private static Action _authSchemeChallenged; + private static Action _authSchemeForbidden; + private static Action _remoteAuthenticationError; + private static Action _signInHandled; + private static Action _signInSkipped; + private static Action _correlationPropertyNotFound; + private static Action _correlationCookieNotFound; + private static Action _unexpectedCorrelationCookieValue; + + static LoggingExtensions() + { + _remoteAuthenticationError = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Information, + formatString: "Error from RemoteAuthentication: {ErrorMessage}."); + _signInHandled = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Debug, + formatString: "The SigningIn event returned Handled."); + _signInSkipped = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "The SigningIn event returned Skipped."); + _authSchemeNotAuthenticatedWithFailure = LoggerMessage.Define( + eventId: 7, + logLevel: LogLevel.Information, + formatString: "{AuthenticationScheme} was not authenticated. Failure message: {FailureMessage}"); + _authSchemeAuthenticated = LoggerMessage.Define( + eventId: 8, + logLevel: LogLevel.Debug, + formatString: "AuthenticationScheme: {AuthenticationScheme} was successfully authenticated."); + _authSchemeNotAuthenticated = LoggerMessage.Define( + eventId: 9, + logLevel: LogLevel.Debug, + formatString: "AuthenticationScheme: {AuthenticationScheme} was not authenticated."); + _authSchemeChallenged = LoggerMessage.Define( + eventId: 12, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} was challenged."); + _authSchemeForbidden = LoggerMessage.Define( + eventId: 13, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} was forbidden."); + _correlationPropertyNotFound = LoggerMessage.Define( + eventId: 14, + logLevel: LogLevel.Warning, + formatString: "{CorrelationProperty} state property not found."); + _correlationCookieNotFound = LoggerMessage.Define( + eventId: 15, + logLevel: LogLevel.Warning, + formatString: "'{CorrelationCookieName}' cookie not found."); + _unexpectedCorrelationCookieValue = LoggerMessage.Define( + eventId: 16, + logLevel: LogLevel.Warning, + formatString: "The correlation cookie value '{CorrelationCookieName}' did not match the expected value '{CorrelationCookieValue}'."); + } + + public static void AuthenticationSchemeAuthenticated(this ILogger logger, string authenticationScheme) + { + _authSchemeAuthenticated(logger, authenticationScheme, null); + } + + public static void AuthenticationSchemeNotAuthenticated(this ILogger logger, string authenticationScheme) + { + _authSchemeNotAuthenticated(logger, authenticationScheme, null); + } + + public static void AuthenticationSchemeNotAuthenticatedWithFailure(this ILogger logger, string authenticationScheme, string failureMessage) + { + _authSchemeNotAuthenticatedWithFailure(logger, authenticationScheme, failureMessage, null); + } + + public static void AuthenticationSchemeChallenged(this ILogger logger, string authenticationScheme) + { + _authSchemeChallenged(logger, authenticationScheme, null); + } + + public static void AuthenticationSchemeForbidden(this ILogger logger, string authenticationScheme) + { + _authSchemeForbidden(logger, authenticationScheme, null); + } + + public static void RemoteAuthenticationError(this ILogger logger, string errorMessage) + { + _remoteAuthenticationError(logger, errorMessage, null); + } + + public static void SigninHandled(this ILogger logger) + { + _signInHandled(logger, null); + } + + public static void SigninSkipped(this ILogger logger) + { + _signInSkipped(logger, null); + } + + public static void CorrelationPropertyNotFound(this ILogger logger, string correlationPrefix) + { + _correlationPropertyNotFound(logger, correlationPrefix, null); + } + + public static void CorrelationCookieNotFound(this ILogger logger, string cookieName) + { + _correlationCookieNotFound(logger, cookieName, null); + } + + public static void UnexpectedCorrelationCookieValue(this ILogger logger, string cookieName, string cookieValue) + { + _unexpectedCorrelationCookieValue(logger, cookieName, cookieValue, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj new file mode 100644 index 0000000000..7e3ce4eb39 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj @@ -0,0 +1,22 @@ + + + + ASP.NET Core common types used by the various authentication middleware components. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs new file mode 100644 index 0000000000..4dbbb7de2d --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// PolicySchemes are used to redirect authentication methods to another scheme. + /// + public class PolicySchemeHandler : SignInAuthenticationHandler + { + public PolicySchemeHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleSignOutAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleAuthenticateAsync() + => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs new file mode 100644 index 0000000000..1921c77ec8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the options used by the . + /// + public class PolicySchemeOptions : AuthenticationSchemeOptions + { } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..b1941a7dca --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs @@ -0,0 +1,100 @@ +// +namespace Microsoft.AspNetCore.Authentication +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string Exception_DefaultDpapiRequiresAppNameKey + { + get => GetString("Exception_DefaultDpapiRequiresAppNameKey"); + } + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string FormatException_DefaultDpapiRequiresAppNameKey() + => GetString("Exception_DefaultDpapiRequiresAppNameKey"); + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string Exception_UnhookAuthenticationStateType + { + get => GetString("Exception_UnhookAuthenticationStateType"); + } + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string FormatException_UnhookAuthenticationStateType() + => GetString("Exception_UnhookAuthenticationStateType"); + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods + { + get => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + } + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string FormatException_AuthenticationTokenDoesNotProvideSyncMethods() + => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + /// + internal static string Exception_RemoteSignInSchemeCannotBeSelf + { + get => GetString("Exception_RemoteSignInSchemeCannotBeSelf"); + } + + /// + /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + /// + internal static string FormatException_RemoteSignInSchemeCannotBeSelf() + => GetString("Exception_RemoteSignInSchemeCannotBeSelf"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs new file mode 100644 index 0000000000..bea4895d62 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs @@ -0,0 +1,245 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + public abstract class RemoteAuthenticationHandler : AuthenticationHandler, IAuthenticationRequestHandler + where TOptions : RemoteAuthenticationOptions, new() + { + private const string CorrelationProperty = ".xsrf"; + private const string CorrelationMarker = "N"; + private const string AuthSchemeKey = ".AuthScheme"; + + private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); + + protected string SignInScheme => Options.SignInScheme; + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new RemoteAuthenticationEvents Events + { + get { return (RemoteAuthenticationEvents)base.Events; } + set { base.Events = value; } + } + + protected RemoteAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) { } + + protected override Task CreateEventsAsync() + => Task.FromResult(new RemoteAuthenticationEvents()); + + public virtual Task ShouldHandleRequestAsync() + => Task.FromResult(Options.CallbackPath == Request.Path); + + public virtual async Task HandleRequestAsync() + { + if (!await ShouldHandleRequestAsync()) + { + return false; + } + + AuthenticationTicket ticket = null; + Exception exception = null; + AuthenticationProperties properties = null; + try + { + var authResult = await HandleRemoteAuthenticateAsync(); + if (authResult == null) + { + exception = new InvalidOperationException("Invalid return state, unable to redirect."); + } + else if (authResult.Handled) + { + return true; + } + else if (authResult.Skipped || authResult.None) + { + return false; + } + else if (!authResult.Succeeded) + { + exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect."); + properties = authResult.Properties; + } + + ticket = authResult?.Ticket; + } + catch (Exception ex) + { + exception = ex; + } + + if (exception != null) + { + Logger.RemoteAuthenticationError(exception.Message); + var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception) + { + Properties = properties + }; + await Events.RemoteFailure(errorContext); + + if (errorContext.Result != null) + { + if (errorContext.Result.Handled) + { + return true; + } + else if (errorContext.Result.Skipped) + { + return false; + } + else if (errorContext.Result.Failure != null) + { + throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure); + } + } + + if (errorContext.Failure != null) + { + throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure); + } + } + + // We have a ticket if we get here + var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket) + { + ReturnUri = ticket.Properties.RedirectUri + }; + + ticket.Properties.RedirectUri = null; + + // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync + ticketContext.Properties.Items[AuthSchemeKey] = Scheme.Name; + + await Events.TicketReceived(ticketContext); + + if (ticketContext.Result != null) + { + if (ticketContext.Result.Handled) + { + Logger.SigninHandled(); + return true; + } + else if (ticketContext.Result.Skipped) + { + Logger.SigninSkipped(); + return false; + } + } + + await Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties); + + // Default redirect path is the base path + if (string.IsNullOrEmpty(ticketContext.ReturnUri)) + { + ticketContext.ReturnUri = "/"; + } + + Response.Redirect(ticketContext.ReturnUri); + return true; + } + + /// + /// Authenticate the user identity with the identity provider. + /// + /// The method process the request on the endpoint defined by CallbackPath. + /// + protected abstract Task HandleRemoteAuthenticateAsync(); + + protected override async Task HandleAuthenticateAsync() + { + var result = await Context.AuthenticateAsync(SignInScheme); + if (result != null) + { + if (result.Failure != null) + { + return result; + } + + // The SignInScheme may be shared with multiple providers, make sure this provider issued the identity. + string authenticatedScheme; + var ticket = result.Ticket; + if (ticket != null && ticket.Principal != null && ticket.Properties != null + && ticket.Properties.Items.TryGetValue(AuthSchemeKey, out authenticatedScheme) + && string.Equals(Scheme.Name, authenticatedScheme, StringComparison.Ordinal)) + { + return AuthenticateResult.Success(new AuthenticationTicket(ticket.Principal, + ticket.Properties, Scheme.Name)); + } + + return AuthenticateResult.Fail("Not authenticated"); + } + + return AuthenticateResult.Fail("Remote authentication does not directly support AuthenticateAsync"); + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + => Context.ForbidAsync(SignInScheme); + + protected virtual void GenerateCorrelationId(AuthenticationProperties properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + var bytes = new byte[32]; + CryptoRandom.GetBytes(bytes); + var correlationId = Base64UrlTextEncoder.Encode(bytes); + + var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow); + + properties.Items[CorrelationProperty] = correlationId; + + var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId; + + Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions); + } + + protected virtual bool ValidateCorrelationId(AuthenticationProperties properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + if (!properties.Items.TryGetValue(CorrelationProperty, out string correlationId)) + { + Logger.CorrelationPropertyNotFound(Options.CorrelationCookie.Name); + return false; + } + + properties.Items.Remove(CorrelationProperty); + + var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId; + + var correlationCookie = Request.Cookies[cookieName]; + if (string.IsNullOrEmpty(correlationCookie)) + { + Logger.CorrelationCookieNotFound(cookieName); + return false; + } + + var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow); + + Response.Cookies.Delete(cookieName, cookieOptions); + + if (!string.Equals(correlationCookie, CorrelationMarker, StringComparison.Ordinal)) + { + Logger.UnexpectedCorrelationCookieValue(cookieName, correlationCookie); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs new file mode 100644 index 0000000000..1bd3b210e5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Authentication.Internal; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the options used by the . + /// + public class RemoteAuthenticationOptions : AuthenticationSchemeOptions + { + private const string CorrelationPrefix = ".AspNetCore.Correlation."; + + private CookieBuilder _correlationCookieBuilder; + + /// + /// Initializes a new . + /// + public RemoteAuthenticationOptions() + { + _correlationCookieBuilder = new CorrelationCookieBuilder(this) + { + Name = CorrelationPrefix, + HttpOnly = true, + SameSite = SameSiteMode.None, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; + } + + /// + /// Checks that the options are valid for a specific scheme + /// + /// The scheme being validated. + public override void Validate(string scheme) + { + base.Validate(scheme); + if (string.Equals(scheme, SignInScheme, StringComparison.Ordinal)) + { + throw new InvalidOperationException(Resources.Exception_RemoteSignInSchemeCannotBeSelf); + } + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + if (CallbackPath == null || !CallbackPath.HasValue) + { + throw new ArgumentException(Resources.FormatException_OptionMustBeProvided(nameof(CallbackPath)), nameof(CallbackPath)); + } + } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with the remote identity provider. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// The HttpMessageHandler used to communicate with remote identity provider. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Used to communicate with the remote identity provider. + /// + public HttpClient Backchannel { get; set; } + + /// + /// Gets or sets the type used to secure data. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the authentication scheme corresponding to the middleware + /// responsible of persisting user's identity after a successful authentication. + /// This value typically corresponds to a cookie middleware registered in the Startup class. + /// When omitted, is used as a fallback value. + /// + public string SignInScheme { get; set; } + + /// + /// Gets or sets the time limit for completing the authentication flow (15 minutes by default). + /// + public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15); + + public new RemoteAuthenticationEvents Events + { + get => (RemoteAuthenticationEvents)base.Events; + set => base.Events = value; + } + + /// + /// Defines whether access and refresh tokens should be stored in the + /// after a successful authorization. + /// This property is set to false by default to reduce + /// the size of the final authentication cookie. + /// + public bool SaveTokens { get; set; } + + /// + /// Determines the settings used to create the correlation cookie before the + /// cookie gets added to the response. + /// + public CookieBuilder CorrelationCookie + { + get => _correlationCookieBuilder; + set => _correlationCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + private class CorrelationCookieBuilder : RequestPathBaseCookieBuilder + { + private readonly RemoteAuthenticationOptions _options; + + public CorrelationCookieBuilder(RemoteAuthenticationOptions remoteAuthenticationOptions) + { + _options = remoteAuthenticationOptions; + } + + protected override string AdditionalPath => _options.CallbackPath; + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var cookieOptions = base.Build(context, expiresFrom); + + if (!Expiration.HasValue || !cookieOptions.Expires.HasValue) + { + cookieOptions.Expires = expiresFrom.Add(_options.RemoteAuthenticationTimeout); + } + + return cookieOptions; + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx new file mode 100644 index 0000000000..9e831dc74f --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + + + The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + + + The AuthenticationTokenProvider's required synchronous events have not been registered. + + + The '{0}' option must be provided. + + + The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs new file mode 100644 index 0000000000..dbd612dc10 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Adds support for SignInAsync + /// + public abstract class SignInAuthenticationHandler : SignOutAuthenticationHandler, IAuthenticationSignInHandler + where TOptions : AuthenticationSchemeOptions, new() + { + public SignInAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignIn); + return (target != null) + ? Context.SignInAsync(target, user, properties) + : HandleSignInAsync(user, properties ?? new AuthenticationProperties()); + } + + /// + /// Override this method to handle SignIn. + /// + /// + /// + /// A Task. + protected abstract Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); + + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs new file mode 100644 index 0000000000..015cb39e05 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Adds support for SignOutAsync + /// + public abstract class SignOutAuthenticationHandler : AuthenticationHandler, IAuthenticationSignOutHandler + where TOptions : AuthenticationSchemeOptions, new() + { + public SignOutAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + return (target != null) + ? Context.SignOutAsync(target, properties) + : HandleSignOutAsync(properties ?? new AuthenticationProperties()); + } + + /// + /// Override this method to handle SignOut. + /// + /// + /// A Task. + protected abstract Task HandleSignOutAsync(AuthenticationProperties properties); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs new file mode 100644 index 0000000000..2320982ce3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides access to the normal system clock with precision in seconds. + /// + public class SystemClock : ISystemClock + { + /// + /// Retrieves the current system time in UTC. + /// + public DateTimeOffset UtcNow + { + get + { + // the clock measures whole seconds only, to have integral expires_in results, and + // because milliseconds do not round-trip serialization formats + var utcNowPrecisionSeconds = new DateTime((DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) * TimeSpan.TicksPerSecond, DateTimeKind.Utc); + return new DateTimeOffset(utcNowPrecisionSeconds); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json new file mode 100644 index 0000000000..08eeb5e7b2 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json @@ -0,0 +1,3330 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Services", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddRemoteScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddPolicyScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Logger", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Logging.ILogger", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UrlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.UrlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Clock", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OptionsMonitor", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Options.IOptionsMonitor", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "System.Object", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimsIssuer", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CurrentUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeAsync", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHandlerAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildRedirectUri", + "Parameters": [ + { + "Name": "targetPath", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ResolveTarget", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateOnceAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateOnceSafeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Schemes", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "schemes", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimsIssuer", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClaimsIssuer", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EventsType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_EventsType", + "Parameters": [ + { + "Name": "value", + "Type": "System.Type" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardDefault", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardDefault", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardAuthenticate", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardChallenge", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardChallenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardForbid", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardForbid", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardSignIn", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardSignIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardSignOut", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardDefaultSelector", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardDefaultSelector", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "T0" + } + ], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TModel", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TData", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.PropertiesSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "serializer", + "Type": "Microsoft.AspNetCore.Authentication.IDataSerializer" + }, + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TData", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Base64UrlTextEncoder", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Decode", + "Parameters": [ + { + "Name": "text", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.TicketSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteIdentity", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteClaim", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "claim", + "Type": "System.Security.Claims.Claim" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadIdentity", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadClaim", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Security.Claims.Claim", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.BaseContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Result", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Result", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.HandleRequestResult" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SkipHandler", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnRemoteFailure", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTicketReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTicketReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteFailure", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RemoteFailureContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TicketReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.TicketReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteFailureContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Failure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + }, + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ResultContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Result", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NoResult", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ReturnUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReturnUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Skipped", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Handle", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SkipHandler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UtcNow", + "Parameters": [], + "ReturnType": "System.DateTimeOffset", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_SignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ShouldHandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GenerateCorrelationId", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidateCorrelationId", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelHttpHandler", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpMessageHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelHttpHandler", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpMessageHandler" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Backchannel", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpClient" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DataProtectionProvider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DataProtectionProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CallbackPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CallbackPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignInScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteAuthenticationTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteAuthenticationTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SaveTokens", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SaveTokens", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CorrelationCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CorrelationCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SystemClock", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.ISystemClock" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_UtcNow", + "Parameters": [], + "ReturnType": "System.DateTimeOffset", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.AuthAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "defaultScheme", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureScheme", + "Type": "System.Action" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddRemoteScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler" + ] + } + ] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs new file mode 100644 index 0000000000..dd5e6fc038 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + /// + /// Base class for authorization handlers that need to be called for a specific requirement type. + /// + public interface IPolicyEvaluator + { + /// + /// Does authentication for and sets the resulting + /// to . If no schemes are set, this is a no-op. + /// + /// The . + /// The . + /// unless all schemes specified by fail to authenticate. + Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context); + + /// + /// Attempts authorization for a policy using . + /// + /// The . + /// The result of a call to . + /// The . + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// Returns if authorization succeeds. + /// Otherwise returns if , otherwise + /// returns + Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj new file mode 100644 index 0000000000..16e4aa2622 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core authorization policy helper classes. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authorization + + + + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs new file mode 100644 index 0000000000..d7d481dcd6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public class PolicyAuthorizationResult + { + private PolicyAuthorizationResult() { } + + /// + /// If true, means the callee should challenge and try again. + /// + public bool Challenged { get; private set; } + + /// + /// Authorization was forbidden. + /// + public bool Forbidden { get; private set; } + + /// + /// Authorization was successful. + /// + public bool Succeeded { get; private set; } + + public static PolicyAuthorizationResult Challenge() + => new PolicyAuthorizationResult { Challenged = true }; + + public static PolicyAuthorizationResult Forbid() + => new PolicyAuthorizationResult { Forbidden = true }; + + public static PolicyAuthorizationResult Success() + => new PolicyAuthorizationResult { Succeeded = true }; + + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs new file mode 100644 index 0000000000..3100ff4d3e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public class PolicyEvaluator : IPolicyEvaluator + { + private readonly IAuthorizationService _authorization; + + /// + /// Constructor + /// + /// The authorization service. + public PolicyEvaluator(IAuthorizationService authorization) + { + _authorization = authorization; + } + + /// + /// Does authentication for and sets the resulting + /// to . If no schemes are set, this is a no-op. + /// + /// The . + /// The . + /// unless all schemes specified by failed to authenticate. + public virtual async Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) + { + if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0) + { + ClaimsPrincipal newPrincipal = null; + foreach (var scheme in policy.AuthenticationSchemes) + { + var result = await context.AuthenticateAsync(scheme); + if (result != null && result.Succeeded) + { + newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal); + } + } + + if (newPrincipal != null) + { + context.User = newPrincipal; + return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes))); + } + else + { + context.User = new ClaimsPrincipal(new ClaimsIdentity()); + return AuthenticateResult.NoResult(); + } + } + + return (context.User?.Identity?.IsAuthenticated ?? false) + ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User")) + : AuthenticateResult.NoResult(); + } + + /// + /// Attempts authorization for a policy using . + /// + /// The . + /// The result of a call to . + /// The . + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// Returns if authorization succeeds. + /// Otherwise returns if , otherwise + /// returns + public virtual async Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + var result = await _authorization.AuthorizeAsync(context.User, resource, policy); + if (result.Succeeded) + { + return PolicyAuthorizationResult.Success(); + } + + // If authentication was successful, return forbidden, otherwise challenge + return (authenticationResult.Succeeded) + ? PolicyAuthorizationResult.Forbid() + : PolicyAuthorizationResult.Challenge(); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs new file mode 100644 index 0000000000..9b72a5cab4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up authorization services in an . + /// + public static class PolicyServiceCollectionExtensions + { + /// + /// Adds authorization policy services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json new file mode 100644 index 0000000000..e8708538d3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json @@ -0,0 +1,211 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization.Policy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "authenticationResult", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Challenged", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Forbidden", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Challenge", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Forbid", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator" + ], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "authenticationResult", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authorization", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthorizationPolicyEvaluator", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs new file mode 100644 index 0000000000..cb3f1b1728 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Specifies that the class or method that this attribute is applied to does not require authorization. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class AllowAnonymousAttribute : Attribute, IAllowAnonymous + { + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs new file mode 100644 index 0000000000..89956c9aa0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Encapsulates a failure result of . + /// + public class AuthorizationFailure + { + private AuthorizationFailure() { } + + /// + /// Failure was due to being called. + /// + public bool FailCalled { get; private set; } + + /// + /// Failure was due to these requirements not being met via . + /// + public IEnumerable FailedRequirements { get; private set; } + + /// + /// Return a failure due to being called. + /// + /// The failure. + public static AuthorizationFailure ExplicitFail() + => new AuthorizationFailure + { + FailCalled = true, + FailedRequirements = new IAuthorizationRequirement[0] + }; + + /// + /// Return a failure due to some requirements not being met via . + /// + /// The requirements that were not met. + /// The failure. + public static AuthorizationFailure Failed(IEnumerable failed) + => new AuthorizationFailure { FailedRequirements = failed }; + + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs new file mode 100644 index 0000000000..a4a923c3c7 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Base class for authorization handlers that need to be called for a specific requirement type. + /// + /// The type of the requirement to handle. + public abstract class AuthorizationHandler : IAuthorizationHandler + where TRequirement : IAuthorizationRequirement + { + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public virtual async Task HandleAsync(AuthorizationHandlerContext context) + { + foreach (var req in context.Requirements.OfType()) + { + await HandleRequirementAsync(context, req); + } + } + + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); + } + + /// + /// Base class for authorization handlers that need to be called for specific requirement and + /// resource types. + /// + /// The type of the requirement to evaluate. + /// The type of the resource to evaluate. + public abstract class AuthorizationHandler : IAuthorizationHandler + where TRequirement : IAuthorizationRequirement + { + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public virtual async Task HandleAsync(AuthorizationHandlerContext context) + { + if (context.Resource is TResource) + { + foreach (var req in context.Requirements.OfType()) + { + await HandleRequirementAsync(context, req, (TResource)context.Resource); + } + } + } + + /// + /// Makes a decision if authorization is allowed based on a specific requirement and resource. + /// + /// The authorization context. + /// The requirement to evaluate. + /// The resource to evaluate. + protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs new file mode 100644 index 0000000000..b6378e4073 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Contains authorization information used by . + /// + public class AuthorizationHandlerContext + { + private HashSet _pendingRequirements; + private bool _failCalled; + private bool _succeedCalled; + + /// + /// Creates a new instance of . + /// + /// A collection of all the for the current authorization action. + /// A representing the current user. + /// An optional resource to evaluate the against. + public AuthorizationHandlerContext( + IEnumerable requirements, + ClaimsPrincipal user, + object resource) + { + if (requirements == null) + { + throw new ArgumentNullException(nameof(requirements)); + } + + Requirements = requirements; + User = user; + Resource = resource; + _pendingRequirements = new HashSet(requirements); + } + + /// + /// The collection of all the for the current authorization action. + /// + public virtual IEnumerable Requirements { get; } + + /// + /// The representing the current user. + /// + public virtual ClaimsPrincipal User { get; } + + /// + /// The optional resource to evaluate the against. + /// + public virtual object Resource { get; } + + /// + /// Gets the requirements that have not yet been marked as succeeded. + /// + public virtual IEnumerable PendingRequirements { get { return _pendingRequirements; } } + + /// + /// Flag indicating whether the current authorization processing has failed. + /// + public virtual bool HasFailed { get { return _failCalled; } } + + /// + /// Flag indicating whether the current authorization processing has succeeded. + /// + public virtual bool HasSucceeded + { + get + { + return !_failCalled && _succeedCalled && !PendingRequirements.Any(); + } + } + + /// + /// Called to indicate will + /// never return true, even if all requirements are met. + /// + public virtual void Fail() + { + _failCalled = true; + } + + /// + /// Called to mark the specified as being + /// successfully evaluated. + /// + /// The requirement whose evaluation has succeeded. + public virtual void Succeed(IAuthorizationRequirement requirement) + { + _succeedCalled = true; + _pendingRequirements.Remove(requirement); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs new file mode 100644 index 0000000000..6899913afb --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Provides programmatic configuration used by and . + /// + public class AuthorizationOptions + { + private IDictionary PolicyMap { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Determines whether authentication handlers should be invoked after a failure. + /// Defaults to true. + /// + public bool InvokeHandlersAfterFailure { get; set; } = true; + + /// + /// Gets or sets the default authorization policy. + /// + /// + /// The default policy is to require any authenticated user. + /// + public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + + /// + /// Add an authorization policy with the provided name. + /// + /// The name of the policy. + /// The authorization policy. + public void AddPolicy(string name, AuthorizationPolicy policy) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + PolicyMap[name] = policy; + } + + /// + /// Add a policy that is built from a delegate with the provided name. + /// + /// The name of the policy. + /// The delegate that will be used to build the policy. + public void AddPolicy(string name, Action configurePolicy) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new AuthorizationPolicyBuilder(); + configurePolicy(policyBuilder); + PolicyMap[name] = policyBuilder.Build(); + } + + /// + /// Returns the policy for the specified name, or null if a policy with the name does not exist. + /// + /// The name of the policy to return. + /// The policy for the specified name, or null if a policy with the name does not exist. + public AuthorizationPolicy GetPolicy(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs new file mode 100644 index 0000000000..36e0ca7c38 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Represents a collection of authorization requirements and the scheme or + /// schemes they are evaluated against, all of which must succeed + /// for authorization to succeed. + /// + public class AuthorizationPolicy + { + /// + /// Creates a new instance of . + /// + /// + /// The list of s which must succeed for + /// this policy to be successful. + /// + /// + /// The authentication schemes the are evaluated against. + /// + public AuthorizationPolicy(IEnumerable requirements, IEnumerable authenticationSchemes) + { + if (requirements == null) + { + throw new ArgumentNullException(nameof(requirements)); + } + + if (authenticationSchemes == null) + { + throw new ArgumentNullException(nameof(authenticationSchemes)); + } + + if (requirements.Count() == 0) + { + throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty); + } + Requirements = new List(requirements).AsReadOnly(); + AuthenticationSchemes = new List(authenticationSchemes).AsReadOnly(); + } + + /// + /// Gets a readonly list of s which must succeed for + /// this policy to be successful. + /// + public IReadOnlyList Requirements { get; } + + /// + /// Gets a readonly list of the authentication schemes the + /// are evaluated against. + /// + public IReadOnlyList AuthenticationSchemes { get; } + + /// + /// Combines the specified into a single policy. + /// + /// The authorization policies to combine. + /// + /// A new which represents the combination of the + /// specified . + /// + public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies) + { + if (policies == null) + { + throw new ArgumentNullException(nameof(policies)); + } + + return Combine((IEnumerable)policies); + } + + /// + /// Combines the specified into a single policy. + /// + /// The authorization policies to combine. + /// + /// A new which represents the combination of the + /// specified . + /// + public static AuthorizationPolicy Combine(IEnumerable policies) + { + if (policies == null) + { + throw new ArgumentNullException(nameof(policies)); + } + + var builder = new AuthorizationPolicyBuilder(); + foreach (var policy in policies) + { + builder.Combine(policy); + } + return builder.Build(); + } + + /// + /// Combines the provided by the specified + /// . + /// + /// A which provides the policies to combine. + /// A collection of authorization data used to apply authorization to a resource. + /// + /// A new which represents the combination of the + /// authorization policies provided by the specified . + /// + public static async Task CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable authorizeData) + { + if (policyProvider == null) + { + throw new ArgumentNullException(nameof(policyProvider)); + } + + if (authorizeData == null) + { + throw new ArgumentNullException(nameof(authorizeData)); + } + + var policyBuilder = new AuthorizationPolicyBuilder(); + var any = false; + foreach (var authorizeDatum in authorizeData) + { + any = true; + var useDefaultPolicy = true; + if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) + { + var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); + if (policy == null) + { + throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy)); + } + policyBuilder.Combine(policy); + useDefaultPolicy = false; + } + var rolesSplit = authorizeDatum.Roles?.Split(','); + if (rolesSplit != null && rolesSplit.Any()) + { + var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); + policyBuilder.RequireRole(trimmedRolesSplit); + useDefaultPolicy = false; + } + var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); + if (authTypesSplit != null && authTypesSplit.Any()) + { + foreach (var authType in authTypesSplit) + { + if (!string.IsNullOrWhiteSpace(authType)) + { + policyBuilder.AuthenticationSchemes.Add(authType.Trim()); + } + } + } + if (useDefaultPolicy) + { + policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync()); + } + } + return any ? policyBuilder.Build() : null; + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs new file mode 100644 index 0000000000..37335df8f2 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs @@ -0,0 +1,250 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Used for building policies during application startup. + /// + public class AuthorizationPolicyBuilder + { + /// + /// Creates a new instance of + /// + /// An array of authentication schemes the policy should be evaluated against. + public AuthorizationPolicyBuilder(params string[] authenticationSchemes) + { + AddAuthenticationSchemes(authenticationSchemes); + } + + /// + /// Creates a new instance of . + /// + /// The to build. + public AuthorizationPolicyBuilder(AuthorizationPolicy policy) + { + Combine(policy); + } + + /// + /// Gets or sets a list of s which must succeed for + /// this policy to be successful. + /// + public IList Requirements { get; set; } = new List(); + + /// + /// Gets or sets a list authentication schemes the + /// are evaluated against. + /// + public IList AuthenticationSchemes { get; set; } = new List(); + + /// + /// Adds the specified authentication to the + /// for this instance. + /// + /// The schemes to add. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes) + { + foreach (var authType in schemes) + { + AuthenticationSchemes.Add(authType); + } + return this; + } + + /// + /// Adds the specified to the + /// for this instance. + /// + /// The authorization requirements to add. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements) + { + foreach (var req in requirements) + { + Requirements.Add(req); + } + return this; + } + + /// + /// Combines the specified into the current instance. + /// + /// The to combine. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + AddAuthenticationSchemes(policy.AuthenticationSchemes.ToArray()); + AddRequirements(policy.Requirements.ToArray()); + return this; + } + + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required. + /// Values the claim must process one or more of for evaluation to succeed. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] requiredValues) + { + if (claimType == null) + { + throw new ArgumentNullException(nameof(claimType)); + } + + return RequireClaim(claimType, (IEnumerable)requiredValues); + } + + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required. + /// Values the claim must process one or more of for evaluation to succeed. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable requiredValues) + { + if (claimType == null) + { + throw new ArgumentNullException(nameof(claimType)); + } + + Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues)); + return this; + } + + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required, which no restrictions on claim value. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireClaim(string claimType) + { + if (claimType == null) + { + throw new ArgumentNullException(nameof(claimType)); + } + + Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues: null)); + return this; + } + + /// + /// Adds a + /// to the current instance. + /// + /// The roles required. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireRole(params string[] roles) + { + if (roles == null) + { + throw new ArgumentNullException(nameof(roles)); + } + + return RequireRole((IEnumerable)roles); + } + + /// + /// Adds a + /// to the current instance. + /// + /// The roles required. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireRole(IEnumerable roles) + { + if (roles == null) + { + throw new ArgumentNullException(nameof(roles)); + } + + Requirements.Add(new RolesAuthorizationRequirement(roles)); + return this; + } + + /// + /// Adds a + /// to the current instance. + /// + /// The user name the current user must possess. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireUserName(string userName) + { + if (userName == null) + { + throw new ArgumentNullException(nameof(userName)); + } + + Requirements.Add(new NameAuthorizationRequirement(userName)); + return this; + } + + /// + /// Adds a to the current instance. + /// + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireAuthenticatedUser() + { + Requirements.Add(new DenyAnonymousAuthorizationRequirement()); + return this; + } + + /// + /// Adds an to the current instance. + /// + /// The handler to evaluate during authorization. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireAssertion(Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Requirements.Add(new AssertionRequirement(handler)); + return this; + } + + /// + /// Adds an to the current instance. + /// + /// The handler to evaluate during authorization. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireAssertion(Func> handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Requirements.Add(new AssertionRequirement(handler)); + return this; + } + + /// + /// Builds a new from the requirements + /// in this instance. + /// + /// + /// A new built from the requirements in this instance. + /// + public AuthorizationPolicy Build() + { + return new AuthorizationPolicy(Requirements, AuthenticationSchemes.Distinct()); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs new file mode 100644 index 0000000000..46dca35fb5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Encapsulates the result of . + /// + public class AuthorizationResult + { + private AuthorizationResult() { } + + /// + /// True if authorization was successful. + /// + public bool Succeeded { get; private set; } + + /// + /// Contains information about why authorization failed. + /// + public AuthorizationFailure Failure { get; private set; } + + /// + /// Returns a successful result. + /// + /// A successful result. + public static AuthorizationResult Success() => new AuthorizationResult { Succeeded = true }; + + public static AuthorizationResult Failed(AuthorizationFailure failure) => new AuthorizationResult { Failure = failure }; + + public static AuthorizationResult Failed() => new AuthorizationResult { Failure = AuthorizationFailure.ExplicitFail() }; + + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs new file mode 100644 index 0000000000..c089fba285 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up authorization services in an . + /// + public static class AuthorizationServiceCollectionExtensions + { + /// + /// Adds authorization services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorization(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + return services; + } + + /// + /// Adds authorization services to the specified . + /// + /// The to add services to. + /// An action delegate to configure the provided . + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorization(this IServiceCollection services, Action configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + services.Configure(configure); + return services.AddAuthorization(); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs new file mode 100644 index 0000000000..866b5dbc51 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Extension methods for . + /// + public static class AuthorizationServiceExtensions + { + /// + /// Checks if a user meets a specific requirement for the specified resource + /// + /// The providing authorization. + /// The user to evaluate the policy against. + /// The resource to evaluate the policy against. + /// The requirement to evaluate the policy against. + /// + /// A flag indicating whether requirement evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (requirement == null) + { + throw new ArgumentNullException(nameof(requirement)); + } + + return service.AuthorizeAsync(user, resource, new IAuthorizationRequirement[] { requirement }); + } + + /// + /// Checks if a user meets a specific authorization policy against the specified resource. + /// + /// The providing authorization. + /// The user to evaluate the policy against. + /// The resource to evaluate the policy against. + /// The policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, AuthorizationPolicy policy) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + return service.AuthorizeAsync(user, resource, policy.Requirements); + } + + /// + /// Checks if a user meets a specific authorization policy against the specified resource. + /// + /// The providing authorization. + /// The user to evaluate the policy against. + /// The policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, AuthorizationPolicy policy) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + return service.AuthorizeAsync(user, resource: null, policy: policy); + } + + /// + /// Checks if a user meets a specific authorization policy against the specified resource. + /// + /// The providing authorization. + /// The user to evaluate the policy against. + /// The name of the policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, string policyName) + { + if (service == null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (policyName == null) + { + throw new ArgumentNullException(nameof(policyName)); + } + + return service.AuthorizeAsync(user, resource: null, policyName: policyName); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs new file mode 100644 index 0000000000..63bfa30d45 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Specifies that the class or method that this attribute is applied to requires the specified authorization. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class AuthorizeAttribute : Attribute, IAuthorizeData + { + /// + /// Initializes a new instance of the class. + /// + public AuthorizeAttribute() { } + + /// + /// Initializes a new instance of the class with the specified policy. + /// + /// The name of the policy to require for authorization. + public AuthorizeAttribute(string policy) + { + Policy = policy; + } + + /// + /// Gets or sets the policy name that determines access to the resource. + /// + public string Policy { get; set; } + + /// + /// Gets or sets a comma delimited list of roles that are allowed to access the resource. + /// + public string Roles { get; set; } + + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + public string AuthenticationSchemes { get; set; } + + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + [Obsolete("Use AuthenticationSchemes instead.", error: false)] + public string ActiveAuthenticationSchemes + { + get => AuthenticationSchemes; + set => AuthenticationSchemes = value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs new file mode 100644 index 0000000000..4bbc283be0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Determines whether an authorization request was successful or not. + /// + public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator + { + /// + /// Determines whether the authorization result was successful or not. + /// + /// The authorization information. + /// The . + public AuthorizationResult Evaluate(AuthorizationHandlerContext context) + => context.HasSucceeded + ? AuthorizationResult.Success() + : AuthorizationResult.Failed(context.HasFailed + ? AuthorizationFailure.ExplicitFail() + : AuthorizationFailure.Failed(context.PendingRequirements)); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs new file mode 100644 index 0000000000..2dae5e5e73 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type used to provide a used for authorization. + /// + public class DefaultAuthorizationHandlerContextFactory : IAuthorizationHandlerContextFactory + { + /// + /// Creates a used for authorization. + /// + /// The requirements to evaluate. + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The . + public virtual AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource) + { + return new AuthorizationHandlerContext(requirements, user, resource); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs new file mode 100644 index 0000000000..d297d4cdc6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// The default implementation of a handler provider, + /// which provides the s for an authorization request. + /// + public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider + { + private readonly IEnumerable _handlers; + + /// + /// Creates a new instance of . + /// + /// The s. + public DefaultAuthorizationHandlerProvider(IEnumerable handlers) + { + if (handlers == null) + { + throw new ArgumentNullException(nameof(handlers)); + } + + _handlers = handlers; + } + + public Task> GetHandlersAsync(AuthorizationHandlerContext context) + => Task.FromResult(_handlers); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs new file mode 100644 index 0000000000..0e4329dcc0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// The default implementation of a policy provider, + /// which provides a for a particular name. + /// + public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private readonly AuthorizationOptions _options; + + /// + /// Creates a new instance of . + /// + /// The options used to configure this instance. + public DefaultAuthorizationPolicyProvider(IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _options = options.Value; + } + + /// + /// Gets the default authorization policy. + /// + /// The default authorization policy. + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(_options.DefaultPolicy); + } + + /// + /// Gets a from the given + /// + /// The policy name to retrieve. + /// The named . + public virtual Task GetPolicyAsync(string policyName) + { + // MVC caches policies specifically for this class, so this method MUST return the same policy per + // policyName for every request or it could allow undesired access. It also must return synchronously. + // A change to either of these behaviors would require shipping a patch of MVC as well. + return Task.FromResult(_options.GetPolicy(policyName)); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs new file mode 100644 index 0000000000..bc5d571c47 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// The default implementation of an . + /// + public class DefaultAuthorizationService : IAuthorizationService + { + private readonly AuthorizationOptions _options; + private readonly IAuthorizationHandlerContextFactory _contextFactory; + private readonly IAuthorizationHandlerProvider _handlers; + private readonly IAuthorizationEvaluator _evaluator; + private readonly IAuthorizationPolicyProvider _policyProvider; + private readonly ILogger _logger; + + /// + /// Creates a new instance of . + /// + /// The used to provide policies. + /// The handlers used to fulfill s. + /// The logger used to log messages, warnings and errors. + /// The used to create the context to handle the authorization. + /// The used to determine if authorization was successful. + /// The used. + public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (policyProvider == null) + { + throw new ArgumentNullException(nameof(policyProvider)); + } + if (handlers == null) + { + throw new ArgumentNullException(nameof(handlers)); + } + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + if (contextFactory == null) + { + throw new ArgumentNullException(nameof(contextFactory)); + } + if (evaluator == null) + { + throw new ArgumentNullException(nameof(evaluator)); + } + + _options = options.Value; + _handlers = handlers; + _policyProvider = policyProvider; + _logger = logger; + _evaluator = evaluator; + _contextFactory = contextFactory; + } + + /// + /// Checks if a user meets a specific set of requirements for the specified resource. + /// + /// The user to evaluate the requirements against. + /// The resource to evaluate the requirements against. + /// The requirements to evaluate. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy otherwise false. + /// + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + { + if (requirements == null) + { + throw new ArgumentNullException(nameof(requirements)); + } + + var authContext = _contextFactory.CreateContext(requirements, user, resource); + var handlers = await _handlers.GetHandlersAsync(authContext); + foreach (var handler in handlers) + { + await handler.HandleAsync(authContext); + if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed) + { + break; + } + } + + var result = _evaluator.Evaluate(authContext); + if (result.Succeeded) + { + _logger.UserAuthorizationSucceeded(); + } + else + { + _logger.UserAuthorizationFailed(); + } + return result; + } + + /// + /// Checks if a user meets a specific authorization policy. + /// + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy otherwise false. + /// + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + { + if (policyName == null) + { + throw new ArgumentNullException(nameof(policyName)); + } + + var policy = await _policyProvider.GetPolicyAsync(policyName); + if (policy == null) + { + throw new InvalidOperationException($"No policy found: {policyName}."); + } + return await this.AuthorizeAsync(user, resource, policy); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs new file mode 100644 index 0000000000..8531c3daab --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Marker interface to enable the . + /// + public interface IAllowAnonymous + { + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs new file mode 100644 index 0000000000..baa6f828cd --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Determines whether an authorization request was successful or not. + /// + public interface IAuthorizationEvaluator + { + /// + /// Determines whether the authorization result was successful or not. + /// + /// The authorization information. + /// The . + AuthorizationResult Evaluate(AuthorizationHandlerContext context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs new file mode 100644 index 0000000000..afe9e43f02 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Classes implementing this interface are able to make a decision if authorization is allowed. + /// + public interface IAuthorizationHandler + { + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization information. + Task HandleAsync(AuthorizationHandlerContext context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs new file mode 100644 index 0000000000..272109eea9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type used to provide a used for authorization. + /// + public interface IAuthorizationHandlerContextFactory + { + /// + /// Creates a used for authorization. + /// + /// The requirements to evaluate. + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The . + AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs new file mode 100644 index 0000000000..7f0d9f5d31 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type which can provide the s for an authorization request. + /// + public interface IAuthorizationHandlerProvider + { + /// + /// Return the handlers that will be called for the authorization request. + /// + /// The . + /// The list of handlers. + Task> GetHandlersAsync(AuthorizationHandlerContext context); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs new file mode 100644 index 0000000000..9e9d0f468a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type which can provide a for a particular name. + /// + public interface IAuthorizationPolicyProvider + { + /// + /// Gets a from the given + /// + /// The policy name to retrieve. + /// The named . + Task GetPolicyAsync(string policyName); + + /// + /// Gets the default authorization policy. + /// + /// The default authorization policy. + Task GetDefaultPolicyAsync(); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs new file mode 100644 index 0000000000..0bdcaff86a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Represents an authorization requirement. + /// + public interface IAuthorizationRequirement + { + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs new file mode 100644 index 0000000000..8976425ba6 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Checks policy based permissions for a user + /// + public interface IAuthorizationService + { + /// + /// Checks if a user meets a specific set of requirements for the specified resource + /// + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The requirements to evaluate. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy; otherwise false. + /// + /// + /// Resource is an optional parameter and may be null. Please ensure that you check it is not + /// null before acting upon it. + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements); + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The user to check the policy against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The name of the policy to check against a specific context. + /// + /// A flag indicating whether authorization has succeeded. + /// Returns a flag indicating whether the user, and optional resource has fulfilled the policy. + /// true when the policy has been fulfilled; otherwise false. + /// + /// + /// Resource is an optional parameter and may be null. Please ensure that you check it is not + /// null before acting upon it. + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs new file mode 100644 index 0000000000..1196db82d4 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Defines the set of data required to apply authorization rules to a resource. + /// + public interface IAuthorizeData + { + /// + /// Gets or sets the policy name that determines access to the resource. + /// + string Policy { get; set; } + + /// + /// Gets or sets a comma delimited list of roles that are allowed to access the resource. + /// + string Roles { get; set; } + + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + string AuthenticationSchemes { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs new file mode 100644 index 0000000000..5fa452b733 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Implements an and + /// that takes a user specified assertion. + /// + public class AssertionRequirement : IAuthorizationHandler, IAuthorizationRequirement + { + /// + /// Function that is called to handle this requirement. + /// + public Func> Handler { get; } + + /// + /// Creates a new instance of . + /// + /// Function that is called to handle this requirement. + public AssertionRequirement(Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Handler = context => Task.FromResult(handler(context)); + } + + /// + /// Creates a new instance of . + /// + /// Function that is called to handle this requirement. + public AssertionRequirement(Func> handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Handler = handler; + } + + /// + /// Calls to see if authorization is allowed. + /// + /// The authorization information. + public async Task HandleAsync(AuthorizationHandlerContext context) + { + if (await Handler(context)) + { + context.Succeed(this); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs new file mode 100644 index 0000000000..93b1deea6d --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Implements an and + /// which requires at least one instance of the specified claim type, and, if allowed values are specified, + /// the claim value must be any of the allowed values. + /// + public class ClaimsAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + /// + /// Creates a new instance of . + /// + /// The claim type that must be present. + /// The optional list of claim values, which, if present, + /// the claim must match. + public ClaimsAuthorizationRequirement(string claimType, IEnumerable allowedValues) + { + if (claimType == null) + { + throw new ArgumentNullException(nameof(claimType)); + } + + ClaimType = claimType; + AllowedValues = allowedValues; + } + + /// + /// Gets the claim type that must be present. + /// + public string ClaimType { get; } + + /// + /// Gets the optional list of claim values, which, if present, + /// the claim must match. + /// + public IEnumerable AllowedValues { get; } + + /// + /// Makes a decision if authorization is allowed based on the claims requirements specified. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimsAuthorizationRequirement requirement) + { + if (context.User != null) + { + var found = false; + if (requirement.AllowedValues == null || !requirement.AllowedValues.Any()) + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)); + } + else + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase) + && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal)); + } + if (found) + { + context.Succeed(requirement); + } + } + return Task.CompletedTask; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs new file mode 100644 index 0000000000..e88cce7aac --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Implements an and + /// which requires the current user must be authenticated. + /// + public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement) + { + var user = context.User; + var userIsAnonymous = + user?.Identity == null || + !user.Identities.Any(i => i.IsAuthenticated); + if (!userIsAnonymous) + { + context.Succeed(requirement); + } + return Task.CompletedTask; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs new file mode 100644 index 0000000000..02ab946fad --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Implements an and + /// which requires the current user name must match the specified value. + /// + public class NameAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + /// + /// Constructs a new instance of . + /// + /// The required name that the current user must have. + public NameAuthorizationRequirement(string requiredName) + { + if (requiredName == null) + { + throw new ArgumentNullException(nameof(requiredName)); + } + + RequiredName = requiredName; + } + + /// + /// Gets the required name that the current user must have. + /// + public string RequiredName { get; } + + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NameAuthorizationRequirement requirement) + { + if (context.User != null) + { + if (context.User.Identities.Any(i => string.Equals(i.Name, requirement.RequiredName))) + { + context.Succeed(requirement); + } + } + return Task.CompletedTask; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs new file mode 100644 index 0000000000..c3f16356d3 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// A helper class to provide a useful which + /// contains a name. + /// + public class OperationAuthorizationRequirement : IAuthorizationRequirement + { + /// + /// The name of this instance of . + /// + public string Name { get; set; } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs new file mode 100644 index 0000000000..6f0b8293f8 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Infrastructure class which allows an to + /// be its own . + /// + public class PassThroughAuthorizationHandler : IAuthorizationHandler + { + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public async Task HandleAsync(AuthorizationHandlerContext context) + { + foreach (var handler in context.Requirements.OfType()) + { + await handler.HandleAsync(context); + } + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs new file mode 100644 index 0000000000..811e17aacd --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization.Infrastructure +{ + /// + /// Implements an and + /// which requires at least one role claim whose value must be any of the allowed roles. + /// + public class RolesAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + /// + /// Creates a new instance of . + /// + /// A collection of allowed roles. + public RolesAuthorizationRequirement(IEnumerable allowedRoles) + { + if (allowedRoles == null) + { + throw new ArgumentNullException(nameof(allowedRoles)); + } + + if (allowedRoles.Count() == 0) + { + throw new InvalidOperationException(Resources.Exception_RoleRequirementEmpty); + } + AllowedRoles = allowedRoles; + } + + /// + /// Gets the collection of allowed roles. + /// + public IEnumerable AllowedRoles { get; } + + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) + { + if (context.User != null) + { + bool found = false; + if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) + { + // Review: What do we want to do here? No roles requested is auto success? + } + else + { + found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); + } + if (found) + { + context.Succeed(requirement); + } + } + return Task.CompletedTask; + } + + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs new file mode 100644 index 0000000000..386df85e09 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _userAuthorizationFailed; + private static Action _userAuthorizationSucceeded; + + static LoggingExtensions() + { + _userAuthorizationSucceeded = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Information, + formatString: "Authorization was successful."); + _userAuthorizationFailed = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Information, + formatString: "Authorization failed."); + } + + public static void UserAuthorizationSucceeded(this ILogger logger) + => _userAuthorizationSucceeded(logger, null); + + public static void UserAuthorizationFailed(this ILogger logger) + => _userAuthorizationFailed(logger, null); + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj b/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj new file mode 100644 index 0000000000..ac4aa6c320 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core authorization classes. +Commonly used types: +Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute +Microsoft.AspNetCore.Authorization.AuthorizeAttribute + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;authorization + false + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..c83fa9ea5e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +// +namespace Microsoft.AspNetCore.Authorization +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authorization.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// AuthorizationPolicy must have at least one requirement. + /// + internal static string Exception_AuthorizationPolicyEmpty + { + get => GetString("Exception_AuthorizationPolicyEmpty"); + } + + /// + /// AuthorizationPolicy must have at least one requirement. + /// + internal static string FormatException_AuthorizationPolicyEmpty() + => GetString("Exception_AuthorizationPolicyEmpty"); + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string Exception_AuthorizationPolicyNotFound + { + get => GetString("Exception_AuthorizationPolicyNotFound"); + } + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string FormatException_AuthorizationPolicyNotFound(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0); + + /// + /// At least one role must be specified. + /// + internal static string Exception_RoleRequirementEmpty + { + get => GetString("Exception_RoleRequirementEmpty"); + } + + /// + /// At least one role must be specified. + /// + internal static string FormatException_RoleRequirementEmpty() + => GetString("Exception_RoleRequirementEmpty"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx new file mode 100644 index 0000000000..a36e55d6b0 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AuthorizationPolicy must have at least one requirement. + + + The AuthorizationPolicy named: '{0}' was not found. + + + At least one role must be specified. + + \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json new file mode 100644 index 0000000000..9910c93f6a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json @@ -0,0 +1,1947 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.AuthorizationServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthorization", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthorization", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configure", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAllowAnonymous" + ], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_FailCalled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FailedRequirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExplicitFail", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "failed", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "T0" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TRequirement", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "T0" + }, + { + "Name": "resource", + "Type": "T1" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TRequirement", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ] + }, + { + "ParameterName": "TResource", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Resource", + "Parameters": [], + "ReturnType": "System.Object", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PendingRequirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasFailed", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasSucceeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Succeed", + "Parameters": [ + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_InvokeHandlersAfterFailure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_InvokeHandlersAfterFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultPolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultPolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "configurePolicy", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policies", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policies", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CombineAsync", + "Parameters": [ + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + }, + { + "Name": "authorizeData", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "authenticationSchemes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Requirements", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthenticationSchemes", + "Parameters": [ + { + "Name": "schemes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddRequirements", + "Parameters": [ + { + "Name": "requirements", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "requiredValues", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "requiredValues", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireRole", + "Parameters": [ + { + "Name": "roles", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireRole", + "Parameters": [ + { + "Name": "roles", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireUserName", + "Parameters": [ + { + "Name": "userName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAuthenticatedUser", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAssertion", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAssertion", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationSchemes", + "Type": "System.String[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "failure", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationFailure" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationServiceExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizeAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizeData" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Policy", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Policy", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Roles", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Roles", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ActiveAuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ActiveAuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policy", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationEvaluator", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Evaluate", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerContextFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlersAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handlers", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationPolicyProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetDefaultPolicyAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationService", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationService" + ], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + }, + { + "Name": "handlers", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "contextFactory", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory" + }, + { + "Name": "evaluator", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAllowAnonymous", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Evaluate", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlersAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultPolicyAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Policy", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Policy", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Roles", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Roles", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.AssertionRequirement", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "System.Func>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClaimType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowedValues", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "allowedValues", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequiredName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requiredName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.OperationAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowedRoles", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "allowedRoles", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs new file mode 100644 index 0000000000..bbb4899c04 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + public class AppendCookieContext + { + public AppendCookieContext(HttpContext context, CookieOptions options, string name, string value) + { + Context = context; + CookieOptions = options; + CookieName = name; + CookieValue = value; + } + + public HttpContext Context { get; } + public CookieOptions CookieOptions { get; } + public string CookieName { get; set; } + public string CookieValue { get; set; } + public bool IsConsentNeeded { get; internal set; } + public bool HasConsent { get; internal set; } + public bool IssueCookie { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs new file mode 100644 index 0000000000..1564193b9e --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.CookiePolicy; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add cookie policy capabilities to an HTTP application pipeline. + /// + public static class CookiePolicyAppBuilderExtensions + { + /// + /// Adds the handler to the specified , which enables cookie policy capabilities. + /// + /// The to add the handler to. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Adds the handler to the specified , which enables cookie policy capabilities. + /// + /// The to add the handler to. + /// A that specifies options for the handler. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app, CookiePolicyOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs new file mode 100644 index 0000000000..1a810b7d55 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + public class CookiePolicyMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public CookiePolicyMiddleware(RequestDelegate next, IOptions options, ILoggerFactory factory) + { + Options = options.Value; + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = factory.CreateLogger(); + } + + public CookiePolicyMiddleware(RequestDelegate next, IOptions options) + { + Options = options.Value; + _next = next; + _logger = NullLogger.Instance; + } + + public CookiePolicyOptions Options { get; set; } + + public Task Invoke(HttpContext context) + { + var feature = context.Features.Get() ?? new ResponseCookiesFeature(context.Features); + var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger); + context.Features.Set(new CookiesWrapperFeature(wrapper)); + context.Features.Set(wrapper); + + return _next(context); + } + + private class CookiesWrapperFeature : IResponseCookiesFeature + { + public CookiesWrapperFeature(ResponseCookiesWrapper wrapper) + { + Cookies = wrapper; + } + + public IResponseCookies Cookies { get; } + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs new file mode 100644 index 0000000000..32d047297a --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.CookiePolicy; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Provides programmatic configuration for the . + /// + public class CookiePolicyOptions + { + /// + /// Affects the cookie's same site attribute. + /// + public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.Lax; + + /// + /// Affects whether cookies must be HttpOnly. + /// + public HttpOnlyPolicy HttpOnly { get; set; } = HttpOnlyPolicy.None; + + /// + /// Affects whether cookies must be Secure. + /// + public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None; + + public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder() + { + Name = ".AspNet.Consent", + Expiration = TimeSpan.FromDays(365), + IsEssential = true, + }; + + /// + /// Checks if consent policies should be evaluated on this request. The default is false. + /// + public Func CheckConsentNeeded { get; set; } + + /// + /// Called when a cookie is appended. + /// + public Action OnAppendCookie { get; set; } + + /// + /// Called when a cookie is deleted. + /// + public Action OnDeleteCookie { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs new file mode 100644 index 0000000000..fd79ea8d4b --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + public class DeleteCookieContext + { + public DeleteCookieContext(HttpContext context, CookieOptions options, string name) + { + Context = context; + CookieOptions = options; + CookieName = name; + } + + public HttpContext Context { get; } + public CookieOptions CookieOptions { get; } + public string CookieName { get; set; } + public bool IsConsentNeeded { get; internal set; } + public bool HasConsent { get; internal set; } + public bool IssueCookie { get; set; } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs new file mode 100644 index 0000000000..82305f4754 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.CookiePolicy +{ + public enum HttpOnlyPolicy + { + None, + Always + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs new file mode 100644 index 0000000000..21b04facc9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _needsConsent; + private static Action _hasConsent; + private static Action _consentGranted; + private static Action _consentWithdrawn; + private static Action _cookieSuppressed; + private static Action _deleteCookieSuppressed; + private static Action _upgradedToSecure; + private static Action _upgradedSameSite; + private static Action _upgradedToHttpOnly; + + static LoggingExtensions() + { + _needsConsent = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Trace, + formatString: "Needs consent: {needsConsent}."); + _hasConsent = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Trace, + formatString: "Has consent: {hasConsent}."); + _consentGranted = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Debug, + formatString: "Consent granted."); + _consentWithdrawn = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Debug, + formatString: "Consent withdrawn."); + _cookieSuppressed = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' suppressed due to consent policy."); + _deleteCookieSuppressed = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "Delete cookie '{key}' suppressed due to developer policy."); + _upgradedToSecure = LoggerMessage.Define( + eventId: 7, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' upgraded to 'secure'."); + _upgradedSameSite = LoggerMessage.Define( + eventId: 8, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' same site mode upgraded to '{mode}'."); + _upgradedToHttpOnly = LoggerMessage.Define( + eventId: 9, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' upgraded to 'httponly'."); + } + + public static void NeedsConsent(this ILogger logger, bool needsConsent) + { + _needsConsent(logger, needsConsent, null); + } + + public static void HasConsent(this ILogger logger, bool hasConsent) + { + _hasConsent(logger, hasConsent, null); + } + + public static void ConsentGranted(this ILogger logger) + { + _consentGranted(logger, null); + } + + public static void ConsentWithdrawn(this ILogger logger) + { + _consentWithdrawn(logger, null); + } + + public static void CookieSuppressed(this ILogger logger, string key) + { + _cookieSuppressed(logger, key, null); + } + + public static void DeleteCookieSuppressed(this ILogger logger, string key) + { + _deleteCookieSuppressed(logger, key, null); + } + + public static void CookieUpgradedToSecure(this ILogger logger, string key) + { + _upgradedToSecure(logger, key, null); + } + + public static void CookieSameSiteUpgraded(this ILogger logger, string key, string mode) + { + _upgradedSameSite(logger, key, mode, null); + } + + public static void CookieUpgradedToHttpOnly(this ILogger logger, string key) + { + _upgradedToHttpOnly(logger, key, null); + } + } +} diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj new file mode 100644 index 0000000000..40f97633ae --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj @@ -0,0 +1,17 @@ + + + + ASP.NET Core cookie policy classes to control the behavior of cookies. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs new file mode 100644 index 0000000000..126c4d7bd5 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature + { + private const string ConsentValue = "yes"; + private readonly ILogger _logger; + private bool? _isConsentNeeded; + private bool? _hasConsent; + + public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger) + { + Context = context; + Feature = feature; + Options = options; + _logger = logger; + } + + private HttpContext Context { get; } + + private IResponseCookiesFeature Feature { get; } + + private IResponseCookies Cookies => Feature.Cookies; + + private CookiePolicyOptions Options { get; } + + public bool IsConsentNeeded + { + get + { + if (!_isConsentNeeded.HasValue) + { + _isConsentNeeded = Options.CheckConsentNeeded == null ? false + : Options.CheckConsentNeeded(Context); + _logger.NeedsConsent(_isConsentNeeded.Value); + } + + return _isConsentNeeded.Value; + } + } + + public bool HasConsent + { + get + { + if (!_hasConsent.HasValue) + { + var cookie = Context.Request.Cookies[Options.ConsentCookie.Name]; + _hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal); + _logger.HasConsent(_hasConsent.Value); + } + + return _hasConsent.Value; + } + } + + public bool CanTrack => !IsConsentNeeded || HasConsent; + + public void GrantConsent() + { + if (!HasConsent && !Context.Response.HasStarted) + { + var cookieOptions = Options.ConsentCookie.Build(Context); + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + Append(Options.ConsentCookie.Name, ConsentValue, cookieOptions); + _logger.ConsentGranted(); + } + _hasConsent = true; + } + + public void WithdrawConsent() + { + if (HasConsent && !Context.Response.HasStarted) + { + var cookieOptions = Options.ConsentCookie.Build(Context); + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + Delete(Options.ConsentCookie.Name, cookieOptions); + _logger.ConsentWithdrawn(); + } + _hasConsent = false; + } + + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + public string CreateConsentCookie() + { + var key = Options.ConsentCookie.Name; + var value = ConsentValue; + var options = Options.ConsentCookie.Build(Context); + ApplyAppendPolicy(ref key, ref value, options); + + var setCookieHeaderValue = new Net.Http.Headers.SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + return setCookieHeaderValue.ToString(); + } + + private bool CheckPolicyRequired() + { + return !CanTrack + || Options.MinimumSameSitePolicy != SameSiteMode.None + || Options.HttpOnly != HttpOnlyPolicy.None + || Options.Secure != CookieSecurePolicy.None; + } + + public void Append(string key, string value) + { + if (CheckPolicyRequired() || Options.OnAppendCookie != null) + { + Append(key, value, new CookieOptions()); + } + else + { + Cookies.Append(key, value); + } + } + + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (ApplyAppendPolicy(ref key, ref value, options)) + { + Cookies.Append(key, value, options); + } + else + { + _logger.CookieSuppressed(key); + } + } + + private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options) + { + var issueCookie = CanTrack || options.IsEssential; + ApplyPolicy(key, options); + if (Options.OnAppendCookie != null) + { + var context = new AppendCookieContext(Context, options, key, value) + { + IsConsentNeeded = IsConsentNeeded, + HasConsent = HasConsent, + IssueCookie = issueCookie, + }; + Options.OnAppendCookie(context); + + key = context.CookieName; + value = context.CookieValue; + issueCookie = context.IssueCookie; + } + + return issueCookie; + } + + public void Delete(string key) + { + if (CheckPolicyRequired() || Options.OnDeleteCookie != null) + { + Delete(key, new CookieOptions()); + } + else + { + Cookies.Delete(key); + } + } + + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Assume you can always delete cookies unless directly overridden in the user event. + var issueCookie = true; + ApplyPolicy(key, options); + if (Options.OnDeleteCookie != null) + { + var context = new DeleteCookieContext(Context, options, key) + { + IsConsentNeeded = IsConsentNeeded, + HasConsent = HasConsent, + IssueCookie = issueCookie, + }; + Options.OnDeleteCookie(context); + + key = context.CookieName; + issueCookie = context.IssueCookie; + } + + if (issueCookie) + { + Cookies.Delete(key, options); + } + else + { + _logger.DeleteCookieSuppressed(key); + } + } + + private void ApplyPolicy(string key, CookieOptions options) + { + switch (Options.Secure) + { + case CookieSecurePolicy.Always: + if (!options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } + break; + case CookieSecurePolicy.SameAsRequest: + // Never downgrade a cookie + if (Context.Request.IsHttps && !options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } + break; + case CookieSecurePolicy.None: + break; + default: + throw new InvalidOperationException(); + } + switch (Options.MinimumSameSitePolicy) + { + case SameSiteMode.None: + break; + case SameSiteMode.Lax: + if (options.SameSite == SameSiteMode.None) + { + options.SameSite = SameSiteMode.Lax; + _logger.CookieSameSiteUpgraded(key, "lax"); + } + break; + case SameSiteMode.Strict: + if (options.SameSite != SameSiteMode.Strict) + { + options.SameSite = SameSiteMode.Strict; + _logger.CookieSameSiteUpgraded(key, "strict"); + } + break; + default: + throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}"); + } + switch (Options.HttpOnly) + { + case HttpOnlyPolicy.Always: + if (!options.HttpOnly) + { + options.HttpOnly = true; + _logger.CookieUpgradedToHttpOnly(key); + } + break; + case HttpOnlyPolicy.None: + break; + default: + throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Options.HttpOnly.ToString()}"); + } + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json new file mode 100644 index 0000000000..01a16c57a9 --- /dev/null +++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json @@ -0,0 +1,548 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.CookiePolicy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseCookiePolicy", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCookiePolicy", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.CookiePolicyOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MinimumSameSitePolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MinimumSameSitePolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConsentCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsentCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CheckConsentNeeded", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CheckConsentNeeded", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnAppendCookie", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAppendCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnDeleteCookie", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnDeleteCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.AppendCookieContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieValue", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieValue", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssueCookie", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssueCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.CookiePolicyOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Options", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "factory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.DeleteCookieContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssueCookie", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssueCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + }, + { + "Name": "name", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Always", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs new file mode 100644 index 0000000000..f1a07c5bf7 --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Owin.Security.DataHandler; +using Microsoft.Owin.Security.DataHandler.Encoder; +using Microsoft.Owin.Security.DataProtection; + +namespace Microsoft.Owin.Security.Interop +{ + public class AspNetTicketDataFormat : SecureDataFormat + { + public AspNetTicketDataFormat(IDataProtector protector) + : base(AspNetTicketSerializer.Default, protector, TextEncodings.Base64Url) + { + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs new file mode 100644 index 0000000000..6a1019fbc8 --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.Owin.Security.DataHandler.Serializer; + +namespace Microsoft.Owin.Security.Interop +{ + // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.DataHandler.TicketSerializer + public class AspNetTicketSerializer : IDataSerializer + { + private const string DefaultStringPlaceholder = "\0"; + private const int FormatVersion = 5; + + public static AspNetTicketSerializer Default { get; } = new AspNetTicketSerializer(); + + public virtual byte[] Serialize(AuthenticationTicket ticket) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, ticket); + } + return memory.ToArray(); + } + } + + public virtual AuthenticationTicket Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + public virtual void Write(BinaryWriter writer, AuthenticationTicket ticket) + { + writer.Write(FormatVersion); + writer.Write(ticket.Identity.AuthenticationType); + + var identity = ticket.Identity; + if (identity == null) + { + throw new ArgumentNullException("ticket.Identity"); + } + + // There is always a single identity + writer.Write(1); + WriteIdentity(writer, identity); + PropertiesSerializer.Write(writer, ticket.Properties); + } + + protected virtual void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) + { + var authenticationType = identity.AuthenticationType ?? string.Empty; + + writer.Write(authenticationType); + WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); + WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); + + // Write the number of claims contained in the identity. + writer.Write(identity.Claims.Count()); + + foreach (var claim in identity.Claims) + { + WriteClaim(writer, claim); + } + + var bootstrap = identity.BootstrapContext as string; + if (!string.IsNullOrEmpty(bootstrap)) + { + writer.Write(true); + writer.Write(bootstrap); + } + else + { + writer.Write(false); + } + + if (identity.Actor != null) + { + writer.Write(true); + WriteIdentity(writer, identity.Actor); + } + else + { + writer.Write(false); + } + } + + protected virtual void WriteClaim(BinaryWriter writer, Claim claim) + { + WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); + writer.Write(claim.Value); + WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); + WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); + WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); + + // Write the number of properties contained in the claim. + writer.Write(claim.Properties.Count); + + foreach (var property in claim.Properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + public virtual AuthenticationTicket Read(BinaryReader reader) + { + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + var scheme = reader.ReadString(); + + // Any identities after the first will be ignored. + var count = reader.ReadInt32(); + if (count < 0) + { + return null; + } + + var identity = ReadIdentity(reader); + var properties = PropertiesSerializer.Read(reader); + + return new AuthenticationTicket(identity, properties); + } + + protected virtual ClaimsIdentity ReadIdentity(BinaryReader reader) + { + var authenticationType = reader.ReadString(); + var nameClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType); + var roleClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType); + + // Read the number of claims contained + // in the serialized identity. + var count = reader.ReadInt32(); + + var identity = new ClaimsIdentity(authenticationType, nameClaimType, roleClaimType); + + for (int index = 0; index != count; ++index) + { + var claim = ReadClaim(reader, identity); + + identity.AddClaim(claim); + } + + // Determine whether the identity + // has a bootstrap context attached. + if (reader.ReadBoolean()) + { + identity.BootstrapContext = reader.ReadString(); + } + + // Determine whether the identity + // has an actor identity attached. + if (reader.ReadBoolean()) + { + identity.Actor = ReadIdentity(reader); + } + + return identity; + } + + protected virtual Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) + { + var type = ReadWithDefault(reader, identity.NameClaimType); + var value = reader.ReadString(); + var valueType = ReadWithDefault(reader, ClaimValueTypes.String); + var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); + var originalIssuer = ReadWithDefault(reader, issuer); + + var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); + + // Read the number of properties stored in the claim. + var count = reader.ReadInt32(); + + for (var index = 0; index != count; ++index) + { + var key = reader.ReadString(); + var propertyValue = reader.ReadString(); + + claim.Properties.Add(key, propertyValue); + } + + return claim; + } + + private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + { + if (string.Equals(value, defaultValue, StringComparison.Ordinal)) + { + writer.Write(DefaultStringPlaceholder); + } + else + { + writer.Write(value); + } + } + + private static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + var value = reader.ReadString(); + if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal)) + { + return defaultValue; + } + return value; + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs b/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs new file mode 100644 index 0000000000..1ae4f00cdb --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs @@ -0,0 +1,280 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Owin.Infrastructure; + +namespace Microsoft.Owin.Security.Interop +{ + // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + public class ChunkingCookieManager : ICookieManager + { + private const string ChunkKeySuffix = "C"; + private const string ChunkCountPrefix = "chunks-"; + + public ChunkingCookieManager() + { + // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case. + // See http://browsercookielimits.x64.me/. + // Leave at least 20 in case CookiePolicy tries to add 'secure' and/or 'httponly'. + ChunkSize = 4070; + } + + /// + /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple + /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all + /// common browsers. + /// + /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain. + /// + public int? ChunkSize { get; set; } + + /// + /// Throw if not all chunks of a cookie are available on a request for re-assembly. + /// + public bool ThrowForPartialCookies { get; set; } + + // Parse the "chunks-XX" to determine how many chunks there should be. + private static int ParseChunksCount(string value) + { + if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal)) + { + var chunksCountString = value.Substring(ChunkCountPrefix.Length); + int chunksCount; + if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount)) + { + return chunksCount; + } + } + return 0; + } + + /// + /// Get the reassembled cookie. Non chunked cookies are returned normally. + /// Cookies with missing chunks just have their "chunks-XX" header returned. + /// + /// + /// + /// The reassembled cookie, if any, or null. + public string GetRequestCookie(IOwinContext context, string key) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var requestCookies = context.Request.Cookies; + var value = requestCookies[key]; + var chunksCount = ParseChunksCount(value); + if (chunksCount > 0) + { + var chunks = new string[chunksCount]; + for (var chunkId = 1; chunkId <= chunksCount; chunkId++) + { + var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)]; + if (string.IsNullOrEmpty(chunk)) + { + if (ThrowForPartialCookies) + { + var totalSize = 0; + for (int i = 0; i < chunkId - 1; i++) + { + totalSize += chunks[i].Length; + } + throw new FormatException( + string.Format(CultureInfo.CurrentCulture, + "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.", + chunkId - 1, chunksCount, totalSize)); + } + // Missing chunk, abort by returning the original cookie value. It may have been a false positive? + return value; + } + + chunks[chunkId - 1] = chunk; + } + + return string.Join(string.Empty, chunks); + } + return value; + } + + /// + /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit + /// then it will be broken down into multiple cookies as follows: + /// Set-Cookie: CookieName=chunks-3; path=/ + /// Set-Cookie: CookieNameC1=Segment1; path=/ + /// Set-Cookie: CookieNameC2=Segment2; path=/ + /// Set-Cookie: CookieNameC3=Segment3; path=/ + /// + /// + /// + /// + /// + public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + var expiresHasValue = options.Expires.HasValue; + + var templateLength = key.Length + "=".Length + + (domainHasValue ? "; domain=".Length + options.Domain.Length : 0) + + (pathHasValue ? "; path=".Length + options.Path.Length : 0) + + (expiresHasValue ? "; expires=ddd, dd-MMM-yyyy HH:mm:ss GMT".Length : 0) + + (options.Secure ? "; secure".Length : 0) + + (options.HttpOnly ? "; HttpOnly".Length : 0); + + // Normal cookie + var responseCookies = context.Response.Cookies; + if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length) + { + responseCookies.Append(key, value, options); + } + else if (ChunkSize.Value < templateLength + 10) + { + // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX". + // No room for data, we can't chunk the options and name + throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data."); + } + else + { + // Break the cookie down into multiple cookies. + // Key = CookieName, value = "Segment1Segment2Segment2" + // Set-Cookie: CookieName=chunks-3; path=/ + // Set-Cookie: CookieNameC1="Segment1"; path=/ + // Set-Cookie: CookieNameC2="Segment2"; path=/ + // Set-Cookie: CookieNameC3="Segment3"; path=/ + var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid. + var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie); + + responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options); + + var offset = 0; + for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++) + { + var remainingLength = value.Length - offset; + var length = Math.Min(dataSizePerCookie, remainingLength); + var segment = value.Substring(offset, length); + offset += length; + + responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options); + } + } + } + + /// + /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on + /// the request, delete each chunk. + /// + /// + /// + /// + public void DeleteCookie(IOwinContext context, string key, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var keys = new List(); + keys.Add(key + "="); + + var requestCookie = context.Request.Cookies[key]; + var chunks = ParseChunksCount(requestCookie); + if (chunks > 0) + { + for (int i = 1; i <= chunks + 1; i++) + { + var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture); + keys.Add(subkey + "="); + } + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + Func predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase)); + if (domainHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => predicate(value); + } + + var responseHeaders = context.Response.Headers; + string[] existingValues; + if (responseHeaders.TryGetValue(Constants.Headers.SetCookie, out existingValues) && existingValues != null) + { + responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + } + + AppendResponseCookie( + context, + key, + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + for (int i = 1; i <= chunks; i++) + { + AppendResponseCookie( + context, + key + "C" + i.ToString(CultureInfo.InvariantCulture), + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } + } +} diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs b/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs new file mode 100644 index 0000000000..1e75761b70 --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Owin.Security.Interop +{ + internal static class Constants + { + internal static class Headers + { + internal const string SetCookie = "Set-Cookie"; + } + } +} diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs b/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs new file mode 100644 index 0000000000..7313588948 --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.DataProtection; + +namespace Microsoft.Owin.Security.Interop +{ + /// + /// Converts an to an + /// . + /// + public sealed class DataProtectorShim : Microsoft.Owin.Security.DataProtection.IDataProtector + { + private readonly IDataProtector _protector; + + public DataProtectorShim(IDataProtector protector) + { + _protector = protector; + } + + public byte[] Protect(byte[] userData) + { + return _protector.Protect(userData); + } + + public byte[] Unprotect(byte[] protectedData) + { + return _protector.Unprotect(protectedData); + } + } +} \ No newline at end of file diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj b/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj new file mode 100644 index 0000000000..a12bc65637 --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj @@ -0,0 +1,16 @@ + + + + A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security and Microsoft.AspNetCore.Authentication. + net461 + $(NoWarn);CS1591 + true + aspnetcore;katana;owin;security + + + + + + + + diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs b/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..490fa7cb2a --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a7922dd8-09f1-43e4-938b-cc523ea08898")] + diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json b/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json new file mode 100644 index 0000000000..bfc0c0076d --- /dev/null +++ b/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json @@ -0,0 +1,372 @@ +{ + "AssemblyIdentity": "Microsoft.Owin.Security.Interop, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Owin.Security.Interop.AspNetTicketDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.Owin.Security.DataHandler.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.Owin.Security.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.Owin.Security.AuthenticationTicket" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket", + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "ticket", + "Type": "Microsoft.Owin.Security.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteIdentity", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteClaim", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "claim", + "Type": "System.Security.Claims.Claim" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadIdentity", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadClaim", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Security.Claims.Claim", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.ChunkingCookieManager", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Owin.Infrastructure.ICookieManager" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ChunkSize", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ChunkSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ThrowForPartialCookies", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ThrowForPartialCookies", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.Owin.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.Owin.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.DataProtectorShim", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "ImplementedInterfaces": [ + "Microsoft.Owin.Security.DataProtection.IDataProtector" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "userData", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Byte[]", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedData", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Byte[]", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Security/test/Directory.Build.props b/src/Security/test/Directory.Build.props new file mode 100644 index 0000000000..b842a48317 --- /dev/null +++ b/src/Security/test/Directory.Build.props @@ -0,0 +1,19 @@ + + + + + netcoreapp2.1 + $(DeveloperBuildTestTfms) + $(StandardTestTfms);netcoreapp2.0 + $(StandardTestTfms);net461 + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs new file mode 100644 index 0000000000..b09f13cab9 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs @@ -0,0 +1,196 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationMiddlewareTests + { + [Fact] + public async Task OnlyInvokesCanHandleRequestHandlers() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + }) + .ConfigureServices(services => services.AddAuthentication(o => + { + o.AddScheme("Skip", s => + { + s.HandlerType = typeof(SkipHandler); + }); + // Won't get hit since CanHandleRequests is false + o.AddScheme("throws", s => + { + s.HandlerType = typeof(ThrowsHandler); + }); + o.AddScheme("607", s => + { + s.HandlerType = typeof(SixOhSevenHandler); + }); + // Won't get run since 607 will finish + o.AddScheme("305", s => + { + s.HandlerType = typeof(ThreeOhFiveHandler); + }); + })); + var server = new TestServer(builder); + var response = await server.CreateClient().GetAsync("http://example.com/"); + Assert.Equal(607, (int)response.StatusCode); + } + + private class ThreeOhFiveHandler : StatusCodeHandler { + public ThreeOhFiveHandler() : base(305) { } + } + + private class SixOhSevenHandler : StatusCodeHandler + { + public SixOhSevenHandler() : base(607) { } + } + + private class SevenOhSevenHandler : StatusCodeHandler + { + public SevenOhSevenHandler() : base(707) { } + } + + private class StatusCodeHandler : IAuthenticationRequestHandler + { + private HttpContext _context; + private int _code; + + public StatusCodeHandler(int code) + { + _code = code; + } + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + _context.Response.StatusCode = _code; + return Task.FromResult(true); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class ThrowsHandler : IAuthenticationHandler + { + private HttpContext _context; + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class SkipHandler : IAuthenticationRequestHandler + { + private HttpContext _context; + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + return Task.FromResult(false); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs new file mode 100644 index 0000000000..3195298c0d --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class Base64UrlTextEncoderTests + { + [Fact] + public void DataOfVariousLengthRoundTripCorrectly() + { + for (int length = 0; length != 256; ++length) + { + var data = new byte[length]; + for (int index = 0; index != length; ++index) + { + data[index] = (byte)(5 + length + (index * 23)); + } + string text = Base64UrlTextEncoder.Encode(data); + byte[] result = Base64UrlTextEncoder.Decode(text); + + for (int index = 0; index != length; ++index) + { + Assert.Equal(data[index], result[index]); + } + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs new file mode 100644 index 0000000000..b083e9d76d --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public class ClaimActionTests + { + [Fact] + public void CanMapSingleValueUserDataToClaim() + { + var userData = new JObject + { + ["name"] = "test" + }; + + var identity = new ClaimsIdentity(); + + var action = new JsonKeyClaimAction("name", "name", "name"); + action.Run(userData, identity, "iss"); + + Assert.Equal("name", identity.FindFirst("name").Type); + Assert.Equal("test", identity.FindFirst("name").Value); + } + + [Fact] + public void CanMapArrayValueUserDataToClaims() + { + var userData = new JObject + { + ["role"] = new JArray { "role1", "role2" } + }; + + var identity = new ClaimsIdentity(); + + var action = new JsonKeyClaimAction("role", "role", "role"); + action.Run(userData, identity, "iss"); + + var roleClaims = identity.FindAll("role").ToList(); + Assert.Equal(2, roleClaims.Count); + Assert.Equal("role", roleClaims[0].Type); + Assert.Equal("role1", roleClaims[0].Value); + Assert.Equal("role", roleClaims[1].Type); + Assert.Equal("role2", roleClaims[1].Value); + } + + [Fact] + public void MapAllSucceeds() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Equal("name0", identity.FindFirst("name0").Type); + Assert.Equal("value0", identity.FindFirst("name0").Value); + Assert.Equal("name1", identity.FindFirst("name1").Type); + Assert.Equal("value1", identity.FindFirst("name1").Value); + } + + [Fact] + public void MapAllAllowesDulicateKeysWithUniqueValues() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim("name0", "value2")); + identity.AddClaim(new Claim("name1", "value3")); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Equal(2, identity.FindAll("name0").Count()); + Assert.Equal(2, identity.FindAll("name1").Count()); + } + + [Fact] + public void MapAllSkipsDuplicateValues() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim("name0", "value0")); + identity.AddClaim(new Claim("name1", "value1")); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Single(identity.FindAll("name0")); + Assert.Single(identity.FindAll("name1")); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs new file mode 100644 index 0000000000..766d1e2e53 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs @@ -0,0 +1,1989 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + public class CookieTests + { + private TestClock _clock = new TestClock(); + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddCookie(o => o.ForwardDefault = "auth1"); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, forwardDefault.SignInCount); + } + + [Fact] + public async Task ForwardSignInWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardSignIn = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, specific.SignInCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignOutCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSignOutWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, selector.SignOutCount); + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, selector.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, forwardDefault.SignInCount); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddCookie(o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddCookie(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("CookieAuthenticationHandler", scheme.HandlerType.Name); + Assert.Null(scheme.DisplayName); + } + + [Fact] + public async Task NormalRequestPassesThrough() + { + var server = CreateServer(s => { }); + var response = await server.CreateClient().GetAsync("http://example.com/normal"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task AjaxLoginRedirectToReturnUrlTurnsInto200WithLocationHeader() + { + var server = CreateServer(o => o.LoginPath = "/login"); + var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest"); + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("http://example.com/login", responded.Single()); + } + + [Fact] + public async Task AjaxForbidTurnsInto403WithLocationHeader() + { + var server = CreateServer(o => o.AccessDeniedPath = "/denied"); + var transaction = await SendAsync(server, "http://example.com/forbid?X-Requested-With=XMLHttpRequest"); + Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("http://example.com/denied", responded.Single()); + } + + [Fact] + public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader() + { + var server = CreateServer(o => o.LogoutPath = "/signout"); + var transaction = await SendAsync(server, "http://example.com/signout?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("/", responded.Single()); + } + + [Fact] + public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader() + { + var server = CreateServer(s => { }); + var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + var responded = transaction.Response.Headers.GetValues("Location"); + Assert.Single(responded); + Assert.StartsWith("http://example.com/Account/Login", responded.Single()); + } + + [Fact] + public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() + { + var server = CreateServer(s => { }); + + var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location; + Assert.Equal("http://example.com/Account/Login?ReturnUrl=%2FCustomRedirect", location.ToString()); + } + + private Task SignInAsAlice(HttpContext context) + { + var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")); + user.AddClaim(new Claim("marker", "true")); + return context.SignInAsync("Cookies", + new ClaimsPrincipal(user), + new AuthenticationProperties()); + } + + private Task SignInAsWrong(HttpContext context) + { + return context.SignInAsync("Oops", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties()); + } + + private Task SignOutAsWrong(HttpContext context) + { + return context.SignOutAsync("Oops"); + } + + [Fact] + public async Task SignInCausesDefaultCookieToBeCreated() + { + var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o => + { + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + }), SignInAsAlice); + + var transaction = await SendAsync(server, "http://example.com/testpath"); + + var setCookie = transaction.SetCookie; + Assert.StartsWith("TestCookie=", setCookie); + Assert.Contains("; path=/", setCookie); + Assert.Contains("; httponly", setCookie); + Assert.Contains("; samesite=", setCookie); + Assert.DoesNotContain("; expires=", setCookie); + Assert.DoesNotContain("; domain=", setCookie); + Assert.DoesNotContain("; secure", setCookie); + } + + [Fact] + public async Task CookieExpirationOptionIsIgnored() + { + var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + // this is currently ignored. Users should set o.ExpireTimeSpan instead + o.Cookie.Expiration = TimeSpan.FromDays(10); + }), SignInAsAlice); + + var transaction = await SendAsync(server, "http://example.com/testpath"); + + var setCookie = transaction.SetCookie; + Assert.StartsWith("TestCookie=", setCookie); + Assert.DoesNotContain("; expires=", setCookie); + } + + [Fact] + public async Task SignInWrongAuthTypeThrows() + { + var server = CreateServer(o => + { + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + }, SignInAsWrong); + + await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); + } + + [Fact] + public async Task SignOutWrongAuthTypeThrows() + { + var server = CreateServer(o => + { + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + }, SignOutAsWrong); + + await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); + } + + [Theory] + [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)] + [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)] + [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)] + public async Task SecureSignInCausesSecureOnlyCookieByDefault( + CookieSecurePolicy cookieSecurePolicy, + string requestUri, + bool shouldBeSecureOnly) + { + var server = CreateServer(o => + { + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + o.Cookie.SecurePolicy = cookieSecurePolicy; + }, SignInAsAlice); + + var transaction = await SendAsync(server, requestUri); + var setCookie = transaction.SetCookie; + + if (shouldBeSecureOnly) + { + Assert.Contains("; secure", setCookie); + } + else + { + Assert.DoesNotContain("; secure", setCookie); + } + } + + [Fact] + public async Task CookieOptionsAlterSetCookieHeader() + { + var server1 = CreateServer(o => + { + o.Cookie.Name = "TestCookie"; + o.Cookie.Path = "/foo"; + o.Cookie.Domain = "another.com"; + o.Cookie.SecurePolicy = CookieSecurePolicy.Always; + o.Cookie.SameSite = SameSiteMode.None; + o.Cookie.HttpOnly = true; + }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); + + var transaction1 = await SendAsync(server1, "http://example.com/base/testpath"); + + var setCookie1 = transaction1.SetCookie; + + Assert.Contains("TestCookie=", setCookie1); + Assert.Contains(" path=/foo", setCookie1); + Assert.Contains(" domain=another.com", setCookie1); + Assert.Contains(" secure", setCookie1); + Assert.DoesNotContain(" samesite", setCookie1); + Assert.Contains(" httponly", setCookie1); + + var server2 = CreateServer(o => + { + o.Cookie.Name = "SecondCookie"; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + o.Cookie.SameSite = SameSiteMode.Strict; + o.Cookie.HttpOnly = false; + }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); + + var transaction2 = await SendAsync(server2, "http://example.com/base/testpath"); + + var setCookie2 = transaction2.SetCookie; + + Assert.Contains("SecondCookie=", setCookie2); + Assert.Contains(" path=/base", setCookie2); + Assert.Contains(" samesite=strict", setCookie2); + Assert.DoesNotContain(" domain=", setCookie2); + Assert.DoesNotContain(" secure", setCookie2); + Assert.DoesNotContain(" httponly", setCookie2); + } + + [Fact] + public async Task CookieContainsIdentity() + { + var server = CreateServer(o => { }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieAppliesClaimsTransform() + { + var server = CreateServer(o => { }, + SignInAsAlice, + baseAddress: null, + claimsTransform: true); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + Assert.Equal("yup", FindClaimValue(transaction2, "xform")); + Assert.Null(FindClaimValue(transaction2, "sync")); + } + + [Fact] + public async Task CookieStopsWorkingAfterExpiration() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + _clock.Add(TimeSpan.FromMinutes(7)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + _clock.Add(TimeSpan.FromMinutes(7)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + Assert.Null(transaction4.SetCookie); + Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieExpirationCanBeOverridenInSignin() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties() { ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5)) })); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + _clock.Add(TimeSpan.FromMinutes(3)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + _clock.Add(TimeSpan.FromMinutes(3)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + Assert.Null(transaction4.SetCookie); + Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); + } + + [Fact] + public async Task ExpiredCookieWithValidatorStillExpired() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + _clock.Add(TimeSpan.FromMinutes(11)); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieCanBeRejectedAndSignedOutByValidator() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.RejectPrincipal(); + ctx.HttpContext.SignOutAsync("Cookies"); + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Contains(".AspNetCore.Cookies=; expires=", transaction2.SetCookie); + Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieNotRenewedAfterSignOut() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + // renews on every request + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + var transaction3 = await server.SendAsync("http://example.com/normal", transaction1.CookieNameValue); + Assert.NotNull(transaction3.SetCookie[0]); + + // signout wins over renew + var transaction4 = await server.SendAsync("http://example.com/signout", transaction3.SetCookie[0]); + Assert.Single(transaction4.SetCookie); + Assert.Contains(".AspNetCore.Cookies=; expires=", transaction4.SetCookie[0]); + } + + [Fact] + public async Task CookieCanBeRenewedByValidator() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(6)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction4.SetCookie); + Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieCanBeReplacedByValidator() + { + var server = CreateServer(o => + { + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice2", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieCanBeUpdatedByValidatorDuringRefresh() + { + var replace = false; + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + if (replace) + { + ctx.ShouldRenew = true; + ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); + ctx.Properties.Items["updated"] = "yes"; + } + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + Assert.Null(FindPropertiesValue(transaction3, "updated")); + + replace = true; + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice2", FindClaimValue(transaction4, ClaimTypes.Name)); + Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); + + replace = false; + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Equal("Alice2", FindClaimValue(transaction5, ClaimTypes.Name)); + Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); + } + + [Fact] + public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(6)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(11)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieCanBeRenewedByValidatorWithModifiedProperties() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + var id = ctx.Principal.Identities.First(); + var claim = id.FindFirst("counter"); + if (claim == null) + { + id.AddClaim(new Claim("counter", "1")); + } + else + { + id.RemoveClaim(claim); + id.AddClaim(new Claim("counter", claim.Value + "1")); + } + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("1", FindClaimValue(transaction2, "counter")); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("11", FindClaimValue(transaction3, "counter")); + + _clock.Add(TimeSpan.FromMinutes(6)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("111", FindClaimValue(transaction4, "counter")); + + _clock.Add(TimeSpan.FromMinutes(11)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Null(FindClaimValue(transaction5, "counter")); + } + + [Fact] + public async Task CookieValidatorOnlyCalledOnce() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(6)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction4.SetCookie); + Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) + { + DateTimeOffset? lastValidateIssuedDate = null; + DateTimeOffset? lastExpiresDate = null; + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = sliding; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + lastValidateIssuedDate = ctx.Properties.IssuedUtc; + lastExpiresDate = ctx.Properties.ExpiresUtc; + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + Assert.NotNull(lastValidateIssuedDate); + Assert.NotNull(lastExpiresDate); + + var firstIssueDate = lastValidateIssuedDate; + var firstExpiresDate = lastExpiresDate; + + _clock.Add(TimeSpan.FromMinutes(1)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(2)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + Assert.NotEqual(lastValidateIssuedDate, firstIssueDate); + Assert.NotEqual(firstExpiresDate, lastExpiresDate); + } + + [Fact] + public async Task CookieExpirationCanBeOverridenInEvent() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents() + { + OnSigningIn = context => + { + context.Properties.ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5)); + return Task.FromResult(0); + } + }; + }, + SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(3)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(3)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction4.SetCookie); + Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieIsRenewedWithSlidingExpiration() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = true; + }, + SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + // transaction4 should arrive with a new SetCookie value + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = true; + o.Events.OnValidatePrincipal = c => + { + // https://github.com/aspnet/Security/issues/1607 + // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal. + Assert.Single(c.Principal.Identities); + Assert.True(c.Principal.Identities.First().HasClaim("marker", "true")); + return Task.CompletedTask; + }; + }, + SignInAsAlice, + claimsTransform: true); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + // transaction4 should arrive with a new SetCookie value + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieUsesPathBaseByDefault() + { + var server = CreateServer(o => { }, + context => + { + Assert.Equal(new PathString("/base"), context.Request.PathBase); + return context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); + }, + new Uri("http://example.com/base")); + + var transaction1 = await SendAsync(server, "http://example.com/base/testpath"); + Assert.Contains("path=/base", transaction1.SetCookie); + } + + [Fact] + public async Task CookieChallengeRedirectsToLoginWithoutCookie() + { + var server = CreateServer(o => { }, SignInAsAlice); + + var url = "http://example.com/challenge"; + var transaction = await SendAsync(server, url); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location; + Assert.Equal("/Account/Login", location.LocalPath); + } + + [Fact] + public async Task CookieForbidRedirectsWithoutCookie() + { + var server = CreateServer(o => { }, SignInAsAlice); + + var url = "http://example.com/forbid"; + var transaction = await SendAsync(server, url); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location; + Assert.Equal("/Account/AccessDenied", location.LocalPath); + } + + [Fact] + public async Task CookieChallengeRedirectsWithLoginPath() + { + var server = CreateServer(o => + { + o.LoginPath = new PathString("/page"); + }); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue); + + Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); + } + + [Fact] + public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated() + { + var server = CreateServer(o => + { + o.LoginPath = new PathString("/page"); + }); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth) + { + var builder = new WebHostBuilder() + .Configure(app => + { + if (useAuth) + { + app.UseAuthentication(); + } + app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + }) + .ConfigureServices(s => s.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var location = transaction.Response.Headers.Location; + if (useAuth) + { + Assert.Equal("/page", location.LocalPath); + } + else + { + Assert.Equal("/login/page", location.LocalPath); + } + Assert.Equal("?ReturnUrl=%2F", location.Query); + } + + [ConditionalFact(Skip = "Revisit, exception no longer thrown")] + public async Task ChallengeDoesNotSet401OnUnauthorized() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + await Assert.ThrowsAsync(() => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme)); + }); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie()); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task CanConfigureDefaultCookieInstance() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity()))); + }) + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(); + services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, + o => o.Cookie.Name = "One"); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com"); + + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.StartsWith("One=", transaction.SetCookie[0]); + } + + [Fact] + public async Task CanConfigureNamedCookieInstance() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookie1", new ClaimsPrincipal(new ClaimsIdentity()))); + }) + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie("Cookie1"); + services.Configure("Cookie1", + o => o.Cookie.Name = "One"); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com"); + + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.StartsWith("One=", transaction.SetCookie[0]); + } + + [Fact] + public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", + new ClaimsPrincipal()))); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.NotNull(transaction.SetCookie); + } + + [Fact] + public async Task MapWillNotAffectSignInRedirectToReturnUrl() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Map("/login", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal()))); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))); + + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.NotNull(transaction.SetCookie); + + var location = transaction.Response.Headers.Location; + Assert.Equal("/page", location.OriginalString); + } + + [Fact] + public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]); + } + + [Fact] + public async Task MapWillNotAffectSignOutRedirectToReturnUrl() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Map("/logout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]); + + var location = transaction.Response.Headers.Location; + Assert.Equal("/page", location.OriginalString); + } + + [Fact] + public async Task MapWillNotAffectAccessDenied() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))); + var server = new TestServer(builder); + var transaction = await server.SendAsync("http://example.com/forbid"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var location = transaction.Response.Headers.Location; + Assert.Equal("/denied", location.LocalPath); + } + + [Fact] + public async Task NestedMapWillNotAffectLogin() + { + var builder = new WebHostBuilder() + .Configure(app => + app.Map("/base", map => + { + map.UseAuthentication(); + map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + })) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))); + var server = new TestServer(builder); + var transaction = await server.SendAsync("http://example.com/base/login"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var location = transaction.Response.Headers.Location; + Assert.Equal("/base/page", location.LocalPath); + Assert.Equal("?ReturnUrl=%2F", location.Query); + } + + [Theory] + [InlineData("/redirect_test")] + [InlineData("http://example.com/redirect_to")] + public async Task RedirectUriIsHoneredAfterSignin(string redirectUrl) + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.Cookie.Name = "TestCookie"; + }, + async context => + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), + new AuthenticationProperties { RedirectUri = redirectUrl }) + ); + var transaction = await SendAsync(server, "http://example.com/testpath"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal(redirectUrl, transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task RedirectUriInQueryIsHoneredAfterSignin() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/ret_path_2", transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task AbsoluteRedirectUriInQueryStringIsRejected() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=http%3A%2F%2Fexample.com%2Fredirect_to"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task EnsurePrecedenceOfRedirectUriAfterSignin() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), + new AuthenticationProperties { RedirectUri = "/redirect_test" }); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/redirect_test", transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task NestedMapWillNotAffectAccessDenied() + { + var builder = new WebHostBuilder() + .Configure(app => + app.Map("/base", map => + { + map.UseAuthentication(); + map.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); + })) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))); + var server = new TestServer(builder); + var transaction = await server.SendAsync("http://example.com/base/forbid"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var location = transaction.Response.Headers.Location; + Assert.Equal("/base/denied", location.LocalPath); + } + + [Fact] + public async Task CanSpecifyAndShareDataProtector() + { + + var dp = new NoOpDataProtector(); + var builder1 = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use((context, next) => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties())); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => + { + o.TicketDataFormat = new TicketDataFormat(dp); + o.Cookie.Name = "Cookie"; + })); + var server1 = new TestServer(builder1); + + var transaction = await SendAsync(server1, "http://example.com/stuff"); + Assert.NotNull(transaction.SetCookie); + + var builder2 = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var result = await context.AuthenticateAsync("Cookies"); + Describe(context.Response, result); + }); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o => + { + o.Cookie.Name = "Cookie"; + o.TicketDataFormat = new TicketDataFormat(dp); + })); + var server2 = new TestServer(builder2); + var transaction2 = await SendAsync(server2, "http://example.com/stuff", transaction.CookieNameValue); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + // Issue: https://github.com/aspnet/Security/issues/949 + [Fact] + public async Task NullExpiresUtcPropertyIsGuarded() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => + { + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = context => + { + context.Properties.ExpiresUtc = null; + context.ShouldRenew = true; + return Task.FromResult(0); + } + }; + })) + .Configure(app => + { + app.UseAuthentication(); + + app.Run(async context => + { + if (context.Request.Path == "/signin") + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); + } + else + { + await context.Response.WriteAsync("ha+1"); + } + }); + }); + + var server = new TestServer(builder); + + var cookie = (await server.SendAsync("http://www.example.com/signin")).SetCookie.FirstOrDefault(); + Assert.NotNull(cookie); + + var transaction = await server.SendAsync("http://www.example.com/", cookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + private class NoOpDataProtector : IDataProtector + { + public IDataProtector CreateProtector(string purpose) + { + return this; + } + + public byte[] Protect(byte[] plaintext) + { + return plaintext; + } + + public byte[] Unprotect(byte[] protectedData) + { + return protectedData; + } + } + + private static string FindClaimValue(Transaction transaction, string claimType) + { + var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + private static string FindPropertiesValue(Transaction transaction, string key) + { + var property = transaction.ResponseElement.Elements("extra").SingleOrDefault(elt => elt.Attribute("type").Value == key); + if (property == null) + { + return null; + } + return property.Attribute("value").Value; + } + + private static async Task GetAuthData(TestServer server, string url, string cookie) + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Cookie", cookie); + + var response2 = await server.CreateClient().SendAsync(request); + var text = await response2.Content.ReadAsStringAsync(); + var me = XElement.Parse(text); + return me; + } + + private class ClaimsTransformer : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal p) + { + var firstId = p.Identities.First(); + if (firstId.HasClaim("marker", "true")) + { + firstId.RemoveClaim(firstId.FindFirst("marker")); + } + // TransformAsync could be called twice on one request if you have a default scheme and also + // call AuthenticateAsync. + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return Task.FromResult(p); + } + } + + private TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null, bool claimsTransform = false) + => CreateServerWithServices(s => + { + s.AddSingleton(_clock); + s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(configureOptions); + if (claimsTransform) + { + s.AddSingleton(); + } + }, testpath, baseAddress); + + private static TestServer CreateServerWithServices(Action configureServices, Func testpath = null, Uri baseAddress = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + PathString remainder; + if (req.Path == new PathString("/normal")) + { + res.StatusCode = 200; + } + else if (req.Path == new PathString("/forbid")) // Simulate forbidden + { + await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/unauthorized")) + { + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties()); + } + else if (req.Path == new PathString("/protected/CustomRedirect")) + { + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); + } + else if (req.Path == new PathString("/me")) + { + Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); + } + else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) + { + var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1)); + Describe(res, ticket); + } + else if (req.Path == new PathString("/testpath") && testpath != null) + { + await testpath(context); + } + else if (req.Path == new PathString("/checkforerrors")) + { + var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // this used to be "Automatic" + if (result.Failure != null) + { + throw new Exception("Failed to authenticate", result.Failure); + } + return; + } + else + { + await next(); + } + }); + }) + .ConfigureServices(configureServices); + var server = new TestServer(builder); + server.BaseAddress = baseAddress; + return server; + } + + private static void Describe(HttpResponse res, AuthenticateResult result) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (result?.Ticket?.Principal != null) + { + xml.Add(result.Ticket.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + if (result?.Ticket?.Properties != null) + { + xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + res.Body.Write(xmlBytes, 0, xmlBytes.Length); + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault(); + } + if (!string.IsNullOrEmpty(transaction.SetCookie)) + { + transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public string SetCookie { get; set; } + public string CookieNameValue { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs new file mode 100644 index 0000000000..d658609b04 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class DynamicSchemeTests + { + [Fact] + public async Task OptionsAreConfiguredOnce() + { + var server = CreateServer(s => + { + s.Configure("One", o => o.Instance = new Singleton()); + s.Configure("Two", o => o.Instance = new Singleton()); + }); + // Add One scheme + var response = await server.CreateClient().GetAsync("http://example.com/add/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + + // Verify option is not recreated + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + + // Add Two scheme + response = await server.CreateClient().GetAsync("http://example.com/add/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + Assert.Equal("2", transaction.FindClaimValue("Count")); + + // Verify options are not recreated + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + Assert.Equal("2", transaction.FindClaimValue("Count")); + } + + [Fact] + public async Task CanAddAndRemoveSchemes() + { + var server = CreateServer(); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/One")); + + // Add One scheme + var response = await server.CreateClient().GetAsync("http://example.com/add/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + + // Add Two scheme + response = await server.CreateClient().GetAsync("http://example.com/add/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + + // Remove Two + response = await server.CreateClient().GetAsync("http://example.com/remove/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/Two")); + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + + // Remove One + response = await server.CreateClient().GetAsync("http://example.com/remove/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/Two")); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/One")); + } + + public class TestOptions : AuthenticationSchemeOptions + { + public Singleton Instance { get; set; } + } + + public class Singleton + { + public static int _count; + + public Singleton() + { + _count++; + Count = _count; + } + + public int Count { get; } + } + + private class TestHandler : AuthenticationHandler + { + public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + if (Options.Instance != null) + { + id.AddClaim(new Claim("Count", Options.Instance.Count.ToString())); + } + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + } + + private static TestServer CreateServer(Action configureServices = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path.StartsWithSegments(new PathString("/add"), out var remainder)) + { + var name = remainder.Value.Substring(1); + var auth = context.RequestServices.GetRequiredService(); + var scheme = new AuthenticationScheme(name, name, typeof(TestHandler)); + auth.AddScheme(scheme); + } + else if (req.Path.StartsWithSegments(new PathString("/auth"), out remainder)) + { + var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; + var result = await context.AuthenticateAsync(name); + res.Describe(result?.Ticket?.Principal); + } + else if (req.Path.StartsWithSegments(new PathString("/remove"), out remainder)) + { + var name = remainder.Value.Substring(1); + var auth = context.RequestServices.GetRequiredService(); + auth.RemoveScheme(name); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + configureServices?.Invoke(services); + services.AddAuthentication(); + }); + return new TestServer(builder); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs new file mode 100644 index 0000000000..b909be9fdc --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs @@ -0,0 +1,836 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + public class FacebookTests + { + private void ConfigureDefaults(FacebookOptions o) + { + o.AppId = "whatever"; + o.AppSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = FacebookDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddFacebook(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication().AddFacebook(o => + { + o.AppId = "whatever"; + o.AppSecret = "whatever"; + o.SignInScheme = FacebookDefaults.AuthenticationScheme; + }), + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultScheme() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication(o => o.DefaultScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => + { + o.AppId = "whatever"; + o.AppSecret = "whatever"; + }), + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultSignInScheme() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication(o => o.DefaultSignInScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => + { + o.AppId = "whatever"; + o.AppSecret = "whatever"; + }), + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddFacebook(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(FacebookDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("FacebookHandler", scheme.HandlerType.Name); + Assert.Equal(FacebookDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task ThrowsIfAppIdMissing() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication().AddFacebook(o => o.SignInScheme = "Whatever"), + async context => + { + await Assert.ThrowsAsync("AppId", () => context.ChallengeAsync("Facebook")); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ThrowsIfAppSecretMissing() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication().AddFacebook(o => o.AppId = "Whatever"), + async context => + { + await Assert.ThrowsAsync("AppSecret", () => context.ChallengeAsync("Facebook")); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer( + app => + { + app.UseAuthentication(); + }, + services => + { + services.AddAuthentication("External") + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + }, + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsConfigured() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo,bar", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwritten() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz,qux", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz,qux", res.Headers.Location.Query); + } + + [Fact] + public async Task NestedMapWillNotAffectRedirect() + { + var server = CreateServer(app => app.Map("/base", map => + { + map.UseAuthentication(); + map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); + }), + services => + { + services.AddAuthentication() + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + }); + }, + handler: null); + + var transaction = await server.SendAsync("http://example.com/base/login"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/base/signin-facebook"), location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task MapWillNotAffectRedirect() + { + var server = CreateServer( + app => + { + app.UseAuthentication(); + app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); + }, + services => + { + services.AddAuthentication() + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.SignInScheme = "External"; + }); + }, + handler: null); + var transaction = await server.SendAsync("http://example.com/login"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication(options => + { + options.DefaultSignInScheme = "External"; + }) + .AddCookie() + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + }); + }, + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=", location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task CustomUserInfoEndpointHasValidGraphQuery() + { + var customUserInfoEndpoint = "https://graph.facebook.com/me?fields=email,timezone,picture"; + var finalUserInfoEndpoint = string.Empty; + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("FacebookTest")); + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.StateDataFormat = stateFormat; + o.UserInformationEndpoint = customUserInfoEndpoint; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == FacebookDefaults.TokenEndpoint) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var graphResponse = JsonConvert.SerializeObject(new + { + access_token = "TestAuthToken" + }); + res.Content = new StringContent(graphResponse, Encoding.UTF8); + return res; + } + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == + new Uri(customUserInfoEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped)) + { + finalUserInfoEndpoint = req.RequestUri.ToString(); + var res = new HttpResponseMessage(HttpStatusCode.OK); + var graphResponse = JsonConvert.SerializeObject(new + { + id = "TestProfileId", + name = "TestName" + }); + res.Content = new StringContent(graphResponse, Encoding.UTF8); + return res; + } + return null; + } + }; + }); + }, + handler: null); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-facebook?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Facebook.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?')); + Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint); + Assert.Contains("&access_token=", finalUserInfoEndpoint); + } + + private static TestServer CreateServer(Action configure, Action configureServices, Func> handler) + { + var builder = new WebHostBuilder() + .Configure(app => + { + configure?.Invoke(app); + app.Use(async (context, next) => + { + if (handler == null || !await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(configureServices); + return new TestServer(builder); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs new file mode 100644 index 0000000000..511a658ff4 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs @@ -0,0 +1,1622 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + public class GoogleTests + { + private void ConfigureDefaults(GoogleOptions o) + { + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = GoogleDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddGoogle(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SignInScheme = GoogleDefaults.AuthenticationScheme; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddGoogle(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(GoogleDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("GoogleHandler", scheme.HandlerType.Name); + Assert.Equal(GoogleDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.ToString(); + Assert.Contains("https://accounts.google.com/o/oauth2/v2/auth?response_type=code", location); + Assert.Contains("&client_id=", location); + Assert.Contains("&redirect_uri=", location); + Assert.Contains("&scope=", location); + Assert.Contains("&state=", location); + + Assert.DoesNotContain("access_type=", location); + Assert.DoesNotContain("prompt=", location); + Assert.DoesNotContain("approval_prompt=", location); + Assert.DoesNotContain("login_hint=", location); + Assert.DoesNotContain("include_granted_scopes=", location); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task Challenge401WillNotTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/401"); + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillSetCorrelationCookie() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google.")); + } + + [Fact] + public async Task ChallengeWillSetDefaultScope() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("&scope=" + UrlEncoder.Default.Encode("openid profile email"), query); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new GoogleChallengeProperties + { + Scope = new string[] { "openid", "https://www.googleapis.com/auth/plus.login" }, + AccessType = "offline", + ApprovalPrompt = "force", + Prompt = "consent", + LoginHint = "test@example.com", + IncludeGrantedScopes = false, + }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("openid https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsParameters() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new AuthenticationProperties(new Dictionary() + { + { "scope", "https://www.googleapis.com/auth/plus.login" }, + { "access_type", "offline" }, + { "approval_prompt", "force" }, + { "prompt", "consent" }, + { "login_hint", "test@example.com" }, + { "include_granted_scopes", "false" } + })); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsQueryArgumentsButParametersWillOverwrite() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new GoogleChallengeProperties(new Dictionary + { + ["scope"] = "https://www.googleapis.com/auth/plus.login", + ["access_type"] = "offline", + ["include_granted_scopes"] = "false", + ["approval_prompt"] = "force", + ["prompt"] = "login", + ["login_hint"] = "this-will-be-overwritten@example.com", + }) + { + Prompt = "consent", + LoginHint = "test@example.com", + }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task AuthenticateWithoutCookieWillFail() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }, + async context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/auth")) + { + var result = await context.AuthenticateAsync("Google"); + Assert.NotNull(result.Failure); + } + }); + var transaction = await server.SendAsync("https://example.com/auth"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ReplyPathWithoutStateQueryStringWillBeRejected() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); + Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWithErrorFails(bool redirect) + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = new TestStateDataFormat(); + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state", + ".AspNetCore.Correlation.Google.corrilationId=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(null)] + [InlineData("CustomIssuer")] + public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SaveTokens = true; + o.StateDataFormat = stateFormat; + if (claimsIssuer != null) + { + o.ClaimsIssuer = claimsIssuer; + } + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + }); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var expectedIssuer = claimsIssuer ?? GoogleDefaults.AuthenticationScheme; + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName, expectedIssuer)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname, expectedIssuer)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + + transaction = await server.SendAsync("https://example.com/tokens", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token")); + Assert.Equal("Bearer", transaction.FindTokenValue("token_type")); + Assert.NotNull(transaction.FindTokenValue("expires_at")); + } + + // REVIEW: Fix this once we revisit error handling to not blow up + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new { Error = "Error" }, + HttpStatusCode.BadRequest); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new object()); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("Failed to retrieve access token.", error.GetBaseException().Message); + } + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + o.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); + return Task.FromResult(0); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); + } + + [Fact] + public async Task NullRedirectUriWillRedirectToSlash() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + o.Events = new OAuthEvents + { + OnTicketReceived = context => + { + context.Properties.RedirectUri = null; + return Task.FromResult(0); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + } + + [Fact] + public async Task ValidateAuthenticatedContext() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.AccessType = "offline"; + o.Events = new OAuthEvents() + { + OnCreatingTicket = context => + { + Assert.NotNull(context.User); + Assert.Equal("Test Access Token", context.AccessToken); + Assert.Equal("Test Refresh Token", context.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(3600), context.ExpiresIn); + Assert.Equal("Test email", context.Identity.FindFirst(ClaimTypes.Email)?.Value); + Assert.Equal("Test User ID", context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value); + Assert.Equal("Test Name", context.Identity.FindFirst(ClaimTypes.Name)?.Value); + Assert.Equal("Test Family Name", context.Identity.FindFirst(ClaimTypes.Surname)?.Value); + Assert.Equal("Test Given Name", context.Identity.FindFirst(ClaimTypes.GivenName)?.Value); + return Task.FromResult(0); + } + }; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + }); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/foo"; + var state = stateFormat.Protect(properties); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task NoStateCausesException() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + + //Post a message to the Google middleware + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); + Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); + } + + [Fact] + public async Task CanRedirectOnError() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Events = new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + }); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."), + transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task AuthenticateAutomaticWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticate", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateGoogleWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticateGoogle", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateFacebookWhenAlreadySignedWithGoogleReturnsNull() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticateFacebook", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Null(transaction.FindClaimValue(ClaimTypes.Name)); + } + + [Fact] + public async Task ChallengeFacebookWhenAlreadySignedWithGoogleSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/challengeFacebook", authCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.StartsWith("https://www.facebook.com/", transaction.Response.Headers.Location.OriginalString); + } + + private HttpMessageHandler CreateBackchannel() + { + return new TestHttpMessageHandler() + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + } + + private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK) + { + var res = new HttpResponseMessage(code); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private class ClaimsTransformer : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal p) + { + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return Task.FromResult(p); + } + } + + private static TestServer CreateServer(Action configureOptions, Func testpath = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync(); + } + else if (req.Path == new PathString("/challengeFacebook")) + { + await context.ChallengeAsync("Facebook"); + } + else if (req.Path == new PathString("/tokens")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + var tokens = result.Properties.GetTokens(); + res.Describe(tokens); + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else if (req.Path == new PathString("/authenticate")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + res.Describe(result.Principal); + } + else if (req.Path == new PathString("/authenticateGoogle")) + { + var result = await context.AuthenticateAsync("Google"); + res.Describe(result?.Principal); + } + else if (req.Path == new PathString("/authenticateFacebook")) + { + var result = await context.AuthenticateAsync("Facebook"); + res.Describe(result?.Principal); + } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync("Google"); + await context.ChallengeAsync("Google"); + } + else if (req.Path == new PathString("/unauthorizedAuto")) + { + var result = await context.AuthenticateAsync("Google"); + await context.ChallengeAsync("Google"); + } + else if (req.Path == new PathString("/401")) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Google", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Google")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Google")); + } + else if (testpath != null) + { + await testpath(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddTransient(); + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = GoogleDefaults.AuthenticationScheme) + .AddGoogle(configureOptions) + .AddFacebook(o => + { + o.ClientId = "Test ClientId"; + o.ClientSecret = "Test AppSecrent"; + }); + }); + return new TestServer(builder); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "corrilationId" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs new file mode 100644 index 0000000000..d7fcdb4cad --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs @@ -0,0 +1,1237 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class JwtBearerTests + { + private void ConfigureDefaults(JwtBearerOptions o) + { + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddJwtBearer(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddJwtBearer(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(JwtBearerDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("JwtBearerHandler", scheme.HandlerType.Name); + Assert.Null(scheme.DisplayName); + } + + [Fact] + public async Task BearerTokenValidation() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + var server = CreateServer(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + } + + [Fact] + public async Task SaveBearerToken() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + var server = CreateServer(o => + { + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + var response = await SendAsync(server, "http://example.com/token", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(tokenText, await response.Response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ThrowAtAuthenticationFailedEvent() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + context.Response.StatusCode = 401; + throw new Exception(); + }, + OnMessageReceived = context => + { + context.Token = "something"; + return Task.FromResult(0); + } + }; + o.SecurityTokenValidators.Clear(); + o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator()); + }, + async (context, next) => + { + try + { + await next(); + Assert.False(true, "Expected exception is not thrown"); + } + catch (Exception) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("i got this"); + } + }); + + var transaction = await server.SendAsync("https://example.com/signIn"); + + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task CustomHeaderReceived() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + + return Task.FromResult(null); + } + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task NoHeaderReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task HeaderWithoutBearerReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Token"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task UnrecognizedTokenReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task InvalidTokenReceived() + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator()); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer is invalid")] + [InlineData(typeof(SecurityTokenNoExpirationException), "The token has no expiration")] + [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid yet")] + [InlineData(typeof(SecurityTokenExpiredException), "The token is expired")] + [InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")] + [InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")] + public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message) + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(ArgumentException))] + public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType) + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures() + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException))); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException))); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\", error_description=\"The audience is invalid; The signature key was not found\"", + response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", null, null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, null, "custom_uri")] + public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error, string description, string uri) + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.Error = error; + context.ErrorDescription = description; + context.ErrorUri = uri; + + return Task.FromResult(0); + } + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + + var builder = new StringBuilder(JwtBearerDefaults.AuthenticationScheme); + + if (!string.IsNullOrEmpty(error)) + { + builder.Append(" error=\""); + builder.Append(error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(description)) + { + if (!string.IsNullOrEmpty(error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(description); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(uri)) + { + if (!string.IsNullOrEmpty(error) || + !string.IsNullOrEmpty(description)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(uri); + builder.Append('\"'); + } + + Assert.Equal(builder.ToString(), response.Response.Headers.WwwAuthenticate.First().ToString()); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() + { + var server = CreateServer(o => + { + o.IncludeErrorDetails = false; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenTokenWasMissing() + { + var server = CreateServer(); + + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task CustomTokenValidated() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + // Retrieve the NameIdentifier claim from the identity + // returned by the custom security token validator. + var identity = (ClaimsIdentity)context.Principal.Identity; + var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); + + Assert.Equal("Bob le Tout Puissant", identifier.Value); + + // Remove the existing NameIdentifier claim and replace it + // with a new one containing a different value. + identity.RemoveClaim(identifier); + // Make sure to use a different name identifier + // than the one defined by BlobTokenValidator. + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique")); + + return Task.FromResult(null); + } + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task RetrievingTokenFromAlternateLocation() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Token = "CustomToken"; + return Task.FromResult(null); + } + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token => + { + Assert.Equal("CustomToken", token); + })); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Tout Puissant", response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnChallengeSkip_ResponseNotModified() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents() + { + OnChallenge = context => + { + context.HandleResponse(); + return Task.FromResult(0); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Empty(response.Response.Headers.WwwAuthenticate); + Assert.Equal(string.Empty, response.ResponseText); + } + + class InvalidTokenValidator : ISecurityTokenValidator + { + public InvalidTokenValidator() + { + ExceptionType = typeof(SecurityTokenException); + } + + public InvalidTokenValidator(Type exceptionType) + { + ExceptionType = exceptionType; + } + + public Type ExceptionType { get; set; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + var constructor = ExceptionType.GetTypeInfo().GetConstructor(new[] { typeof(string) }); + var exception = (Exception)constructor.Invoke(new[] { ExceptionType.Name }); + throw exception; + } + } + + class BlobTokenValidator : ISecurityTokenValidator + { + private Action _tokenValidator; + + public BlobTokenValidator(string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + + } + public BlobTokenValidator(string authenticationScheme, Action tokenValidator) + { + AuthenticationScheme = authenticationScheme; + _tokenValidator = tokenValidator; + } + + public string AuthenticationScheme { get; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + validatedToken = null; + _tokenValidator?.Invoke(securityToken); + + var claims = new[] + { + // Make sure to use a different name identifier + // than the one defined by CustomTokenValidated. + new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationScheme)); + } + } + + private static TestServer CreateServer(Action options = null, Func, Task> handlerBeforeAuth = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + if (handlerBeforeAuth != null) + { + app.Use(handlerBeforeAuth); + } + + app.UseAuthentication(); + app.Use(async (context, next) => + { + if (context.Request.Path == new PathString("/checkforerrors")) + { + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); // this used to be "Automatic" + if (result.Failure != null) + { + throw new Exception("Failed to authenticate", result.Failure); + } + return; + } + else if (context.Request.Path == new PathString("/oauth")) + { + if (context.User == null || + context.User.Identity == null || + !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = 401; + // REVIEW: no more automatic challenge + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + return; + } + + var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier); + if (identifier == null) + { + context.Response.StatusCode = 500; + return; + } + + await context.Response.WriteAsync(identifier.Value); + } + else if (context.Request.Path == new PathString("/token")) + { + var token = await context.GetTokenAsync("access_token"); + await context.Response.WriteAsync(token); + } + else if (context.Request.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + } + else if (context.Request.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); + } + else if (context.Request.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync(JwtBearerDefaults.AuthenticationScheme)); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options)); + + return new TestServer(builder); + } + + // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header) + private static async Task SendAsync(TestServer server, string uri, string authorizationHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", authorizationHeader); + } + + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj new file mode 100644 index 0000000000..6c8d518ffa --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -0,0 +1,47 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs new file mode 100644 index 0000000000..e2e13f270e --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs @@ -0,0 +1,713 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount +{ + public class MicrosoftAccountTests + { + private void ConfigureDefaults(MicrosoftAccountOptions o) + { + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SignInScheme = MicrosoftAccountDefaults.AuthenticationScheme; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddMicrosoftAccount(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(MicrosoftAccountDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("MicrosoftAccountHandler", scheme.HandlerType.Name); + Assert.Equal(MicrosoftAccountDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ClientId = "Test Client Id"; + o.ClientSecret = "Test Client Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=", location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsConfigured() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo%20bar", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwritten() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challengeWithOtherScope"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challengeWithOtherScopeWithBaseAuthenticationProperties"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("MsftTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Client Id"; + o.ClientSecret = "Test Client Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://graph.microsoft.com/v1.0/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + givenName = "Test Given Name", + surname = "Test Family Name", + mail = "Test email" + }); + } + + return null; + } + }; + o.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Microsoft") }, "Microsoft")); + return Task.FromResult(null); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Microsoft.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Microsoft.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); + } + + private static TestServer CreateServer(Action configureOptions) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync("Microsoft"); + } + else if (req.Path == new PathString("/challengeWithOtherScope")) + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await context.ChallengeAsync("Microsoft", properties); + } + else if (req.Path == new PathString("/challengeWithOtherScopeWithBaseAuthenticationProperties")) + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await context.ChallengeAsync("Microsoft", properties); + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Microsoft", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Microsoft")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Microsoft")); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => { }) + .AddMicrosoftAccount(configureOptions); + }); + return new TestServer(builder); + } + + private static HttpResponseMessage ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs new file mode 100644 index 0000000000..c359bb0e8c --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs @@ -0,0 +1,149 @@ +using System; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test +{ + public class OAuthChallengePropertiesTest + { + [Fact] + public void ScopeProperty() + { + var properties = new OAuthChallengeProperties + { + Scope = new string[] { "foo", "bar" } + }; + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]); + } + + [Fact] + public void ScopeProperty_NullValue() + { + var properties = new OAuthChallengeProperties(); + properties.Parameters["scope"] = new string[] { "foo", "bar" }; + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + + properties.Scope = null; + Assert.Null(properties.Scope); + } + + [Fact] + public void SetScope() + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("foo", "bar"); + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]); + } + + [Fact] + public void OidcMaxAge() + { + var properties = new OpenIdConnectChallengeProperties() + { + MaxAge = TimeSpan.FromSeconds(200) + }; + Assert.Equal(TimeSpan.FromSeconds(200), properties.MaxAge); + } + + [Fact] + public void OidcMaxAge_NullValue() + { + var properties = new OpenIdConnectChallengeProperties(); + properties.Parameters["max_age"] = TimeSpan.FromSeconds(500); + Assert.Equal(TimeSpan.FromSeconds(500), properties.MaxAge); + + properties.MaxAge = null; + Assert.Null(properties.MaxAge); + } + + [Fact] + public void OidcPrompt() + { + var properties = new OpenIdConnectChallengeProperties() + { + Prompt = "login" + }; + Assert.Equal("login", properties.Prompt); + Assert.Equal("login", properties.Parameters["prompt"]); + } + + [Fact] + public void OidcPrompt_NullValue() + { + var properties = new OpenIdConnectChallengeProperties(); + properties.Parameters["prompt"] = "consent"; + Assert.Equal("consent", properties.Prompt); + + properties.Prompt = null; + Assert.Null(properties.Prompt); + } + + [Fact] + public void GoogleProperties() + { + var properties = new GoogleChallengeProperties() + { + AccessType = "offline", + ApprovalPrompt = "force", + LoginHint = "test@example.com", + Prompt = "login", + }; + Assert.Equal("offline", properties.AccessType); + Assert.Equal("offline", properties.Parameters["access_type"]); + Assert.Equal("force", properties.ApprovalPrompt); + Assert.Equal("force", properties.Parameters["approval_prompt"]); + Assert.Equal("test@example.com", properties.LoginHint); + Assert.Equal("test@example.com", properties.Parameters["login_hint"]); + Assert.Equal("login", properties.Prompt); + Assert.Equal("login", properties.Parameters["prompt"]); + } + + [Fact] + public void GoogleProperties_NullValues() + { + var properties = new GoogleChallengeProperties(); + properties.Parameters["access_type"] = "offline"; + properties.Parameters["approval_prompt"] = "force"; + properties.Parameters["login_hint"] = "test@example.com"; + properties.Parameters["prompt"] = "login"; + Assert.Equal("offline", properties.AccessType); + Assert.Equal("force", properties.ApprovalPrompt); + Assert.Equal("test@example.com", properties.LoginHint); + Assert.Equal("login", properties.Prompt); + + properties.AccessType = null; + Assert.Null(properties.AccessType); + + properties.ApprovalPrompt = null; + Assert.Null(properties.ApprovalPrompt); + + properties.LoginHint = null; + Assert.Null(properties.LoginHint); + + properties.Prompt = null; + Assert.Null(properties.Prompt); + } + + [Fact] + public void GoogleIncludeGrantedScopes() + { + var properties = new GoogleChallengeProperties() + { + IncludeGrantedScopes = true + }; + Assert.True(properties.IncludeGrantedScopes); + Assert.Equal(true, properties.Parameters["include_granted_scopes"]); + + properties.IncludeGrantedScopes = false; + Assert.False(properties.IncludeGrantedScopes); + Assert.Equal(false, properties.Parameters["include_granted_scopes"]); + + properties.IncludeGrantedScopes = null; + Assert.Null(properties.IncludeGrantedScopes); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs new file mode 100644 index 0000000000..4b822b611f --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs @@ -0,0 +1,752 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthTests + { + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.DefaultSignInScheme = "auth1"; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = "default"; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOAuth("default", o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.SignInScheme = "weeblie"; + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.CallbackPath = "/whatever"; + o.AuthorizationEndpoint = "/whatever"; + o.TokenEndpoint = "/whatever"; + })); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddOAuth("oauth", o => { }); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync("oauth"); + Assert.NotNull(scheme); + Assert.Equal("OAuthHandler`1", scheme.HandlerType.Name); + Assert.Equal(OAuthDefaults.DisplayName, scheme.DisplayName); + } + + [Fact] + public async Task ThrowsIfClientIdMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.SignInScheme = "whatever"; + o.CallbackPath = "/"; + o.ClientSecret = "whatever"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + })); + await Assert.ThrowsAsync("ClientId", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfClientSecretMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.SignInScheme = "whatever"; + o.ClientId = "Whatever;"; + o.CallbackPath = "/"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + })); + await Assert.ThrowsAsync("ClientSecret", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfCallbackPathMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("CallbackPath", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfTokenEndpointMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.CallbackPath = "/"; + o.AuthorizationEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("TokenEndpoint", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfAuthorizationEndpointMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.CallbackPath = "/"; + o.TokenEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("AuthorizationEndpoint", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/oauth-callback", correlation); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_CorrelationIdCookieOptions_CanBeOverriden() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.CorrelationCookie.Path = "/"; + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/", correlation); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsConfigured() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo%20bar", res.Headers.Location.Query); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwritten() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await ctx.ChallengeAsync("Weblie", properties); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await ctx.ChallengeAsync("Weblie", properties); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + private void ConfigureDefaults(OAuthOptions o) + { + o.ClientId = "Test Id"; + o.ClientSecret = "secret"; + o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AuthorizationEndpoint = "https://example.com/provider/login"; + o.TokenEndpoint = "https://example.com/provider/token"; + o.CallbackPath = "/oauth-callback"; + } + + [Fact] + public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + opt.ClientId = "Test Id"; + opt.ClientSecret = "secret"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.AuthorizationEndpoint = "https://example.com/provider/login"; + opt.TokenEndpoint = "https://example.com/provider/token"; + opt.CallbackPath = "/oauth-callback"; + opt.StateDataFormat = new TestStateDataFormat(); + opt.Events = new OAuthEvents() + { + OnRemoteFailure = context => + { + Assert.Contains("declined", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + })); + + var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=declined&state=protected_state", + ".AspNetCore.Correlation.Weblie.corrilationId=N"); + + Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode); + Assert.Null(transaction.Response.Headers.Location); + } + + private static TestServer CreateServer(Action configureServices, Func> handler = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + if (handler == null || ! await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(configureServices); + return new TestServer(builder); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "corrilationId" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs new file mode 100644 index 0000000000..5614fe8fea --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs @@ -0,0 +1,21 @@ +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class MockOpenIdConnectMessage : OpenIdConnectMessage + { + public string TestAuthorizeEndpoint { get; set; } + + public string TestLogoutRequest { get; set; } + + public override string CreateAuthenticationRequestUrl() + { + return TestAuthorizeEndpoint ?? base.CreateAuthenticationRequestUrl(); + } + + public override string CreateLogoutRequestUrl() + { + return TestLogoutRequest ?? base.CreateLogoutRequestUrl(); + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs new file mode 100644 index 0000000000..cbafc46223 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -0,0 +1,616 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectChallengeTests + { + private static readonly string ChallengeEndpoint = TestServerBuilder.TestHost + TestServerBuilder.Challenge; + + [Fact] + public async Task ChallengeRedirectIsIssuedCorrectly() + { + var settings = new TestSettings( + opt => + { + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; + opt.ClientId = "Test Id"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri, + OpenIdConnectParameterNames.SkuTelemetry, + OpenIdConnectParameterNames.VersionTelemetry); + } + + [Fact] + public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.DisableTelemetry = true; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query); + Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query); + } + + /* + Example of a form post + +
+ + + + + + + + +
+ + + */ + [Fact] + public async Task ChallengeFormPostIssuedCorrectly() + { + var settings = new TestSettings( + opt => + { + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost; + opt.ClientId = "Test Id"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType); + + var body = await res.Content.ReadAsStringAsync(); + settings.ValidateChallengeFormPost( + body, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task ChallengeCanSetUserStateThroughProperties(string userState) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var settings = new TestSettings(o => + { + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + o.StateDataFormat = stateFormat; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = stateFormat.Unprotect(actualState); + + Assert.Equal(userState ?? string.Empty, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task OnRedirectToIdentityProviderEventCanSetState(string userState) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var settings = new TestSettings(opt => + { + opt.StateDataFormat = stateFormat; + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.State = userState; + return Task.FromResult(0); + } + }; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = stateFormat.Unprotect(actualState); + + if (userState != null) + { + Assert.Equal(userState, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + else + { + Assert.False(actualProperties.Items.ContainsKey(OpenIdConnectDefaults.UserstatePropertiesKey)); + } + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventIsHit() + { + var eventIsHit = false; + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + eventIsHit = true; + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + Assert.True(eventIsHit); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceValues() + { + var newClientId = Guid.NewGuid().ToString(); + + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.ClientId = newClientId; + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + + var actual = res.Headers.Location.Query.Trim('?').Split('&').Single(seg => seg.StartsWith($"{OpenIdConnectParameterNames.ClientId}=")); + Assert.Equal($"{OpenIdConnectParameterNames.ClientId}={newClientId}", actual); + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceMessage() + { + var newMessage = new MockOpenIdConnectMessage + { + IssuerAddress = "http://example.com/", + TestAuthorizeEndpoint = $"http://example.com/{Guid.NewGuid()}/oauth2/signin" + }; + + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage = newMessage; + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + // The CreateAuthenticationRequestUrl method is overridden MockOpenIdConnectMessage where + // query string is not generated and the authorization endpoint is replaced. + Assert.Equal(newMessage.TestAuthorizeEndpoint, res.Headers.Location.AbsoluteUri); + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventHandlesResponse() + { + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.Response.StatusCode = 410; + context.Response.Headers.Add("tea", "Oolong"); + context.HandleResponse(); + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Gone, res.StatusCode); + Assert.Equal("Oolong", res.Headers.GetValues("tea").Single()); + Assert.Null(res.Headers.Location); + } + + // This test can be further refined. When one auth handler skips, the authentication responsibility + // will be flowed to the next one. A dummy auth handler can be added to ensure the correct logic. + [Fact] + public async Task OnRedirectToIdentityProviderEventHandleResponse() + { + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.HandleResponse(); + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Null(res.Headers.Location); + } + + [Theory] + [InlineData(OpenIdConnectRedirectBehavior.RedirectGet)] + [InlineData(OpenIdConnectRedirectBehavior.FormPost)] + public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavior method) + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = method; + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + }); + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie); + var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single(); + Assert.True(nonceCookie.Expires.HasValue); + Assert.True(nonceCookie.Expires > DateTime.UtcNow); + Assert.True(nonceCookie.HttpOnly); + Assert.Equal("/signin-oidc", nonceCookie.Path); + Assert.Equal("N", nonceCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.None, nonceCookie.SameSite); + + var correlationCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(".AspNetCore.Correlation.", StringComparison.Ordinal)).Single(); + Assert.True(correlationCookie.Expires.HasValue); + Assert.True(nonceCookie.Expires > DateTime.UtcNow); + Assert.True(correlationCookie.HttpOnly); + Assert.Equal("/signin-oidc", correlationCookie.Path); + Assert.False(StringSegment.IsNullOrEmpty(correlationCookie.Value)); + + Assert.Equal(2, challengeCookies.Count); + } + + [Fact] + public async Task Challenge_WithEmptyConfig_Fails() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = new OpenIdConnectConfiguration(); + }); + + var server = settings.CreateTestServer(); + var exception = await Assert.ThrowsAsync(() => server.SendAsync(ChallengeEndpoint)); + Assert.Equal("Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.", exception.Message); + } + + [Fact] + public async Task Challenge_WithDefaultMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } + + [Fact] + public async Task Challenge_WithSpecificMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromMinutes(20); + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } + + [Fact] + public async Task Challenge_HasExpectedPromptParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location, OpenIdConnectParameterNames.Prompt); + Assert.Contains("prompt=consent", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenPromptParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + var properties = new OpenIdConnectChallengeProperties() + { + Prompt = "login", + }; + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("prompt=login", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenPromptParamFromBaseAuthenticationProperties() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.PromptKey, "login"); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("prompt=login", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenScopeParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }); + var properties = new OpenIdConnectChallengeProperties(); + properties.SetScope("baz", "qux"); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenScopeParamFromBaseAuthenticationProperties() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenMaxAgeParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromSeconds(500); + }); + var properties = new OpenIdConnectChallengeProperties() + { + MaxAge = TimeSpan.FromSeconds(1234), + }; + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("max_age=1234", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationPropertiesm() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromSeconds(500); + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.MaxAgeKey, TimeSpan.FromSeconds(1234)); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("max_age=1234", res.Headers.Location.Query); + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs new file mode 100644 index 0000000000..ed368c1ef7 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs @@ -0,0 +1,574 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectConfigurationTests + { + private void ConfigureDefaults(OpenIdConnectOptions o) + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, selector.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task MetadataAddressIsGeneratedFromAuthorityWhenMissing() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication() + .AddCookie() + .AddOpenIdConnect(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = Guid.NewGuid().ToString(); + o.SignInScheme = Guid.NewGuid().ToString(); + }); + }) + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + var resolver = context.RequestServices.GetRequiredService(); + var handler = await resolver.GetHandlerAsync(context, OpenIdConnectDefaults.AuthenticationScheme) as OpenIdConnectHandler; + Assert.Equal($"{TestServerBuilder.DefaultAuthority}/.well-known/openid-configuration", handler.Options.MetadataAddress); + }); + }); + var server = new TestServer(builder); + var transaction = await server.SendAsync(@"https://example.com"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public Task ThrowsWhenSignInSchemeIsSetToSelf() + { + return TestConfigurationException( + o => + { + o.SignInScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }, + ex => Assert.Contains("cannot be set to itself", ex.Message)); + } + + [Fact] + public Task ThrowsWhenClientIdIsMissing() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.Authority = TestServerBuilder.DefaultAuthority; + }, + ex => Assert.Equal("ClientId", ex.ParamName)); + } + + [Fact] + public Task ThrowsWhenAuthorityIsMissing() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("Provide Authority, MetadataAddress, Configuration, or ConfigurationManager to OpenIdConnectOptions", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenAuthorityIsNotHttps() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.MetadataAddress = "http://example.com"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenMetadataAddressIsNotHttps() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.MetadataAddress = "http://example.com"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenMaxAgeIsNegative() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + o.MaxAge = TimeSpan.FromSeconds(-1); + }, + ex => Assert.StartsWith("The value must not be a negative TimeSpan.", ex.Message) + ); + } + + private TestServer BuildTestServer(Action options) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication() + .AddCookie() + .AddOpenIdConnect(options); + }) + .Configure(app => app.UseAuthentication()); + + return new TestServer(builder); + } + + private async Task TestConfigurationException( + Action options, + Action verifyException) + where T : Exception + { + var exception = await Assert.ThrowsAsync(() => BuildTestServer(options).SendAsync(@"https://example.com")); + verifyException(exception); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs new file mode 100644 index 0000000000..7530b00c31 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs @@ -0,0 +1,1347 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectEventTests + { + private readonly RequestDelegate AppWritePath = context => context.Response.WriteAsync(context.Request.Path); + private readonly RequestDelegate AppNotImpl = context => { throw new NotImplementedException("App"); }; + + [Fact] + public async Task OnMessageReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectRemoteFailure = true, + }; + events.OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", ""); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectRemoteFailure = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTicketReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenResponseReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + return Task.FromResult(0); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/end", response.Headers.Location.GetLeftPart(UriPartial.Path)); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Handled_RedirectNotInvoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + events.OnRedirectToIdentityProviderForSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + events.ValidateExpectations(); + Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + Assert.True(SetCookieHeaderValue.TryParseStrictList(values.ToList(), out var parsedValues)); + Assert.Equal(1, parsedValues.Count); + Assert.True(StringSegment.IsNullOrEmpty(parsedValues.Single().Value)); + } + + [Fact] + public async Task OnRemoteSignOut_Handled_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRemoteSignOut_Skip_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/redirect", response.Headers.Location.AbsoluteUri); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Handled_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Skipped_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + private class ExpectedOidcEvents : OpenIdConnectEvents + { + public bool ExpectMessageReceived { get; set; } + public bool InvokedMessageReceived { get; set; } + + public bool ExpectTokenValidated { get; set; } + public bool InvokedTokenValidated { get; set; } + + public bool ExpectRemoteFailure { get; set; } + public bool InvokedRemoteFailure { get; set; } + + public bool ExpectTicketReceived { get; set; } + public bool InvokedTicketReceived { get; set; } + + public bool ExpectAuthorizationCodeReceived { get; set; } + public bool InvokedAuthorizationCodeReceived { get; set; } + + public bool ExpectTokenResponseReceived { get; set; } + public bool InvokedTokenResponseReceived { get; set; } + + public bool ExpectUserInfoReceived { get; set; } + public bool InvokedUserInfoReceived { get; set; } + + public bool ExpectAuthenticationFailed { get; set; } + public bool InvokeAuthenticationFailed { get; set; } + + public bool ExpectRedirectForSignOut { get; set; } + public bool InvokedRedirectForSignOut { get; set; } + + public bool ExpectRemoteSignOut { get; set; } + public bool InvokedRemoteSignOut { get; set; } + + public bool ExpectRedirectToSignedOut { get; set; } + public bool InvokedRedirectToSignedOut { get; set; } + + public override Task MessageReceived(MessageReceivedContext context) + { + InvokedMessageReceived = true; + return base.MessageReceived(context); + } + + public override Task TokenValidated(TokenValidatedContext context) + { + InvokedTokenValidated = true; + return base.TokenValidated(context); + } + + public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) + { + InvokedAuthorizationCodeReceived = true; + return base.AuthorizationCodeReceived(context); + } + + public override Task TokenResponseReceived(TokenResponseReceivedContext context) + { + InvokedTokenResponseReceived = true; + return base.TokenResponseReceived(context); + } + + public override Task UserInformationReceived(UserInformationReceivedContext context) + { + InvokedUserInfoReceived = true; + return base.UserInformationReceived(context); + } + + public override Task AuthenticationFailed(AuthenticationFailedContext context) + { + InvokeAuthenticationFailed = true; + return base.AuthenticationFailed(context); + } + + public override Task TicketReceived(TicketReceivedContext context) + { + InvokedTicketReceived = true; + return base.TicketReceived(context); + } + + public override Task RemoteFailure(RemoteFailureContext context) + { + InvokedRemoteFailure = true; + return base.RemoteFailure(context); + } + + public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) + { + InvokedRedirectForSignOut = true; + return base.RedirectToIdentityProviderForSignOut(context); + } + + public override Task RemoteSignOut(RemoteSignOutContext context) + { + InvokedRemoteSignOut = true; + return base.RemoteSignOut(context); + } + + public override Task SignedOutCallbackRedirect(RemoteSignOutContext context) + { + InvokedRedirectToSignedOut = true; + return base.SignedOutCallbackRedirect(context); + } + + public void ValidateExpectations() + { + Assert.Equal(ExpectMessageReceived, InvokedMessageReceived); + Assert.Equal(ExpectTokenValidated, InvokedTokenValidated); + Assert.Equal(ExpectAuthorizationCodeReceived, InvokedAuthorizationCodeReceived); + Assert.Equal(ExpectTokenResponseReceived, InvokedTokenResponseReceived); + Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived); + Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed); + Assert.Equal(ExpectTicketReceived, InvokedTicketReceived); + Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure); + Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut); + Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut); + Assert.Equal(ExpectRedirectToSignedOut, InvokedRedirectToSignedOut); + } + } + + private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appCode) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication(auth => + { + auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(o => + { + o.Events = events; + o.ClientId = "ClientId"; + o.GetClaimsFromUserInfoEndpoint = true; + o.Configuration = new OpenIdConnectConfiguration() + { + TokenEndpoint = "http://testhost/tokens", + UserInfoEndpoint = "http://testhost/user", + EndSessionEndpoint = "http://testhost/end" + }; + o.StateDataFormat = new TestStateDataFormat(); + o.SecurityTokenValidator = new TestTokenValidator(); + o.ProtocolValidator = new TestProtocolValidator(); + o.BackchannelHttpHandler = new TestBackchannel(); + }); + }) + .Configure(app => + { + app.UseAuthentication(); + app.Run(appCode); + }); + + return new TestServer(builder); + } + + private Task PostAsync(TestServer server, string path, string form) + { + var client = server.CreateClient(); + var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".corrilationId=N"; + client.DefaultRequestHeaders.Add("Cookie", cookie); + return client.PostAsync("signin-oidc", + new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded")); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "corrilationId" }, + { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + + private class TestTokenValidator : ISecurityTokenValidator + { + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get { return 1024; } + set { throw new NotImplementedException(); } + } + + public bool CanReadToken(string securityToken) + { + Assert.Equal("my_id_token", securityToken); + return true; + } + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + Assert.Equal("my_id_token", securityToken); + validatedToken = new JwtSecurityToken(); + return new ClaimsPrincipal(new ClaimsIdentity("customAuthType")); + } + } + + private class TestProtocolValidator : OpenIdConnectProtocolValidator + { + public override void ValidateAuthenticationResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateTokenResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateUserInfoResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + } + + private class TestBackchannel : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (string.Equals("/tokens", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() { Content = + new StringContent("{ \"id_token\": \"my_id_token\", \"access_token\": \"my_access_token\" }", Encoding.ASCII, "application/json") }); + } + if (string.Equals("/user", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() { Content = new StringContent("{ }", Encoding.ASCII, "application/json") }); + } + + throw new NotImplementedException(request.RequestUri.ToString()); + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs new file mode 100644 index 0000000000..da52e0e4cb --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectTests + { + static string noncePrefix = "OpenIdConnect." + "Nonce."; + static string nonceDelimiter = "."; + const string DefaultHost = @"https://example.com"; + const string Logout = "/logout"; + const string Signin = "/signin"; + const string Signout = "/signout"; + + /// + /// Tests RedirectForSignOutContext replaces the OpenIdConnectMesssage correctly. + /// + /// Task + [Fact] + public async Task SignOutSettingMessage() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + EndSessionEndpoint = "https://example.com/signout_test/signout_request" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + setting.ValidateSignoutRedirect( + transaction.Response.Headers.Location, + OpenIdConnectParameterNames.SkuTelemetry, + OpenIdConnectParameterNames.VersionTelemetry); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsNonceCookiePath_ToCallBackPath() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)); + Assert.Contains("path=/signin-oidc", nonce); + } + + [Fact] + public async Task RedirectToIdentityProvider_NonceCookieOptions_CanBeOverriden() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + opt.NonceCookie.Path = "/"; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)); + Assert.Contains("path=/", nonce); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/signin-oidc", correlation); + } + + [Fact] + public async Task RedirectToIdentityProvider_CorrelationIdCookieOptions_CanBeOverriden() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + opt.CorrelationCookie.Path = "/"; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/", correlation); + } + + [Fact] + public async Task EndSessionRequestDoesNotIncludeTelemetryParametersWhenDisabled() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = configuration; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.DisableTelemetry = true; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query); + Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query); + setting.ValidateSignoutRedirect(transaction.Response.Headers.Location); + } + + [Fact] + public async Task SignOutFormPostWithDefaultRedirectUri() + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + }); + var server = settings.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + + settings.ValidateSignoutFormPost(transaction, + OpenIdConnectParameterNames.PostLogoutRedirectUri); + } + + [Fact] + public async Task SignOutRedirectWithDefaultRedirectUri() + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + }); + var server = settings.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + settings.ValidateSignoutRedirect(transaction.Response.Headers.Location, + OpenIdConnectParameterNames.PostLogoutRedirectUri); + } + + [Fact] + public async Task SignOutWithCustomRedirectUri() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var server = TestServerBuilder.CreateServer(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.Configuration = configuration; + o.StateDataFormat = stateFormat; + o.SignedOutCallbackPath = "/thelogout"; + o.SignedOutRedirectUri = "https://example.com/postlogout"; + }); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&') + .Select(each => each.Split('=')) + .ToDictionary(pair => pair[0], pair => pair[1]); + + string redirectUri; + Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri)); + Assert.Equal(UrlEncoder.Default.Encode("https://example.com/thelogout"), redirectUri, true); + + string state; + Assert.True(query.TryGetValue("state", out state)); + var properties = stateFormat.Unprotect(state); + Assert.Equal("https://example.com/postlogout", properties.RedirectUri, true); + } + + [Fact] + public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var server = TestServerBuilder.CreateServer(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.StateDataFormat = stateFormat; + o.ClientId = "Test Id"; + o.Configuration = configuration; + o.SignedOutRedirectUri = "https://example.com/postlogout"; + }); + + var transaction = await server.SendAsync("https://example.com/signout_with_specific_redirect_uri"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&') + .Select(each => each.Split('=')) + .ToDictionary(pair => pair[0], pair => pair[1]); + + string redirectUri; + Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri)); + Assert.Equal(UrlEncoder.Default.Encode("https://example.com/signout-callback-oidc"), redirectUri, true); + + string state; + Assert.True(query.TryGetValue("state", out state)); + var properties = stateFormat.Unprotect(state); + Assert.Equal("http://www.example.com/specific_redirect_uri", properties.RedirectUri, true); + } + + [Fact] + public async Task SignOut_WithMissingConfig_Throws() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = new OpenIdConnectConfiguration(); + }); + var server = setting.CreateTestServer(); + + var exception = await Assert.ThrowsAsync(() => server.SendAsync(DefaultHost + TestServerBuilder.Signout)); + Assert.Equal("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.", exception.Message); + } + + // Test Cases for calculating the expiration time of cookie from cookie name + [Fact] + public void NonceCookieExpirationTime() + { + DateTime utcNow = DateTime.UtcNow; + + Assert.Equal(DateTime.MaxValue, GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime("", TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1))); + + Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + } + + private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) + { + DateTime nonceTime = DateTime.MinValue; + string timestamp = null; + int endOfTimestamp; + if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal)) + { + timestamp = keyname.Substring(noncePrefix.Length); + endOfTimestamp = timestamp.IndexOf('.'); + + if (endOfTimestamp != -1) + { + timestamp = timestamp.Substring(0, endOfTimestamp); + try + { + nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture)); + if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime)) + nonceTime = DateTime.MaxValue; + else + nonceTime += nonceLifetime; + } + catch + { + } + } + } + + return nonceTime; + } + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs new file mode 100644 index 0000000000..c37da8c043 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class TestServerBuilder + { + public static readonly string DefaultAuthority = @"https://login.microsoftonline.com/common"; + public static readonly string TestHost = @"https://example.com"; + public static readonly string Challenge = "/challenge"; + public static readonly string ChallengeWithOutContext = "/challengeWithOutContext"; + public static readonly string ChallengeWithProperties = "/challengeWithProperties"; + public static readonly string Signin = "/signin"; + public static readonly string Signout = "/signout"; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions() => + new OpenIdConnectOptions + { + Authority = DefaultAuthority, + ClientId = Guid.NewGuid().ToString(), + Configuration = CreateDefaultOpenIdConnectConfiguration() + }; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions(Action update) + { + var options = CreateOpenIdConnectOptions(); + update?.Invoke(options); + return options; + } + + public static OpenIdConnectConfiguration CreateDefaultOpenIdConnectConfiguration() => + new OpenIdConnectConfiguration() + { + AuthorizationEndpoint = DefaultAuthority + "/oauth2/authorize", + EndSessionEndpoint = DefaultAuthority + "/oauth2/endsessionendpoint", + TokenEndpoint = DefaultAuthority + "/oauth2/token" + }; + + public static IConfigurationManager CreateDefaultOpenIdConnectConfigurationManager() => + new StaticConfigurationManager(CreateDefaultOpenIdConnectConfiguration()); + + public static TestServer CreateServer(Action options) + { + return CreateServer(options, handler: null, properties: null); + } + + public static TestServer CreateServer( + Action options, + Func handler, + AuthenticationProperties properties) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + + if (req.Path == new PathString(Challenge)) + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString(ChallengeWithProperties)) + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties); + } + else if (req.Path == new PathString(ChallengeWithOutContext)) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString(Signin)) + { + await context.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal()); + } + else if (req.Path == new PathString(Signout)) + { + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) + { + await context.SignOutAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddOpenIdConnect(options); + }); + + return new TestServer(builder); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs new file mode 100644 index 0000000000..609aed6f6a --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal static class TestServerExtensions + { + public static Task SendAsync(this TestServer server, string url) + { + return SendAsync(server, url, cookieHeader: null); + } + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + + var transaction = new TestTransaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs new file mode 100644 index 0000000000..a1e0233f3a --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs @@ -0,0 +1,351 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + /// + /// This helper class is used to check that query string parameters are as expected. + /// + internal class TestSettings + { + private readonly Action _configureOptions; + private OpenIdConnectOptions _options; + + public TestSettings() : this(configure: null) + { + } + + public TestSettings(Action configure) + { + _configureOptions = o => + { + configure?.Invoke(o); + _options = o; + _options.BackchannelHttpHandler = new MockBackchannel(); + }; + } + + public UrlEncoder Encoder => UrlEncoder.Default; + + public string ExpectedState { get; set; } + + public TestServer CreateTestServer(AuthenticationProperties properties = null) => TestServerBuilder.CreateServer(_configureOptions, handler: null, properties: properties); + + public IDictionary ValidateChallengeFormPost(string responseBody, params string[] parametersToValidate) + { + IDictionary formInputs = null; + var errors = new List(); + var xdoc = XDocument.Parse(responseBody.Replace("doctype", "DOCTYPE")); + var forms = xdoc.Descendants("form"); + if (forms.Count() != 1) + { + errors.Add("Only one form element is expected in response body."); + } + else + { + formInputs = forms.Single() + .Elements("input") + .ToDictionary(elem => elem.Attribute("name").Value, + elem => elem.Attribute("value").Value); + + ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false); + } + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The challenge form post is not valid."); + // buf.AppendLine(); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return formInputs; + } + + public IDictionary ValidateSignoutFormPost(TestTransaction transaction, params string[] parametersToValidate) + { + IDictionary formInputs = null; + var errors = new List(); + var xdoc = XDocument.Parse(transaction.ResponseText.Replace("doctype", "DOCTYPE")); + var forms = xdoc.Descendants("form"); + if (forms.Count() != 1) + { + errors.Add("Only one form element is expected in response body."); + } + else + { + formInputs = forms.Single() + .Elements("input") + .ToDictionary(elem => elem.Attribute("name").Value, + elem => elem.Attribute("value").Value); + + ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false); + } + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The signout form post is not valid."); + // buf.AppendLine(); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return formInputs; + } + + public IDictionary ValidateChallengeRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Authentication, parametersToValidate); + + public IDictionary ValidateSignoutRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Logout, parametersToValidate); + + private IDictionary ValidateRedirectCore(Uri redirectUri, OpenIdConnectRequestType requestType, string[] parametersToValidate) + { + var errors = new List(); + + // Validate the authority + ValidateExpectedAuthority(redirectUri.AbsoluteUri, errors, requestType); + + // Convert query to dictionary + var queryDict = string.IsNullOrEmpty(redirectUri.Query) ? + new Dictionary() : + redirectUri.Query.TrimStart('?').Split('&').Select(part => part.Split('=')).ToDictionary(parts => parts[0], parts => parts[1]); + + // Validate the query string parameters + ValidateParameters(queryDict, parametersToValidate, errors, htmlEncoded: true); + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The redirect uri is not valid."); + buf.AppendLine(redirectUri.AbsoluteUri); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return queryDict; + } + + private void ValidateParameters( + IDictionary actualValues, + IEnumerable parametersToValidate, + ICollection errors, + bool htmlEncoded) + { + foreach (var paramToValidate in parametersToValidate) + { + switch (paramToValidate) + { + case OpenIdConnectParameterNames.ClientId: + ValidateClientId(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseType: + ValidateResponseType(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseMode: + ValidateResponseMode(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Scope: + ValidateScope(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.RedirectUri: + ValidateRedirectUri(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Resource: + ValidateResource(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.State: + ValidateState(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.SkuTelemetry: + ValidateSkuTelemetry(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.VersionTelemetry: + ValidateVersionTelemetry(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.PostLogoutRedirectUri: + ValidatePostLogoutRedirectUri(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.MaxAge: + ValidateMaxAge(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Prompt: + ValidatePrompt(actualValues, errors, htmlEncoded); + break; + default: + throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\"."); + } + } + } + + private void ValidateExpectedAuthority(string absoluteUri, ICollection errors, OpenIdConnectRequestType requestType) + { + string expectedAuthority; + switch (requestType) + { + case OpenIdConnectRequestType.Token: + expectedAuthority = _options.Configuration?.TokenEndpoint ?? _options.Authority + @"/oauth2/token"; + break; + case OpenIdConnectRequestType.Logout: + expectedAuthority = _options.Configuration?.EndSessionEndpoint ?? _options.Authority + @"/oauth2/logout"; + break; + default: + expectedAuthority = _options.Configuration?.AuthorizationEndpoint ?? _options.Authority + @"/oauth2/authorize"; + break; + } + + if (!absoluteUri.StartsWith(expectedAuthority)) + { + errors.Add($"ExpectedAuthority: {expectedAuthority}"); + } + } + + private void ValidateClientId(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ClientId, _options.ClientId, actualParams, errors, htmlEncoded); + + private void ValidateResponseType(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ResponseType, _options.ResponseType, actualParams, errors, htmlEncoded); + + private void ValidateResponseMode(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ResponseMode, _options.ResponseMode, actualParams, errors, htmlEncoded); + + private void ValidateScope(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.Scope, string.Join(" ", _options.Scope), actualParams, errors, htmlEncoded); + + private void ValidateRedirectUri(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.RedirectUri, TestServerBuilder.TestHost + _options.CallbackPath, actualParams, errors, htmlEncoded); + + private void ValidateResource(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.RedirectUri, _options.Resource, actualParams, errors, htmlEncoded); + + private void ValidateState(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.State, ExpectedState, actualParams, errors, htmlEncoded); + + private void ValidateSkuTelemetry(IDictionary actualParams, ICollection errors, bool htmlEncoded) => +#if NETCOREAPP2_0 || NETCOREAPP2_1 + ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NETSTANDARD1_4", actualParams, errors, htmlEncoded); +#elif NET461 + ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NET451", actualParams, errors, htmlEncoded); +#else +#error Invalid target framework. +#endif + + private void ValidateVersionTelemetry(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.VersionTelemetry, typeof(OpenIdConnectMessage).GetTypeInfo().Assembly.GetName().Version.ToString(), actualParams, errors, htmlEncoded); + + private void ValidatePostLogoutRedirectUri(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.PostLogoutRedirectUri, "https://example.com/signout-callback-oidc", actualParams, errors, htmlEncoded); + + private void ValidateMaxAge(IDictionary actualQuery, ICollection errors, bool htmlEncoded) + { + if(_options.MaxAge.HasValue) + { + Assert.Equal(TimeSpan.FromMinutes(20), _options.MaxAge.Value); + string expectedMaxAge = "1200"; + ValidateParameter(OpenIdConnectParameterNames.MaxAge, expectedMaxAge, actualQuery, errors, htmlEncoded); + } + else if(actualQuery.ContainsKey(OpenIdConnectParameterNames.MaxAge)) + { + errors.Add($"Parameter {OpenIdConnectParameterNames.MaxAge} is present but it should be absent"); + } + } + + private void ValidatePrompt(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.Prompt, _options.Prompt, actualParams, errors, htmlEncoded); + + private void ValidateParameter( + string parameterName, + string expectedValue, + IDictionary actualParams, + ICollection errors, + bool htmlEncoded) + { + string actualValue; + if (actualParams.TryGetValue(parameterName, out actualValue)) + { + if (htmlEncoded) + { + expectedValue = Encoder.Encode(expectedValue); + } + + if (actualValue != expectedValue) + { + errors.Add($"Parameter {parameterName}'s expected value is '{expectedValue}' but its actual value is '{actualValue}'"); + } + } + else + { + errors.Add($"Parameter {parameterName} is missing"); + } + } + + private class MockBackchannel : HttpMessageHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/.well-known/openid-configuration")) + { + return await ReturnResource("wellknownconfig.json"); + } + if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/discovery/keys")) + { + return await ReturnResource("wellknownkeys.json"); + } + + throw new NotImplementedException(); + } + + private async Task ReturnResource(string resource) + { + var resourceName = "Microsoft.AspNetCore.Authentication.Test.OpenIdConnect." + resource; + using (var stream = typeof(MockBackchannel).Assembly.GetManifestResourceStream(resourceName)) + using (var reader = new StreamReader(stream)) + { + var body = await reader.ReadToEndAsync(); + var content = new StringContent(body, Encoding.UTF8, "application/json"); + return new HttpResponseMessage() + { + Content = content, + }; + } + } + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs new file mode 100644 index 0000000000..4f924172c6 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class TestTransaction + { + public HttpRequestMessage Request { get; set; } + + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json new file mode 100644 index 0000000000..4d46a8cf0a --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json @@ -0,0 +1,23 @@ +{ + "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/authorize", + "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token", + "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ], + "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys", + "response_modes_supported": [ "query", "fragment", "form_post" ], + "subject_types_supported": [ "pairwise" ], + "id_token_signing_alg_values_supported": [ "RS256" ], + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout", + "response_types_supported": [ "code", "id_token", "code id_token", "token id_token", "token" ], + "scopes_supported": [ "openid" ], + "issuer": "https://sts.windows.net/{tenantid}/", + "claims_supported": [ "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname" ], + "microsoft_multi_refresh_token": true, + "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession", + "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo", + "tenant_region_scope": null, + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com" +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json new file mode 100644 index 0000000000..77cc5562af --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json @@ -0,0 +1,31 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "SSQdhI1cKvhQEDSJxE2gGYs40Q0", + "x5t": "SSQdhI1cKvhQEDSJxE2gGYs40Q0", + "n": "pJUB90EMxiNjgkVz5CLLUuG5bYwirL2LXfVsq_nnY686WzbinkvFnNs6LvrJ6DWD5NV1-0Tq2eZj7WU8H9ytmDPsRnJ0b49gRCJYOg6-SdOe9Tl0lB0IBJE1aWh3OdCVrZLE4LH4-LGIDrkwnCV8dKFkO3EIUYPaEysL4g4wLx-TCfpMWE37XC09P-nBRVkRNcihrzY38_MC42NkRdDwByZemXkQKddnn5Y5o4rVzPGqQy3vjmTjKolYEIBYa7n3yF0848MG0k338bjnyceJgmZzjxttkWTVDikQXSldbu3QCrCAlipbWPUAXaZK8buY8LP80G4U_wx4LuZ_Krq5OQ", + "e": "AQAB", + "x5c": [ "MIIDBTCCAe2gAwIBAgIQHJ7yHxNEM7tBeqcRTMBhhTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDEwODAwMDAwMFoXDTIwMDEwOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKSVAfdBDMYjY4JFc+Qiy1LhuW2MIqy9i131bKv552OvOls24p5LxZzbOi76yeg1g+TVdftE6tnmY+1lPB/crZgz7EZydG+PYEQiWDoOvknTnvU5dJQdCASRNWlodznQla2SxOCx+PixiA65MJwlfHShZDtxCFGD2hMrC+IOMC8fkwn6TFhN+1wtPT/pwUVZETXIoa82N/PzAuNjZEXQ8AcmXpl5ECnXZ5+WOaOK1czxqkMt745k4yqJWBCAWGu598hdPOPDBtJN9/G458nHiYJmc48bbZFk1Q4pEF0pXW7t0AqwgJYqW1j1AF2mSvG7mPCz/NBuFP8MeC7mfyq6uTkCAwEAAaMhMB8wHQYDVR0OBBYEFFVWt/4iTcBiWk+EX1dCKZGoAlBQMA0GCSqGSIb3DQEBCwUAA4IBAQCJrLUiCfldfDFhZLG08DpLcwpn8Mvhm61ZpPANyCawRgaoBOsxFJew1qpC2wfXh3yUwJEIkkTGXAsiWENkeqmjd2XXQwZuIHIo5/KyZLfo2m1CmEH/QXL6uRx1f8liPr4efC8H+7/sduf6nPfk0UpNsAHA6RxJFy2jg2wHU+Ux4jK4Gc5d/rJPhJhvyS9Zax1hbTlf+N32ZvS780VMDb/nf2LdtACL0ya/+KSDGVXS3GhS9FLEXrBNjq921ghVIFJhPzm54yfeupIwz+zfISoTHIYw37i7iNUbkvDrm14a27pwLS/tfSuJHKcGPt1sjMu6SS/pf1BlvdoFkKdLLaUb" ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FSimuFrFNoC0sJXGmv13nNZceDc", + "x5t": "FSimuFrFNoC0sJXGmv13nNZceDc", + "n": "yCYaJF8uHoV2L31cjZUDdcodK1Y1EsTLkDD-DEXFyGeHaQ92T9t6MU6zazBzHvJRarG6OMI1GwsFxZ9opSVOeuRjuL3H2ehmUyuKOAnL8uT4cfkdfbg9AIN_63COccfFn0br_xUszZ7lkF5mb63sze-G66YQcbdTCWgsXpxR6491b57Gc4HVTV8cEgU4byezhJIiirrPDmt23QJIjr6XtvUMSNW88u0kX7PKOUnVCns2AG8DB2I-JExTiXwhFVu5JUqgpgmjIngvd5eyNzOgFJMnpWNXabKDP3oMLvQxjdq9xwWuTu0IQLpmUxEF9jVc8vKV1Pu2xHcS7ON5xJrUzw", + "e": "AQAB", + "x5c": [ "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8", + "x5t": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8", + "n": "oZ-QQrNuB4ei9ATYrT61ebPtvwwYWnsrTpp4ISSp6niZYb92XM0oUTNgqd_C1vGN8J-y9wCbaJWkpBf46CjdZehrqczPhzhHau8WcRXocSB1u_tuZhv1ooAZ4bAcy79UkeLiG60HkuTNJJC8CfaTp1R97szBhuk0Vz5yt4r5SpfewIlBCnZUYwkDS172H9WapQu-3P2Qjh0l-JLyCkdrhvizZUk0atq5_AIDKRU-A0pRGc-EZhUL0LqUMz6c6M2s_4GnQaScv44A5iZUDD15B6e8Apb2yARohkWmOnmRcTVfes8EkfxjzZEzm3cNkvP0ogILyISHKlkzy2OmlU6iXw", + "e": "AQAB", + "x5c": [ "MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=" ] + } + ] +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs new file mode 100644 index 0000000000..368026beb8 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs @@ -0,0 +1,487 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class PolicyTests + { + [Fact] + public async Task CanDispatch() + { + var server = CreateServer(services => + { + services.AddLogging().AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + o.AddScheme("auth3", "auth3"); + }) + .AddPolicyScheme("policy1", "policy1", p => + { + p.ForwardDefault = "auth1"; + }) + .AddPolicyScheme("policy2", "policy2", p => + { + p.ForwardAuthenticate = "auth2"; + }); + }); + + var transaction = await server.SendAsync("http://example.com/auth/policy1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + + transaction = await server.SendAsync("http://example.com/auth/auth1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + + transaction = await server.SendAsync("http://example.com/auth/auth2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + + transaction = await server.SendAsync("http://example.com/auth/auth3"); + Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3")); + + transaction = await server.SendAsync("http://example.com/auth/policy2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + } + + [Fact] + public async Task DefaultTargetSelectorWinsOverDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth2"; + p.ForwardDefaultSelector = ctx => "auth1"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task NullDefaultTargetSelectorFallsBacktoDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth1"; + p.ForwardDefaultSelector = ctx => null; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task SpecificTargetAlwaysWinsOverDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth2"; + p.ForwardDefaultSelector = ctx => "auth2"; + p.ForwardAuthenticate = "auth1"; + p.ForwardSignIn = "auth1"; + p.ForwardSignOut = "auth1"; + p.ForwardForbid = "auth1"; + p.ForwardChallenge = "auth1"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task VirtualSchemeTargetsForwardWithDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => p.ForwardDefault= "auth1"); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task VirtualSchemeTargetsOverrideDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => + { + p.ForwardDefault= "auth1"; + p.ForwardChallenge = "auth2"; + p.ForwardSignIn = "auth2"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(1, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(1, handler2.SignInCount); + } + + [Fact] + public async Task CanDynamicTargetBasedOnQueryString() + { + var server = CreateServer(services => + { + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + o.AddScheme("auth3", "auth3"); + }) + .AddPolicyScheme("dynamic", "dynamic", p => + { + p.ForwardDefaultSelector = c => c.Request.QueryString.Value.Substring(1); + }); + }); + + var transaction = await server.SendAsync("http://example.com/auth/dynamic?auth1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + transaction = await server.SendAsync("http://example.com/auth/dynamic?auth2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + transaction = await server.SendAsync("http://example.com/auth/dynamic?auth3"); + Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3")); + } + + private class TestHandler : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + private class TestHandler2 : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + private static TestServer CreateServer(Action configure = null, string defaultScheme = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path.StartsWithSegments(new PathString("/auth"), out var remainder)) + { + var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; + var result = await context.AuthenticateAsync(name); + res.Describe(result?.Ticket?.Principal); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + configure?.Invoke(services); + }); + return new TestServer(builder); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs new file mode 100644 index 0000000000..bda4b09fa7 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.DataHandler +{ + public class SecureDataFormatTests + { + public SecureDataFormatTests() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDataProtection(); + ServiceProvider = serviceCollection.BuildServiceProvider(); + } + + public IServiceProvider ServiceProvider { get; } + + [Fact] + public void ProtectDataRoundTrips() + { + var provider = ServiceProvider.GetRequiredService(); + var prototector = provider.CreateProtector("test"); + var secureDataFormat = new SecureDataFormat(new StringSerializer(), prototector); + + string input = "abcdefghijklmnopqrstuvwxyz0123456789"; + var protectedData = secureDataFormat.Protect(input); + var result = secureDataFormat.Unprotect(protectedData); + Assert.Equal(input, result); + } + + [Fact] + public void ProtectWithPurposeRoundTrips() + { + var provider = ServiceProvider.GetRequiredService(); + var prototector = provider.CreateProtector("test"); + var secureDataFormat = new SecureDataFormat(new StringSerializer(), prototector); + + string input = "abcdefghijklmnopqrstuvwxyz0123456789"; + string purpose = "purpose1"; + var protectedData = secureDataFormat.Protect(input, purpose); + var result = secureDataFormat.Unprotect(protectedData, purpose); + Assert.Equal(input, result); + } + + [Fact] + public void UnprotectWithDifferentPurposeFails() + { + var provider = ServiceProvider.GetRequiredService(); + var prototector = provider.CreateProtector("test"); + var secureDataFormat = new SecureDataFormat(new StringSerializer(), prototector); + + string input = "abcdefghijklmnopqrstuvwxyz0123456789"; + string purpose = "purpose1"; + var protectedData = secureDataFormat.Protect(input, purpose); + var result = secureDataFormat.Unprotect(protectedData); // Null other purpose + Assert.Null(result); + + result = secureDataFormat.Unprotect(protectedData, "purpose2"); + Assert.Null(result); + } + + private class StringSerializer : IDataSerializer + { + public byte[] Serialize(string model) + { + return Encoding.UTF8.GetBytes(model); + } + + public string Deserialize(byte[] data) + { + return Encoding.UTF8.GetString(data); + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs new file mode 100644 index 0000000000..c34e4fd2da --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TestClock : ISystemClock + { + public TestClock() + { + UtcNow = new DateTimeOffset(2013, 6, 11, 12, 34, 56, 789, TimeSpan.Zero); + } + + public DateTimeOffset UtcNow { get; set; } + + public void Add(TimeSpan timeSpan) + { + UtcNow = UtcNow + timeSpan; + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs new file mode 100644 index 0000000000..87d6d95a2c --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class TestExtensions + { + public const string CookieAuthenticationScheme = "External"; + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (principal != null) + { + foreach (var identity in principal.Identities) + { + xml.Add(identity.Claims.Select(claim => + new XElement("claim", new XAttribute("type", claim.Type), + new XAttribute("value", claim.Value), + new XAttribute("issuer", claim.Issuer)))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + res.Body.Write(xmlBytes, 0, xmlBytes.Length); + } + + public static void Describe(this HttpResponse res, IEnumerable tokens) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (tokens != null) + { + foreach (var token in tokens) + { + xml.Add(new XElement("token", new XAttribute("name", token.Name), + new XAttribute("value", token.Value))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + res.Body.Write(xmlBytes, 0, xmlBytes.Length); + } + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs new file mode 100644 index 0000000000..cd9fe9fb1a --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Tests +{ + public class TestAuthHandler : AuthenticationHandler, IAuthenticationSignInHandler + { + public TestAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + protected override Task HandleAuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler2 : TestHandler + { + } + + public class TestHandler3 : TestHandler + { + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs new file mode 100644 index 0000000000..5289e38809 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + public Func Sender { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + if (Sender != null) + { + return Task.FromResult(Sender(request)); + } + + return Task.FromResult(null); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs new file mode 100644 index 0000000000..a1e58743b6 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TicketSerializerTests + { + [Fact] + public void CanRoundTripEmptyPrincipal() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + properties.RedirectUri = "bye"; + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Empty(readTicket.Principal.Identities); + Assert.Equal("bye", readTicket.Properties.RedirectUri); + Assert.Equal("Hello", readTicket.AuthenticationScheme); + } + } + + [Fact] + public void CanRoundTripBootstrapContext() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { BootstrapContext = "bootstrap" }); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Single(readTicket.Principal.Identities); + Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); + Assert.Equal("bootstrap", readTicket.Principal.Identities.First().BootstrapContext); + } + } + + [Fact] + public void CanRoundTripActorIdentity() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + + var actor = new ClaimsIdentity("actor"); + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { Actor = actor }); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Single(readTicket.Principal.Identities); + Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); + + var identity = (ClaimsIdentity) readTicket.Principal.Identity; + Assert.NotNull(identity.Actor); + Assert.Equal("actor", identity.Actor.AuthenticationType); + } + } + + [ConditionalFact] + [FrameworkSkipCondition( + RuntimeFrameworks.Mono, + SkipReason = "Test fails with Mono 4.0.4. Build rarely reaches testing with Mono 4.2.1")] + public void CanRoundTripClaimProperties() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + + var claim = new Claim("type", "value", "valueType", "issuer", "original-issuer"); + claim.Properties.Add("property-1", "property-value"); + + // Note: a null value MUST NOT result in a crash + // and MUST instead be treated like an empty string. + claim.Properties.Add("property-2", null); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + ticket.Principal.AddIdentity(new ClaimsIdentity(new[] { claim }, "misc")); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Single(readTicket.Principal.Identities); + Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); + + var readClaim = readTicket.Principal.FindFirst("type"); + Assert.NotNull(claim); + Assert.Equal("type", claim.Type); + Assert.Equal("value", claim.Value); + Assert.Equal("valueType", claim.ValueType); + Assert.Equal("issuer", claim.Issuer); + Assert.Equal("original-issuer", claim.OriginalIssuer); + + var property1 = readClaim.Properties["property-1"]; + Assert.Equal("property-value", property1); + + var property2 = readClaim.Properties["property-2"]; + Assert.Equal(string.Empty, property2); + } + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs new file mode 100644 index 0000000000..4d4023bee5 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs @@ -0,0 +1,181 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TokenExtensionTests + { + [Fact] + public void CanStoreMultipleTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.Equal("1", props.GetTokenValue("One")); + Assert.Equal("2", props.GetTokenValue("Two")); + Assert.Equal("3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void SubsequentStoreTokenDeletesPreviousTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + + props.StoreTokens(tokens); + + props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); + + Assert.Equal("0", props.GetTokenValue("Zero")); + Assert.Null(props.GetTokenValue("One")); + Assert.Null(props.GetTokenValue("Two")); + Assert.Null(props.GetTokenValue("Three")); + Assert.Single(props.GetTokens()); + } + + [Fact] + public void CanUpdateTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + tok1.Value = ".1"; + tok2.Value = ".2"; + tok3.Value = ".3"; + props.StoreTokens(tokens); + + Assert.Equal(".1", props.GetTokenValue("One")); + Assert.Equal(".2", props.GetTokenValue("Two")); + Assert.Equal(".3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void CanUpdateTokenValues() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.True(props.UpdateTokenValue("One", ".11")); + Assert.True(props.UpdateTokenValue("Two", ".22")); + Assert.True(props.UpdateTokenValue("Three", ".33")); + + Assert.Equal(".11", props.GetTokenValue("One")); + Assert.Equal(".22", props.GetTokenValue("Two")); + Assert.Equal(".33", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void UpdateTokenValueReturnsFalseForUnknownToken() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.False(props.UpdateTokenValue("ONE", ".11")); + Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); + + Assert.Null(props.GetTokenValue("ONE")); + Assert.Null(props.GetTokenValue("Jigglypuff")); + Assert.Equal(3, props.GetTokens().Count()); + + } + + //public class TestAuthHandler : IAuthenticationHandler + //{ + // private readonly AuthenticationProperties _props; + // public TestAuthHandler(AuthenticationProperties props) + // { + // _props = props; + // } + + // public Task AuthenticateAsync(AuthenticateContext context) + // { + // context.Authenticated(new ClaimsPrincipal(), _props.Items, new Dictionary()); + // return Task.FromResult(0); + // } + + // public Task ChallengeAsync(AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + + // public void GetDescriptions(DescribeSchemesContext context) + // { + // throw new NotImplementedException(); + // } + + // public Task SignInAsync(ClaimsPrincipal principal, AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + + // public Task SignOutAsync(AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + //} + + //[Fact] + //public async Task CanGetTokenFromContext() + //{ + // var props = new AuthenticationProperties(); + // var tokens = new List(); + // var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + // var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + // var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + // tokens.Add(tok1); + // tokens.Add(tok2); + // tokens.Add(tok3); + // props.StoreTokens(tokens); + + // var context = new DefaultHttpContext(); + // var handler = new TestAuthHandler(props); + // context.Features.Set(new HttpAuthenticationFeature() { Handler = handler }); + + // Assert.Equal("1", await context.GetTokenAsync("One")); + // Assert.Equal("2", await context.GetTokenAsync("Two")); + // Assert.Equal("3", await context.GetTokenAsync("Three")); + //} + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs new file mode 100644 index 0000000000..f7128a6f11 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme + "=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType, string issuer = null) + { + var claim = ResponseElement.Elements("claim") + .SingleOrDefault(elt => elt.Attribute("type").Value == claimType && + (issuer == null || elt.Attribute("issuer").Value == issuer)); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + public string FindTokenValue(string name) + { + var claim = ResponseElement.Elements("token") + .SingleOrDefault(elt => elt.Attribute("name").Value == name); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs new file mode 100644 index 0000000000..c1937d136c --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs @@ -0,0 +1,685 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + public class TwitterTests + { + private void ConfigureDefaults(TwitterOptions o) + { + o.ConsumerKey = "whatever"; + o.ConsumerSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = TwitterDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddTwitter(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.SignInScheme = TwitterDefaults.AuthenticationScheme; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddTwitter(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(TwitterDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("TwitterHandler", scheme.HandlerType.Name); + Assert.Equal(TwitterDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.Events = new TwitterEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + }, + async context => + { + await context.ChallengeAsync("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + /// + /// Validates the Twitter Options to check if the Consumer Key is missing in the TwitterOptions and if so throws the ArgumentException + /// + /// + [Fact] + public async Task ThrowsIfClientIdMissing() + { + var server = CreateServer(o => + { + o.ConsumerSecret = "Test Consumer Secret"; + }); + + await Assert.ThrowsAsync("ConsumerKey", async () => await server.SendAsync("http://example.com/challenge")); + } + + /// + /// Validates the Twitter Options to check if the Consumer Secret is missing in the TwitterOptions and if so throws the ArgumentException + /// + /// + [Fact] + public async Task ThrowsIfClientSecretMissing() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + }); + + await Assert.ThrowsAsync("ConsumerSecret", async () => await server.SendAsync("http://example.com/challenge")); + } + + [Fact] + public async Task BadSignInWillThrow() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + + // Send a bogus sign in + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-twitter")); + Assert.Equal("Invalid state cookie.", error.GetBaseException().Message); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + }, + async context => + { + await context.ChallengeAsync("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + } + + [Fact] + public async Task BadCallbackCallsRemoteAuthFailedWithState() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + o.Events = new TwitterEvents() + { + OnRemoteFailure = context => + { + Assert.NotNull(context.Failure); + Assert.Equal("The user denied permissions.", context.Failure.Message); + Assert.NotNull(context.Properties); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.Items["testkey"] = "testvalue"; + await context.ChallengeAsync("Twitter", properties); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie)); + Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues)); + Assert.Single(setCookieValues); + var setCookieValue = setCookieValues.Single(); + var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value); + + var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG"); + request.Headers.Add(HeaderNames.Cookie, cookie.ToString()); + var client = server.CreateClient(); + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + private static TestServer CreateServer(Action options, Func> handler = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Twitter", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Twitter")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Twitter")); + } + else if (handler == null || ! await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + Action wrapOptions = o => + { + o.SignInScheme = "External"; + options(o); + }; + services.AddAuthentication() + .AddCookie("External", _ => { }) + .AddTwitter(wrapOptions); + }); + return new TestServer(builder); + } + + private HttpResponseMessage BackchannelRequestToken(HttpRequestMessage req) + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }; + } + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs new file mode 100644 index 0000000000..0de867d286 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + public class CustomStateDataFormat : ISecureDataFormat + { + public const string ValidStateData = "ValidStateData"; + + private string lastSavedAuthenticationProperties; + private DataContractSerializer serializer = new DataContractSerializer(typeof(AuthenticationProperties)); + + public string Protect(AuthenticationProperties data) + { + lastSavedAuthenticationProperties = Serialize(data); + return ValidStateData; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + return Protect(data); + } + + public AuthenticationProperties Unprotect(string state) + { + return state == ValidStateData ? DeSerialize(lastSavedAuthenticationProperties) : null; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + return Unprotect(protectedText); + } + + private string Serialize(AuthenticationProperties data) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + serializer.WriteObject(memoryStream, data); + memoryStream.Position = 0; + return new StreamReader(memoryStream).ReadToEnd(); + } + } + + private AuthenticationProperties DeSerialize(string state) + { + var stateDataAsBytes = Encoding.UTF8.GetBytes(state); + + using (var ms = new MemoryStream(stateDataAsBytes, false)) + { + return (AuthenticationProperties)serializer.ReadObject(ms); + } + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml new file mode 100644 index 0000000000..dfdb0d68d0 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml @@ -0,0 +1,83 @@ + + + 2014-04-18T20:21:17.341Z + 2014-04-19T08:21:17.341Z + + + +
http://automation1/
+
+
+ + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + + + + + + + Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g= + + + bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg== + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I + + + + + http://Automation1 + + + + + Test + + + Test + + + user1@praburajgmail.onmicrosoft.com + + + 4afbc689-805b-48cf-a24c-d4aa3248a248 + + + c2f0cd49-5e53-4520-8ed9-4e178dc488c5 + + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +
\ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs new file mode 100644 index 0000000000..dfe8607242 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + internal class TestSecurityToken : SecurityToken + { + public override string Id => "id"; + + public override string Issuer => "issuer"; + + public override SecurityKey SecurityKey => throw new NotImplementedException(); + + public override SecurityKey SigningKey + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override DateTime ValidFrom => new DateTime(2008, 3, 22); + + public override DateTime ValidTo => new DateTime(2017, 3, 22); + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs new file mode 100644 index 0000000000..05882518f9 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + internal class TestSecurityTokenValidator : ISecurityTokenValidator + { + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes { get; set; } = 1024 * 5; + + public bool CanReadToken(string securityToken) + { + return true; + } + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + if (!string.IsNullOrEmpty(securityToken) && securityToken.Contains("ThisIsAValidToken")) + { + validatedToken = new TestSecurityToken(); + return new ClaimsPrincipal(new ClaimsIdentity("Test")); + } + + throw new SecurityTokenException("The security token did not contain ThisIsAValidToken"); + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml new file mode 100644 index 0000000000..2addae96c1 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml @@ -0,0 +1,83 @@ + + + 2014-04-18T20:21:17.341Z + 2014-04-19T08:21:17.341Z + + + +
http://automation1/
+
+
+ + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + + + + + + + Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g= + + + bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg== + + + ThisIsAValidToken + + + + + t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I + + + + + http://Automation1 + + + + + Test + + + Test + + + user1@praburajgmail.onmicrosoft.com + + + 4afbc689-805b-48cf-a24c-d4aa3248a248 + + + c2f0cd49-5e53-4520-8ed9-4e178dc488c5 + + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +
\ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs new file mode 100644 index 0000000000..bc1ef757f1 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs @@ -0,0 +1,443 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + public class WsFederationTest + { + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddWsFederation(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(WsFederationDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("WsFederationHandler", scheme.HandlerType.Name); + Assert.Equal(WsFederationDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task MissingConfigurationThrows() + { + var builder = new WebHostBuilder() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(); + }); + var server = new TestServer(builder); + var httpClient = server.CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var exception = await Assert.ThrowsAsync(() => httpClient.GetAsync("/")); + Assert.Equal("Provide MetadataAddress, Configuration, or ConfigurationManager to WsFederationOptions", exception.Message); + } + + [Fact] + public async Task ChallengeRedirects() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task MapWillNotAffectRedirect() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/mapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task PreMappedWillAffectRedirect() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/premapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "premapped-challenge/signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task ValidTokenIsAccepted() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/ValidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsRefused() + { + var httpClient = CreateClient(); + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var exception = await Assert.ThrowsAsync(() => httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form)); + Assert.Contains("Unsolicited logins are not allowed.", exception.InnerException.Message); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsAcceptedWhenAllowed() + { + var httpClient = CreateClient(allowUnsolicited: true); + + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task InvalidTokenIsRejected() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/InvalidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task RemoteSignoutRequestTriggersSignout() + { + var httpClient = CreateClient(); + + var response = await httpClient.GetAsync("/signin-wsfed?wa=wsignoutcleanup1.0"); + response.EnsureSuccessStatusCode(); + + var cookie = response.Headers.GetValues(HeaderNames.SetCookie).Single(); + Assert.Equal(".AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax", cookie); + Assert.Equal("OnRemoteSignOut", response.Headers.GetValues("EventHeader").Single()); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task EventsResolvedFromDI() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.EventsType = typeof(MyWsFedEvents); + }); + }) + .Configure(app => + { + app.Run(context => context.ChallengeAsync()); + }); + var server = new TestServer(builder); + + var result = await server.CreateClient().GetAsync(""); + Assert.Contains("CustomKey=CustomValue", result.Headers.Location.Query); + } + + private class MyWsFedEvents : WsFederationEvents + { + public override Task RedirectToIdentityProvider(RedirectContext context) + { + context.ProtocolMessage.SetParameter("CustomKey", "CustomValue"); + return base.RedirectToIdentityProvider(context); + } + } + + private FormUrlEncodedContent CreateSignInContent(string tokenFile, string wctx = null, bool suppressWctx = false) + { + var kvps = new List>(); + kvps.Add(new KeyValuePair("wa", "wsignin1.0")); + kvps.Add(new KeyValuePair("wresult", File.ReadAllText(tokenFile))); + if (!string.IsNullOrEmpty(wctx)) + { + kvps.Add(new KeyValuePair("wctx", wctx)); + } + if (suppressWctx) + { + kvps.Add(new KeyValuePair("suppressWctx", "true")); + } + return new FormUrlEncodedContent(kvps); + } + + private void CopyCookies(HttpResponseMessage response, HttpRequestMessage request) + { + var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList()); + foreach (var cookie in cookies) + { + if (cookie.Value.HasValue) + { + request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); + } + } + } + + private HttpClient CreateClient(bool allowUnsolicited = false) + { + var builder = new WebHostBuilder() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.StateDataFormat = new CustomStateDataFormat(); + options.SecurityTokenHandlers = new List() { new TestSecurityTokenValidator() }; + options.UseTokenLifetime = false; + options.AllowUnsolicitedLogins = allowUnsolicited; + options.Events = new WsFederationEvents() + { + OnMessageReceived = context => + { + if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress)) + { + Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value"); + } + context.HttpContext.Items["MessageReceived"] = true; + return Task.FromResult(0); + }, + OnRedirectToIdentityProvider = context => + { + if (context.ProtocolMessage.IsSignInMessage) + { + // Sign in message + context.ProtocolMessage.Wctx = "customValue"; + } + + return Task.FromResult(0); + }, + OnSecurityTokenReceived = context => + { + context.HttpContext.Items["SecurityTokenReceived"] = true; + return Task.FromResult(0); + }, + OnSecurityTokenValidated = context => + { + Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked"); + Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked"); + + if (context.Principal != null) + { + var identity = context.Principal.Identities.Single(); + identity.AddClaim(new Claim("ReturnEndpoint", "true")); + identity.AddClaim(new Claim("Authenticated", "true")); + identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String)); + } + + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + context.HttpContext.Items["AuthenticationFailed"] = true; + //Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked. + context.HttpContext.Request.Path = new PathString("/AuthenticationFailed"); + context.SkipHandler(); + return Task.FromResult(0); + }, + OnRemoteSignOut = context => + { + context.Response.Headers["EventHeader"] = "OnRemoteSignOut"; + return Task.FromResult(0); + } + }; + }); + }); + var server = new TestServer(builder); + return server.CreateClient(); + } + + private void ConfigureApp(IApplicationBuilder app) + { + app.Map("/PreMapped-Challenge", mapped => + { + mapped.UseAuthentication(); + mapped.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.UseAuthentication(); + + app.Map("/Logout", subApp => + { + subApp.Run(async context => + { + if (context.User.Identity.IsAuthenticated) + { + var authProperties = new AuthenticationProperties() { RedirectUri = context.Request.GetEncodedUrl() }; + await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, authProperties); + await context.Response.WriteAsync("Signing out..."); + } + else + { + await context.Response.WriteAsync("SignedOut"); + } + }); + }); + + app.Map("/AuthenticationFailed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("AuthenticationFailed"); + }); + }); + + app.Map("/signout-wsfed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("signout-wsfed"); + }); + }); + + app.Map("/mapped-challenge", subApp => + { + subApp.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.Run(async context => + { + var result = context.AuthenticateAsync(); + if (context.User == null || !context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + await context.Response.WriteAsync("Unauthorized"); + } + else + { + var identity = context.User.Identities.Single(); + if (identity.NameClaimType == "Name_Failed" && identity.RoleClaimType == "Role_Failed") + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("SignIn_Failed"); + } + else if (!identity.HasClaim("Authenticated", "true") || !identity.HasClaim("ReturnEndpoint", "true") || !identity.HasClaim(identity.RoleClaimType, "Guest")) + { + await context.Response.WriteAsync("Provider not invoked"); + return; + } + else + { + await context.Response.WriteAsync(WsFederationDefaults.AuthenticationScheme); + } + } + }); + } + + private class WaadMetadataDocumentHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var metadata = File.ReadAllText(@"WsFederation/federationmetadata.xml"); + var newResponse = new HttpResponseMessage() { Content = new StringContent(metadata, Encoding.UTF8, "text/xml") }; + return Task.FromResult(newResponse); + } + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml new file mode 100644 index 0000000000..920ed66a4f --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + wFJy/A1QstqtLHauYGcqwwHvn3HUW25DcWI/XLOmXOM= + + + R6fPw+BiFS9XYdkhwNJRjGxVftA2j9TdkF5d5jgR8uG1QMyuEA/Eizeq1HnnUj2Yi+sqNG+HzaZQclECeiJfi88Ry+keorDCo9KgdnjlZZc+WFzrJZeHjaDIvFD6B4OAN0mTq5kbpwr7+idzSbvyRXAnpvJxOrViZKE4HpwltGAZGDTkjsVkd8Z/wfoN7ehN4Ei7u/mOAiEU4FkWYFU/BfSVRVIUDyyQ7DGfQFJvCwHWFvsq+M1wfOUzQO5K+M9EU2m4VEP1qqbexXaZMAbcjqyUn4eN7doWjWE59jkXGbn+GR8qgCJqLOaYwXnH5XD0pMjy71aKGyLNaUb3wCwjkA== + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + UPN + UPN of the user + + + Name + The display name for the user + + + Given Name + First name of the user + + + Surname + Last name of the user + + + Authentication Instant + The time (UTC) at which the user authenticated to the identity provider + + + Authentication Method + The method of authentication used by the identity provider + + + TenantId + Identifier for the user's tenant + + + IdentityProvider + Identity provider for the user. + + + + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+
+ + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + +
https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+
+ + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + +
\ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer new file mode 100644 index 0000000000..bfd5220e2c Binary files /dev/null and b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer differ diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer new file mode 100644 index 0000000000..6acc7af5a6 Binary files /dev/null and b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer differ diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs new file mode 100644 index 0000000000..143be1b9be --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class AuthorizationPolicyFacts + { + [Fact] + public void RequireRoleThrowsIfEmpty() + { + Assert.Throws(() => new AuthorizationPolicyBuilder().RequireRole()); + } + + [Fact] + public async Task CanCombineAuthorizeAttributes() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute(), + new AuthorizeAttribute("1") { AuthenticationSchemes = "dupe" }, + new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" }, + new AuthorizeAttribute { Roles = "r1,r2", AuthenticationSchemes = "roles" }, + }; + var options = new AuthorizationOptions(); + options.AddPolicy("1", policy => policy.RequireClaim("1")); + options.AddPolicy("2", policy => policy.RequireClaim("2")); + + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.Contains("dupe", combined.AuthenticationSchemes); + Assert.Contains("roles", combined.AuthenticationSchemes); + Assert.Equal(4, combined.Requirements.Count()); + Assert.Contains(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement); + Assert.Equal(2, combined.Requirements.OfType().Count()); + Assert.Single(combined.Requirements.OfType()); + } + + [Fact] + public async Task CanReplaceDefaultPolicy() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute(), + new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" } + }; + var options = new AuthorizationOptions(); + options.DefaultPolicy = new AuthorizationPolicyBuilder("default").RequireClaim("default").Build(); + options.AddPolicy("2", policy => policy.RequireClaim("2")); + + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.Contains("dupe", combined.AuthenticationSchemes); + Assert.Contains("default", combined.AuthenticationSchemes); + Assert.Equal(2, combined.Requirements.Count()); + Assert.DoesNotContain(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement); + Assert.Equal(2, combined.Requirements.OfType().Count()); + } + + [Fact] + public async Task CombineMustTrimRoles() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute() { Roles = "r1 , r2" } + }; + var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement); + var rolesAuthorizationRequirement = combined.Requirements.OfType().First(); + Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count()); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1")); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2")); + } + + [Fact] + public async Task CombineMustTrimAuthenticationScheme() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute() { AuthenticationSchemes = "a1 , a2" } + }; + var options = new AuthorizationOptions(); + + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1")); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2")); + } + + [Fact] + public async Task CombineMustIgnoreEmptyAuthenticationScheme() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute() { AuthenticationSchemes = "a1 , , ,,, a2" } + }; + var options = new AuthorizationOptions(); + + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1")); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2")); + } + + [Fact] + public async Task CombineMustIgnoreEmptyRoles() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute() { Roles = "r1 , ,, , r2" } + }; + var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + + // Act + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); + + // Assert + Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement); + var rolesAuthorizationRequirement = combined.Requirements.OfType().First(); + Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count()); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1")); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2")); + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs new file mode 100644 index 0000000000..ef17b94620 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs @@ -0,0 +1,1168 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class DefaultAuthorizationServiceTests + { + private IAuthorizationService BuildAuthorizationService(Action setupServices = null) + { + var services = new ServiceCollection(); + services.AddAuthorization(); + services.AddLogging(); + services.AddOptions(); + setupServices?.Invoke(services); + return services.BuildServiceProvider().GetRequiredService(); + } + + [Fact] + public async Task AuthorizeCombineThrowsOnUnknownPolicy() + { + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); + + // Act + await Assert.ThrowsAsync(() => AuthorizationPolicy.CombineAsync(provider, new AuthorizeAttribute[] { + new AuthorizeAttribute { Policy = "Wut" } + })); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") })); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => { + policy.AddAuthenticationSchemes("Basic"); + policy.RequireClaim("Permission", "CanViewPage"); + }); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsAmongValues() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim("Permission", "CanViewAnything") + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldInvokeAllHandlersByDefault() + { + // Arrange + var handler1 = new FailHandler(); + var handler2 = new FailHandler(); + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(handler1); + services.AddSingleton(handler2); + services.AddAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom"); + + // Assert + Assert.False(allowed.Succeeded); + Assert.True(allowed.Failure.FailCalled); + Assert.True(handler1.Invoked); + Assert.True(handler2.Invoked); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Authorize_ShouldInvokeAllHandlersDependingOnSetting(bool invokeAllHandlers) + { + // Arrange + var handler1 = new FailHandler(); + var handler2 = new FailHandler(); + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(handler1); + services.AddSingleton(handler2); + services.AddAuthorization(options => + { + options.InvokeHandlersAfterFailure = invokeAllHandlers; + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom"); + + // Assert + Assert.False(allowed.Succeeded); + Assert.True(handler1.Invoked); + Assert.Equal(invokeAllHandlers, handler2.Invoked); + } + + private class FailHandler : IAuthorizationHandler + { + public bool Invoked { get; set; } + + public Task HandleAsync(AuthorizationHandlerContext context) + { + Invoked = true; + context.Fail(); + return Task.FromResult(0); + } + } + + [Fact] + public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + Assert.IsType(allowed.Failure.FailedRequirements.First()); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfNoClaims() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[0], + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfUserIsNull() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldAllowWithNoAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ThrowsWithUnknownPolicy() + { + // Arrange + var authorizationService = BuildAuthorizationService(); + + // Act + // Assert + var exception = await Assert.ThrowsAsync(() => authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "whatever", "BogusPolicy")); + Assert.Equal("No policy found: BogusPolicy.", exception.Message); + } + + [Fact] + public async Task Authorize_CustomRolePolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireRole("Administrator") + .RequireClaim(ClaimTypes.Role, "User"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "User"), + new Claim(ClaimTypes.Role, "Administrator") + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_HasAnyClaimOfTypePolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim(ClaimTypes.Role); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "none"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_PolicyCanAuthenticationSchemeWithNameClaim() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireClaim(ClaimTypes.Name); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task RolePolicyCanRequireSingleRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Admin") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task RolePolicyCanRequireOneOfManyRoles() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin", "Users"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Users") }, "AuthType")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task RolePolicyCanBlockWrongRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "Nope"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task RolePolicyCanBlockNoRole() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireRole("Admin", "Users")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public void PolicyThrowsWithNoRequirements() + { + Assert.Throws(() => BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => { }); + }); + })); + } + + [Fact] + public async Task RequireUserNameFailsForWrongUserName() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Tek"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task CanRequireUserName() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Hao"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CanRequireUserNameWithDiffClaimType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var identity = new ClaimsIdentity("AuthType", "Name", "Role"); + identity.AddClaim(new Claim("Name", "Hao")); + var user = new ClaimsPrincipal(identity); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CanRequireRoleWithDiffClaimType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireRole("Hao")); + }); + }); + var identity = new ClaimsIdentity("AuthType", "Name", "Role"); + identity.AddClaim(new Claim("Role", "Hao")); + var user = new ClaimsPrincipal(identity); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CanApproveAnyAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + user.AddIdentity(new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Name"), + }, + "AuthType")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CanBlockNonAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); + + // Assert + Assert.False(allowed.Succeeded); + } + + public class CustomRequirement : IAuthorizationRequirement { } + public class CustomHandler : AuthorizationHandler + { + public bool Invoked { get; set; } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement) + { + Invoked = true; + context.Succeed(requirement); + return Task.FromResult(0); + } + } + + [Fact] + public async Task CustomReqWithNoHandlerFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task CustomReqWithHandlerSucceeds() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddTransient(); + services.AddAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); + + // Assert + Assert.True(allowed.Succeeded); + } + + public class PassThroughRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public PassThroughRequirement(bool succeed) + { + Succeed = succeed; + } + + public bool Succeed { get; set; } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PassThroughRequirement requirement) + { + if (Succeed) { + context.Succeed(requirement); + } + return Task.FromResult(0); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PassThroughRequirementWillSucceedWithoutCustomHandler(bool shouldSucceed) + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Passthrough", policy => policy.Requirements.Add(new PassThroughRequirement(shouldSucceed))); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Passthrough"); + + // Assert + Assert.Equal(shouldSucceed, allowed.Succeeded); + } + + [Fact] + public async Task CanCombinePolicies() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CombinePoliciesWillFailIfBasePolicyFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.False(allowed.Succeeded); + } + + [Fact] + public async Task CombinedPoliciesWillFailIfExtraRequirementFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.False(allowed.Succeeded); + } + + public class ExpenseReport { } + + public static class Operations + { + public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" }; + public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; + public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; + } + + public class ExpenseReportAuthorizationHandler : AuthorizationHandler + { + public ExpenseReportAuthorizationHandler(IEnumerable authorized) + { + _allowed = authorized; + } + + private IEnumerable _allowed; + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) + { + if (_allowed.Contains(requirement)) + { + context.Succeed(requirement); + } + return Task.FromResult(0); + } + } + + public class SuperUserHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) + { + if (context.User.HasClaim("SuperUser", "yes")) + { + context.Succeed(requirement); + } + return Task.FromResult(0); + } + } + + [Fact] + public async Task CanAuthorizeAllSuperuserOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + services.AddTransient(); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SuperUser", "yes"), + }, + "AuthType") + ); + + // Act + // Assert + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Delete)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Create)).Succeeded); + } + + public class NotCalledHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, string resource) + { + throw new NotImplementedException(); + } + } + + public class EvenHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, int id) + { + if (id % 2 == 0) + { + context.Succeed(requirement); + } + return Task.FromResult(0); + } + } + + [Fact] + public async Task CanUseValueTypeResource() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddTransient(); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + }, + "AuthType") + ); + + // Act + // Assert + Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, 2, Operations.Edit)).Succeeded); + } + + + [Fact] + public async Task DoesNotCallHandlerWithWrongResourceType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddTransient(); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SuperUser", "yes") + }, + "AuthType") + ); + + // Act + // Assert + Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded); + } + + [Fact] + public async Task CanAuthorizeOnlyAllowedOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + }); + var user = new ClaimsPrincipal(); + + // Act + // Assert + Assert.True((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Edit)).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Delete)).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Create)).Succeeded); + } + + [Fact] + public async Task AuthorizeHandlerNotCalledWithNullResource() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + }); + var user = new ClaimsPrincipal(); + + // Act + // Assert + Assert.False((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded); + } + + [Fact] + public async Task CanAuthorizeWithAssertionRequirement() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireAssertion(context => true)); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task CanAuthorizeWithAsyncAssertionRequirement() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireAssertion(context => Task.FromResult(true))); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed.Succeeded); + } + + public class StaticPolicyProvider : IAuthorizationPolicyProvider + { + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + + public Task GetPolicyAsync(string policyName) + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + } + + [Fact] + public async Task CanReplaceDefaultPolicyProvider() + { + var authorizationService = BuildAuthorizationService(services => + { + // This will ignore the policy options + services.AddSingleton(); + services.AddAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireAssertion(context => true)); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed.Succeeded); + } + + public class DynamicPolicyProvider : IAuthorizationPolicyProvider + { + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + + public Task GetPolicyAsync(string policyName) + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireClaim(policyName).Build()); + } + } + + [Fact] + public async Task CanUseDynamicPolicyProvider() + { + var authorizationService = BuildAuthorizationService(services => + { + // This will ignore the policy options + services.AddSingleton(); + services.AddAuthorization(options => { }); + }); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim("1", "1")); + id.AddClaim(new Claim("2", "2")); + var user = new ClaimsPrincipal(id); + + // Act + // Assert + Assert.False((await authorizationService.AuthorizeAsync(user, "0")).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, "1")).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, "2")).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, "3")).Succeeded); + } + + public class SuccessEvaluator : IAuthorizationEvaluator + { + public AuthorizationResult Evaluate(AuthorizationHandlerContext context) => AuthorizationResult.Success(); + } + + [Fact] + public async Task CanUseCustomEvaluatorThatOverridesRequirement() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Fail", p => p.RequireAssertion(c => false))); + }); + var result = await authorizationService.AuthorizeAsync(null, "Fail"); + Assert.True(result.Succeeded); + } + + + public class BadContextMaker : IAuthorizationHandlerContextFactory + { + public AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource) + { + return new BadContext(); + } + } + + public class BadContext : AuthorizationHandlerContext + { + public BadContext() : base(new List(), null, null) { } + + public override bool HasFailed + { + get + { + return true; + } + } + + public override bool HasSucceeded + { + get + { + return false; + } + } + } + + [Fact] + public async Task CanUseCustomContextThatAlwaysFails() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true))); + }); + Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded); + } + + public class SadHandlerProvider : IAuthorizationHandlerProvider + { + public Task> GetHandlersAsync(AuthorizationHandlerContext context) + { + return Task.FromResult>(new IAuthorizationHandler[1] { new FailHandler() }); + } + } + + [Fact] + public async Task CanUseCustomHandlerProvider() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true))); + }); + Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded); + } + + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj new file mode 100644 index 0000000000..d4379c3aab --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj @@ -0,0 +1,18 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs new file mode 100644 index 0000000000..2384e6db5f --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Policy.Test +{ + public class PolicyEvaluatorTests + { + [Fact] + public async Task AuthenticateFailsIfNoPrincipalReturned() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddSingleton(); + context.RequestServices = services.BuildServiceProvider(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthenticateAsync(policy, context); + + // Assert + Assert.False(result.Succeeded); + } + + [Fact] + public async Task AuthenticateMergeSchemes() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddSingleton(); + context.RequestServices = services.BuildServiceProvider(); + var policy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("A","B","C").RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthenticateAsync(policy, context); + + // Assert + Assert.True(result.Succeeded); + Assert.Equal(3, result.Principal.Identities.Count()); + } + + + [Fact] + public async Task AuthorizeSucceedsEvenIfAuthenticationFails() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null); + + // Assert + Assert.True(result.Succeeded); + Assert.False(result.Challenged); + Assert.False(result.Forbidden); + } + + [Fact] + public async Task AuthorizeSucceedsOnlyIfResourceSpecified() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(c => c.Resource != null).Build(); + var success = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "whatever")); + + // Act + var result = await evaluator.AuthorizeAsync(policy, success, context, resource: null); + var result2 = await evaluator.AuthorizeAsync(policy, success, context, resource: new object()); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result2.Succeeded); + } + + [Fact] + public async Task AuthorizeChallengesIfAuthenticationFails() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.Challenged); + Assert.False(result.Forbidden); + } + + [Fact] + public async Task AuthorizeForbidsIfAuthenticationSuceeds() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "scheme")), context, resource: null); + + // Assert + Assert.False(result.Succeeded); + Assert.False(result.Challenged); + Assert.True(result.Forbidden); + } + + private IPolicyEvaluator BuildEvaluator(Action setupServices = null) + { + var services = new ServiceCollection() + .AddAuthorization() + .AddAuthorizationPolicyEvaluator() + .AddLogging() + .AddOptions(); + setupServices?.Invoke(services); + return services.BuildServiceProvider().GetRequiredService(); + } + + public class HappyAuthorization : IAuthorizationService + { + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Success()); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => Task.FromResult(AuthorizationResult.Success()); + } + + public class SadAuthorization : IAuthorizationService + { + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Failed()); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => Task.FromResult(AuthorizationResult.Failed()); + } + + public class SadAuthentication : IAuthenticationService + { + public Task AuthenticateAsync(HttpContext context, string scheme) + { + return Task.FromResult(AuthenticateResult.Fail("Sad.")); + } + + public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + } + + public class EchoAuthentication : IAuthenticationService + { + public Task AuthenticateAsync(HttpContext context, string scheme) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(scheme)), scheme))); + } + + public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + } + + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs new file mode 100644 index 0000000000..e645745b35 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Internal +{ + public class CookieChunkingTests + { + [Fact] + public void AppendLargeCookie_Appended() + { + HttpContext context = new DefaultHttpContext(); + + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + var values = context.Response.Headers["Set-Cookie"]; + Assert.Single(values); + Assert.Equal("TestCookie=" + testString + "; path=/; samesite=lax", values[0]); + } + + [Fact] + public void AppendLargeCookieWithLimit_Chunked() + { + HttpContext context = new DefaultHttpContext(); + + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + new ChunkingCookieManager() { ChunkSize = 44 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + var values = context.Response.Headers["Set-Cookie"]; + Assert.Equal(9, values.Count); + Assert.Equal(new[] + { + "TestCookie=chunks-8; path=/; samesite=lax", + "TestCookieC1=abcdefgh; path=/; samesite=lax", + "TestCookieC2=ijklmnop; path=/; samesite=lax", + "TestCookieC3=qrstuvwx; path=/; samesite=lax", + "TestCookieC4=yz012345; path=/; samesite=lax", + "TestCookieC5=6789ABCD; path=/; samesite=lax", + "TestCookieC6=EFGHIJKL; path=/; samesite=lax", + "TestCookieC7=MNOPQRST; path=/; samesite=lax", + "TestCookieC8=UVWXYZ; path=/; samesite=lax", + }, values); + } + + [Fact] + public void GetLargeChunkedCookie_Reassembled() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers["Cookie"] = new[] + { + "TestCookie=chunks-7", + "TestCookieC1=abcdefghi", + "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ" + }; + + string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie"); + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + Assert.Equal(testString, result); + } + + [Fact] + public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers["Cookie"] = new[] + { + "TestCookie=chunks-7", + "TestCookieC1=abcdefghi", + // Missing chunk "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ" + }; + + Assert.Throws(() => new ChunkingCookieManager() { ThrowForPartialCookies = true } + .GetRequestCookie(context, "TestCookie")); + } + + [Fact] + public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassembled() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers["Cookie"] = new[] + { + "TestCookie=chunks-7", + "TestCookieC1=abcdefghi", + // Missing chunk "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ" + }; + + string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie"); + string testString = "chunks-7"; + Assert.Equal(testString, result); + } + + [Fact] + public void DeleteChunkedCookieWithOptions_AllDeleted() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.Append("Cookie", "TestCookie=chunks-7"); + + new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" }); + var cookies = context.Response.Headers["Set-Cookie"]; + Assert.Equal(8, cookies.Count); + Assert.Equal(new[] + { + "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + }, cookies); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj new file mode 100644 index 0000000000..20cd400ce7 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj @@ -0,0 +1,15 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs new file mode 100644 index 0000000000..fffb8cc883 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs @@ -0,0 +1,660 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.CookiePolicy.Test +{ + public class CookieConsentTests + { + [Fact] + public async Task ConsentChecksOffByDefault() + { + var httpContext = await RunTestAsync(options => { }, requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.False(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task ConsentEnabledForTemplateScenario() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NonEssentialCookiesWithOptionsExcluded() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false }); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnAppendCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.False(context.HasConsent); + Assert.False(context.IssueCookie); + context.IssueCookie = true; + }; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false }); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NeedsConsentDoesNotPreventEssentialCookies() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true }); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task EssentialCookiesCanBeExcludedByOnAppendCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnAppendCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.True(context.HasConsent); + Assert.True(context.IssueCookie); + context.IssueCookie = false; + }; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true }); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task HasConsentReadsRequestCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task HasConsentIgnoresInvalidRequestCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=IAmATeapot"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task GrantConsentSetsCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(2, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("yes", consentCookie.Value); + Assert.True(consentCookie.Expires.HasValue); + Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364)); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + var testCookie = cookies[1]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("Value", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.Null(testCookie.Expires); + } + + [Fact] + public async Task GrantConsentAppliesPolicyToConsentCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnAppendCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + Assert.Equal("yes", context.CookieValue); + Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite); + context.CookieName += "1"; + context.CookieValue += "1"; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("yes1", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + async context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + await context.Response.WriteAsync("Started."); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + Assert.Throws(() => context.Response.Cookies.Append("Test", "Value")); + + await context.Response.WriteAsync("Granted."); + }); + + var reader = new StreamReader(httpContext.Response.Body); + Assert.Equal("Started.Granted.", await reader.ReadToEndAsync()); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task WithdrawConsentWhenNotHasConsentNoOps() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task WithdrawConsentDeletesCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value1"); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value2"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(2, cookies.Count); + var testCookie = cookies[0]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("Value1", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.Null(testCookie.Expires); + var consentCookie = cookies[1]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task WithdrawConsentAppliesPolicyToDeleteCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnDeleteCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + context.CookieName += "1"; + }; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + async context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value1"); + + await context.Response.WriteAsync("Started."); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + // Doesn't throw the normal InvalidOperationException because the cookie is never written + context.Response.Cookies.Append("Test", "Value2"); + + await context.Response.WriteAsync("Withdrawn."); + }); + + var reader = new StreamReader(httpContext.Response.Body); + Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync()); + Assert.Equal("Test=Value1; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task DeleteCookieDoesNotRequireConsent() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Delete("Test"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var testCookie = cookies[0]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.NotNull(testCookie.Expires); + } + + [Fact] + public async Task OnDeleteCookieCanSuppressCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnDeleteCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.False(context.HasConsent); + Assert.True(context.IssueCookie); + context.IssueCookie = false; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Delete("Test"); + return Task.CompletedTask; + }); + + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task CreateConsentCookieMatchesGrantConsentCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + var cookie = feature.CreateConsentCookie(); + context.Response.Headers["ManualCookie"] = cookie; + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("yes", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]); + Assert.Equal(1, cookies.Count); + var manualCookie = cookies[0]; + Assert.Equal(consentCookie.Name, manualCookie.Name); + Assert.Equal(consentCookie.Value, manualCookie.Value); + Assert.Equal(consentCookie.SameSite, manualCookie.SameSite); + Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second. + } + + [Fact] + public async Task CreateConsentCookieAppliesPolicy() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnAppendCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + Assert.Equal("yes", context.CookieValue); + Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite); + context.CookieName += "1"; + context.CookieValue += "1"; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + var cookie = feature.CreateConsentCookie(); + context.Response.Headers["ManualCookie"] = cookie; + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("yes1", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]); + Assert.Equal(1, cookies.Count); + var manualCookie = cookies[0]; + Assert.Equal(consentCookie.Name, manualCookie.Name); + Assert.Equal(consentCookie.Value, manualCookie.Value); + Assert.Equal(consentCookie.SameSite, manualCookie.SameSite); + Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second. + } + + private Task RunTestAsync(Action configureOptions, Action configureRequest, RequestDelegate handleRequest) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.Configure(configureOptions); + }) + .Configure(app => + { + app.UseCookiePolicy(); + app.Run(handleRequest); + }); + var server = new TestServer(builder); + return server.SendAsync(configureRequest); + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs new file mode 100644 index 0000000000..a2592e5575 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs @@ -0,0 +1,471 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.CookiePolicy.Test +{ + public class CookiePolicyTests + { + private RequestDelegate SecureCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }; + private RequestDelegate HttpCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { HttpOnly = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true }); + return Task.FromResult(0); + }; + private RequestDelegate SameSiteCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { SameSite = Http.SameSiteMode.None }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax }); + context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict }); + return Task.FromResult(0); + }; + + [Fact] + public async Task SecureAlwaysSetsSecure() + { + await RunTest("/secureAlways", + new CookiePolicyOptions + { + Secure = CookieSecurePolicy.Always + }, + SecureCookieAppends, + new RequestTest("http://example.com/secureAlways", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SecureNoneLeavesSecureUnchanged() + { + await RunTest("/secureNone", + new CookiePolicyOptions + { + Secure = CookieSecurePolicy.None + }, + SecureCookieAppends, + new RequestTest("http://example.com/secureNone", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SecureSameUsesRequest() + { + await RunTest("/secureSame", + new CookiePolicyOptions + { + Secure = CookieSecurePolicy.SameAsRequest + }, + SecureCookieAppends, + new RequestTest("http://example.com/secureSame", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + }), + new RequestTest("https://example.com/secureSame", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task HttpOnlyAlwaysSetsItAlways() + { + await RunTest("/httpOnlyAlways", + new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.Always + }, + HttpCookieAppends, + new RequestTest("http://example.com/httpOnlyAlways", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax; httponly", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax; httponly", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax; httponly", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task HttpOnlyNoneLeavesItAlone() + { + await RunTest("/httpOnlyNone", + new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.None + }, + HttpCookieAppends, + new RequestTest("http://example.com/httpOnlyNone", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SameSiteStrictSetsItAlways() + { + await RunTest("/sameSiteStrict", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.Strict + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteStrict", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); + })); + } + + [Fact] + public async Task SameSiteLaxSetsItAlways() + { + await RunTest("/sameSiteLax", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.Lax + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteLax", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); + })); + } + + [Fact] + public async Task SameSiteNoneLeavesItAlone() + { + await RunTest("/sameSiteNone", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.None + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteNone", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); + })); + } + + [Fact] + public async Task CookiePolicyCanHijackAppend() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + OnAppendCookie = ctx => ctx.CookieName = ctx.CookieValue = "Hao" + }); + app.Run(context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("Hao=Hao; path=/; secure; samesite=lax", transaction.SetCookie[3]); + } + + [Fact] + public async Task CookiePolicyCanHijackDelete() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + OnDeleteCookie = ctx => ctx.CookieName = "A" + }); + app.Run(context => + { + context.Response.Cookies.Delete("A"); + context.Response.Cookies.Delete("B", new CookieOptions { Secure = false }); + context.Response.Cookies.Delete("C", new CookieOptions()); + context.Response.Cookies.Delete("D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(1, transaction.SetCookie.Count); + Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax", transaction.SetCookie[0]); + } + + [Fact] + public async Task CookiePolicyCallsCookieFeature() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(next => context => + { + context.Features.Set(new TestCookieFeature()); + return next(context); + }); + app.UseCookiePolicy(new CookiePolicyOptions + { + OnDeleteCookie = ctx => ctx.CookieName = "A" + }); + app.Run(context => + { + Assert.Throws(() => context.Response.Cookies.Delete("A")); + Assert.Throws(() => context.Response.Cookies.Delete("A", new CookieOptions())); + Assert.Throws(() => context.Response.Cookies.Append("A", "A")); + Assert.Throws(() => context.Response.Cookies.Append("A", "A", new CookieOptions())); + return context.Response.WriteAsync("Done"); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + Assert.Equal("Done", transaction.ResponseText); + } + + [Fact] + public async Task CookiePolicyAppliesToCookieAuth() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + o.Cookie.HttpOnly = false; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + }); + }) + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always, + }); + app.UseAuthentication(); + app.Run(context => + { + return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("TestUser", "Cookies")))); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(1, transaction.SetCookie.Count); + var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]); + Assert.Equal("TestCookie", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + } + + [Fact] + public async Task CookiePolicyAppliesToCookieAuthChunks() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + o.Cookie.HttpOnly = false; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + }); + }) + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always, + }); + app.UseAuthentication(); + app.Run(context => + { + return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity(new string('c', 1024 * 5), "Cookies")))); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(3, transaction.SetCookie.Count); + + var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]); + Assert.Equal("TestCookie", cookie.Name); + Assert.Equal("chunks-2", cookie.Value); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + + cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[1]); + Assert.Equal("TestCookieC1", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + + cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[2]); + Assert.Equal("TestCookieC2", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + } + + private class TestCookieFeature : IResponseCookiesFeature + { + public IResponseCookies Cookies { get; } = new BadCookies(); + + private class BadCookies : IResponseCookies + { + public void Append(string key, string value) + { + throw new NotImplementedException(); + } + + public void Append(string key, string value, CookieOptions options) + { + throw new NotImplementedException(); + } + + public void Delete(string key) + { + throw new NotImplementedException(); + } + + public void Delete(string key, CookieOptions options) + { + throw new NotImplementedException(); + } + } + } + + private class RequestTest + { + public RequestTest(string testUri, Action verify) + { + TestUri = testUri; + Verification = verify; + } + + public async Task Execute(TestServer server) + { + var transaction = await server.SendAsync(TestUri); + Verification(transaction); + } + + public string TestUri { get; set; } + public Action Verification { get; set; } + } + + private async Task RunTest( + string path, + CookiePolicyOptions cookiePolicy, + RequestDelegate configureSetup, + params RequestTest[] tests) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Map(path, map => + { + map.UseCookiePolicy(cookiePolicy); + map.Run(configureSetup); + }); + }); + var server = new TestServer(builder); + foreach (var test in tests) + { + await test.Execute(server); + } + } + } +} \ No newline at end of file diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj new file mode 100644 index 0000000000..d7a42f3efb --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj @@ -0,0 +1,17 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs new file mode 100644 index 0000000000..9456094d41 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + // REVIEW: Should find a shared home for these potentially (Copied from Auth tests) + public static class TestExtensions + { + public const string CookieAuthenticationScheme = "External"; + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (principal != null) + { + foreach (var identity in principal.Identities) + { + xml.Add(identity.Claims.Select(claim => + new XElement("claim", new XAttribute("type", claim.Type), + new XAttribute("value", claim.Value), + new XAttribute("issuer", claim.Issuer)))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + res.Body.Write(xmlBytes, 0, xmlBytes.Length); + } + } +} diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs new file mode 100644 index 0000000000..040e0b3391 --- /dev/null +++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + // REVIEW: Should find a shared home for these potentially (Copied from Auth tests) + public class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme + "=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType, string issuer = null) + { + var claim = ResponseElement.Elements("claim") + .SingleOrDefault(elt => elt.Attribute("type").Value == claimType && + (issuer == null || elt.Attribute("issuer").Value == issuer)); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } +} diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs b/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs new file mode 100644 index 0000000000..e2e4fd7d07 --- /dev/null +++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs @@ -0,0 +1,332 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Testing; +using Owin; +using Xunit; + +namespace Microsoft.Owin.Security.Interop +{ + public class CookiesInteropTests + { + [Fact] + public async Task AspNetCoreWithInteropCookieContainsIdentity() + { + var identity = new ClaimsIdentity("Cookies"); + identity.AddClaim(new Claim(ClaimTypes.Name, "Alice")); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var interopServer = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + }); + + app.Run(context => + { + context.Authentication.SignIn(identity); + return Task.FromResult(0); + }); + }); + + var transaction = await SendAsync(interopServer, "http://example.com"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + var result = await context.AuthenticateAsync("Cookies"); + await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name); + }); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login"); + foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie)) + { + request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value); + } + var response = await newServer.CreateClient().SendAsync(request); + + Assert.Equal("Alice", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task AspNetCoreWithLargeInteropCookieContainsIdentity() + { + var identity = new ClaimsIdentity("Cookies"); + identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5))); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var interopServer = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + CookieManager = new ChunkingCookieManager(), + }); + + app.Run(context => + { + context.Authentication.SignIn(identity); + return Task.FromResult(0); + }); + }); + + var transaction = await SendAsync(interopServer, "http://example.com"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + var result = await context.AuthenticateAsync("Cookies"); + await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name); + }); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login"); + foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie)) + { + request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value); + } + var response = await newServer.CreateClient().SendAsync(request); + + Assert.Equal(1024 * 5, (await response.Content.ReadAsStringAsync()).Length); + } + + [Fact] + public async Task InteropWithNewCookieContainsIdentity() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim(ClaimTypes.Name, "Alice")); + user.AddIdentity(identity); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookies", user)); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var cookies = await SendAndGetCookies(newServer, "http://example.com/login"); + + var server = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + }); + + app.Run(async context => + { + var result = await context.Authentication.AuthenticateAsync("Cookies"); + Describe(context.Response, result); + }); + }); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies); + + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task InteropWithLargeNewCookieContainsIdentity() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5))); + user.AddIdentity(identity); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookies", user)); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var cookies = await SendAndGetCookies(newServer, "http://example.com/login"); + + var server = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + CookieManager = new ChunkingCookieManager(), + }); + + app.Run(async context => + { + var result = await context.Authentication.AuthenticateAsync("Cookies"); + Describe(context.Response, result); + }); + }); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies); + + Assert.Equal(1024 * 5, FindClaimValue(transaction2, ClaimTypes.Name).Length); + } + + private static async Task> SendAndGetCookies(AspNetCore.TestHost.TestServer server, string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await server.CreateClient().SendAsync(request); + if (response.Headers.Contains("Set-Cookie")) + { + IList cookieHeaders = new List(); + foreach (var cookie in SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList())) + { + cookieHeaders.Add(cookie.Name + "=" + cookie.Value); + } + return cookieHeaders; + } + return null; + } + + private static string FindClaimValue(Transaction transaction, string claimType) + { + XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + private static void Describe(IOwinResponse res, AuthenticateResult result) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (result != null && result.Identity != null) + { + xml.Add(result.Identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + if (result != null && result.Properties != null) + { + xml.Add(result.Properties.Dictionary.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + + private static async Task SendAsync(TestServer server, string uri, IList cookieHeaders = null, bool ajaxRequest = false) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (cookieHeaders != null) + { + request.Headers.Add("Cookie", cookieHeaders); + } + if (ajaxRequest) + { + request.Headers.Add("X-Requested-With", "XMLHttpRequest"); + } + var transaction = new Transaction + { + Request = request, + Response = await server.HttpClient.SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + if (transaction.SetCookie != null && transaction.SetCookie.Any()) + { + transaction.CookieNameValue = transaction.SetCookie.First().Split(new[] { ';' }, 2).First(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + public string CookieNameValue { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + } + + } +} + diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj b/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj new file mode 100644 index 0000000000..f369f1f01a --- /dev/null +++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj @@ -0,0 +1,18 @@ + + + + net461 + + + + + + + + + + + + + + diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs b/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs new file mode 100644 index 0000000000..769adc015b --- /dev/null +++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Xunit; + +namespace Microsoft.Owin.Security.Interop.Test +{ + public class TicketInteropTests + { + [Fact] + public void NewSerializerCanReadInteropTicket() + { + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim("Test", "Value")); + + var expires = DateTime.Today; + var issued = new DateTime(1979, 11, 11); + var properties = new Owin.Security.AuthenticationProperties(); + properties.IsPersistent = true; + properties.RedirectUri = "/redirect"; + properties.Dictionary["key"] = "value"; + properties.ExpiresUtc = expires; + properties.IssuedUtc = issued; + + var interopTicket = new Owin.Security.AuthenticationTicket(identity, properties); + var interopSerializer = new AspNetTicketSerializer(); + + var bytes = interopSerializer.Serialize(interopTicket); + + var newSerializer = new TicketSerializer(); + var newTicket = newSerializer.Deserialize(bytes); + + Assert.NotNull(newTicket); + Assert.Single(newTicket.Principal.Identities); + var newIdentity = newTicket.Principal.Identity as ClaimsIdentity; + Assert.NotNull(newIdentity); + Assert.Equal("scheme", newIdentity.AuthenticationType); + Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value")); + Assert.NotNull(newTicket.Properties); + Assert.True(newTicket.Properties.IsPersistent); + Assert.Equal("/redirect", newTicket.Properties.RedirectUri); + Assert.Equal("value", newTicket.Properties.Items["key"]); + Assert.Equal(expires, newTicket.Properties.ExpiresUtc); + Assert.Equal(issued, newTicket.Properties.IssuedUtc); + } + + [Fact] + public void InteropSerializerCanReadNewTicket() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim("Test", "Value")); + user.AddIdentity(identity); + + var expires = DateTime.Today; + var issued = new DateTime(1979, 11, 11); + var properties = new AspNetCore.Authentication.AuthenticationProperties(); + properties.IsPersistent = true; + properties.RedirectUri = "/redirect"; + properties.Items["key"] = "value"; + properties.ExpiresUtc = expires; + properties.IssuedUtc = issued; + + var newTicket = new AspNetCore.Authentication.AuthenticationTicket(user, properties, "scheme"); + var newSerializer = new TicketSerializer(); + + var bytes = newSerializer.Serialize(newTicket); + + var interopSerializer = new AspNetTicketSerializer(); + var interopTicket = interopSerializer.Deserialize(bytes); + + Assert.NotNull(interopTicket); + var newIdentity = interopTicket.Identity; + Assert.NotNull(newIdentity); + Assert.Equal("scheme", newIdentity.AuthenticationType); + Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value")); + Assert.NotNull(interopTicket.Properties); + Assert.True(interopTicket.Properties.IsPersistent); + Assert.Equal("/redirect", interopTicket.Properties.RedirectUri); + Assert.Equal("value", interopTicket.Properties.Dictionary["key"]); + Assert.Equal(expires, interopTicket.Properties.ExpiresUtc); + Assert.Equal(issued, interopTicket.Properties.IssuedUtc); + } + } +} + + diff --git a/src/Security/version.props b/src/Security/version.props new file mode 100644 index 0000000000..478dfd16ed --- /dev/null +++ b/src/Security/version.props @@ -0,0 +1,12 @@ + + + 2.1.2 + rtm + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + + diff --git a/src/ServerTests/Directory.Build.props b/src/ServerTests/Directory.Build.props index fc93574bae..cefef093b1 100644 --- a/src/ServerTests/Directory.Build.props +++ b/src/ServerTests/Directory.Build.props @@ -9,7 +9,7 @@ Microsoft ASP.NET Core - https://github.com/aspnet/servertests + https://github.com/aspnet/AspNetCore git $(MSBuildThisFileDirectory) true diff --git a/src/Session/Directory.Build.props b/src/Session/Directory.Build.props index 48f12702ba..cd0187875b 100644 --- a/src/Session/Directory.Build.props +++ b/src/Session/Directory.Build.props @@ -9,7 +9,7 @@ Microsoft ASP.NET Core - https://github.com/aspnet/Session + https://github.com/aspnet/AspNetCore git $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk diff --git a/src/StaticFiles/Directory.Build.props b/src/StaticFiles/Directory.Build.props index c46c5e323b..cd0187875b 100644 --- a/src/StaticFiles/Directory.Build.props +++ b/src/StaticFiles/Directory.Build.props @@ -9,7 +9,7 @@ Microsoft ASP.NET Core - https://github.com/aspnet/StaticFiles + https://github.com/aspnet/AspNetCore git $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk diff --git a/src/Templating/Directory.Build.props b/src/Templating/Directory.Build.props index 9887edd5d5..d7eb964138 100644 --- a/src/Templating/Directory.Build.props +++ b/src/Templating/Directory.Build.props @@ -10,7 +10,7 @@ Microsoft ASP.NET Core $(MSBuildThisFileDirectory) - https://github.com/aspnet/Templating + https://github.com/aspnet/AspNetCore git true