From 3a8ea672eae590785a78d74777abb76bb9664130 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 25 Jun 2015 17:19:27 -0700 Subject: [PATCH] AuthN and AuthZ API changes (Async, Challenge) --- samples/CookieSample/Startup.cs | 2 +- samples/CookieSessionSample/Startup.cs | 2 +- samples/OpenIdConnectSample/Startup.cs | 2 +- samples/SocialSample/Startup.cs | 6 +- .../CookieAuthenticationHandler.cs | 393 ++++++++++-------- .../OAuthAuthenticationHandler.cs | 94 ++--- .../OAuthBearerAuthenticationHandler.cs | 39 +- .../OpenIdConnectAuthenticationHandler.cs | 61 +-- .../TwitterAuthenticationHandler.cs | 68 +-- .../AuthenticationHandler.cs | 381 ++++++----------- .../AuthenticationMiddleware.cs | 1 - ...thenticationServiceCollectionExtensions.cs | 8 +- ...aimsTransformationAuthenticationHandler.cs | 55 +-- .../ClaimsTransformationMiddleware.cs | 6 +- .../ClaimsTransformationOptions.cs | 33 +- .../AuthorizationContext.cs | 8 +- .../AuthorizationHandler.cs | 29 +- .../AuthorizationOptions.cs | 3 +- .../AuthorizationPolicy.cs | 5 +- .../AuthorizationPolicyBuilder.cs | 8 +- .../AuthorizeAttribute.cs | 2 +- .../ClaimsAuthorizationRequirement.cs | 2 +- .../DefaultAuthorizationService.cs | 1 + .../DelegateRequirement.cs | 22 + .../DenyAnonymousAuthorizationRequirement.cs | 2 +- .../IAuthorizeData.cs | 14 + .../NameAuthorizationRequirement.cs | 2 +- .../RolesAuthorizationRequirement.cs | 2 +- .../AuthenticationHandlerFacts.cs | 26 +- .../Cookies/CookieMiddlewareTests.cs | 161 ++++--- .../Facebook/FacebookMiddlewareTests.cs | 6 +- .../Google/GoogleMiddlewareTests.cs | 109 ++--- .../MicrosoftAccountMiddlewareTests.cs | 51 ++- .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 33 +- .../OpenIdConnectHandlerTests.cs | 16 +- .../OpenIdConnectMiddlewareTests.cs | 12 +- .../Twitter/TwitterMiddlewareTests.cs | 79 +++- .../DefaultAuthorizationServiceTests.cs | 32 +- 38 files changed, 933 insertions(+), 843 deletions(-) create mode 100644 src/Microsoft.AspNet.Authorization/DelegateRequirement.cs create mode 100644 src/Microsoft.AspNet.Authorization/IAuthorizeData.cs diff --git a/samples/CookieSample/Startup.cs b/samples/CookieSample/Startup.cs index 6547450e51..04694b7cfd 100644 --- a/samples/CookieSample/Startup.cs +++ b/samples/CookieSample/Startup.cs @@ -28,7 +28,7 @@ namespace CookieSample if (string.IsNullOrEmpty(context.User.Identity.Name)) { var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") })); - context.Authentication.SignIn(CookieAuthenticationDefaults.AuthenticationScheme, user); + await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); return; diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs index f0b1b5219b..5858c2c39f 100644 --- a/samples/CookieSessionSample/Startup.cs +++ b/samples/CookieSessionSample/Startup.cs @@ -36,7 +36,7 @@ namespace CookieSessionSample { claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe")); } - context.Authentication.SignIn(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(claims))); + await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(claims))); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); return; diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 2077d802bb..b07d87da50 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -40,7 +40,7 @@ namespace OpenIdConnectSample { if (string.IsNullOrEmpty(context.User.Identity.Name)) { - context.Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); + await context.Authentication.ChallengeAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index eb6c74563b..62d9c83803 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -187,7 +187,7 @@ namespace CookieSample { // 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 (/). - context.Authentication.Challenge(authType, new AuthenticationProperties() { RedirectUri = "/" }); + await context.Authentication.ChallengeAsync(authType, new AuthenticationProperties() { RedirectUri = "/" }); return; } @@ -207,7 +207,7 @@ namespace CookieSample { signoutApp.Run(async context => { - context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationScheme); + await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(""); await context.Response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); @@ -222,7 +222,7 @@ namespace CookieSample if (string.IsNullOrEmpty(context.User.Identity.Name)) { // The cookie middleware will intercept this 401 and redirect to /login - context.Authentication.Challenge(); + await context.Authentication.ChallengeAsync(); return; } await next(); diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index b790291ea9..66fe679bd1 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -8,6 +8,8 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Authentication.Cookies @@ -26,12 +28,7 @@ namespace Microsoft.AspNet.Authentication.Cookies private DateTimeOffset _renewExpiresUtc; private string _sessionKey; - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); - } - - protected override async Task AuthenticateCoreAsync() + public override async Task AuthenticateAsync() { AuthenticationTicket ticket = null; try @@ -99,7 +96,6 @@ namespace Microsoft.AspNet.Authentication.Cookies await Options.Notifications.ValidatePrincipal(context); - AuthenticateCalled = true; return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); } catch (Exception exception) @@ -115,19 +111,72 @@ namespace Microsoft.AspNet.Authentication.Cookies } } - protected override void ApplyResponseGrant() + private CookieOptions BuildCookieOptions() { - ApplyResponseGrantAsync().GetAwaiter().GetResult(); + var cookieOptions = new CookieOptions + { + Domain = Options.CookieDomain, + HttpOnly = Options.CookieHttpOnly, + Path = Options.CookiePath ?? (RequestPathBase.HasValue ? RequestPathBase.ToString() : "/"), + }; + if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = Request.IsHttps; + } + else + { + cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + } + return cookieOptions; } - protected override async Task ApplyResponseGrantAsync() + private async Task ApplyCookie(AuthenticationTicket model) { - var signin = SignInContext; - var shouldSignin = signin != null; - var signout = SignOutContext; - var shouldSignout = signout != null; + var cookieOptions = BuildCookieOptions(); - if (!(shouldSignin || shouldSignout || _shouldRenew)) + model.Properties.IssuedUtc = _renewIssuedUtc; + model.Properties.ExpiresUtc = _renewExpiresUtc; + + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RenewAsync(_sessionKey, model); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.AuthenticationScheme)); + model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + } + + var cookieValue = Options.TicketDataFormat.Protect(model); + + if (model.Properties.IsPersistent) + { + cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; + } + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + cookieOptions); + + Response.Headers.Set( + HeaderNameCacheControl, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNamePragma, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNameExpires, + HeaderValueMinusOne); + } + + protected override async Task FinishResponseAsync() + { + // Only renew if requested, and neither sign in or sign out was called + if (!_shouldRenew || SignInAccepted || SignOutAccepted) { return; } @@ -135,133 +184,89 @@ namespace Microsoft.AspNet.Authentication.Cookies var model = await AuthenticateAsync(); try { - var cookieOptions = new CookieOptions + await ApplyCookie(model); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) { - Domain = Options.CookieDomain, - HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? (RequestPathBase.HasValue ? RequestPathBase.ToString() : "/"), - }; - if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + throw; + } + } + } + + protected override async Task HandleSignInAsync(SignInContext signin) + { + var model = await AuthenticateAsync(); + try + { + var cookieOptions = BuildCookieOptions(); + + var signInContext = new CookieResponseSignInContext( + Context, + Options, + Options.AuthenticationScheme, + signin.Principal, + new AuthenticationProperties(signin.Properties), + cookieOptions); + + DateTimeOffset issuedUtc; + if (signInContext.Properties.IssuedUtc.HasValue) { - cookieOptions.Secure = Request.IsHttps; + issuedUtc = signInContext.Properties.IssuedUtc.Value; } else { - cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + issuedUtc = Options.SystemClock.UtcNow; + signInContext.Properties.IssuedUtc = issuedUtc; } - if (shouldSignin) + if (!signInContext.Properties.ExpiresUtc.HasValue) { - var signInContext = new CookieResponseSignInContext( - Context, - Options, - Options.AuthenticationScheme, - signin.Principal, - new AuthenticationProperties(signin.Properties), - cookieOptions); - - DateTimeOffset issuedUtc; - if (signInContext.Properties.IssuedUtc.HasValue) - { - issuedUtc = signInContext.Properties.IssuedUtc.Value; - } - else - { - issuedUtc = Options.SystemClock.UtcNow; - signInContext.Properties.IssuedUtc = issuedUtc; - } - - if (!signInContext.Properties.ExpiresUtc.HasValue) - { - signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); - } - - Options.Notifications.ResponseSignIn(signInContext); - - if (signInContext.Properties.IsPersistent) - { - var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); - signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; - } - - model = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); - if (Options.SessionStore != null) - { - if (_sessionKey != null) - { - await Options.SessionStore.RemoveAsync(_sessionKey); - } - _sessionKey = await Options.SessionStore.StoreAsync(model); - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, - Options.ClaimsIssuer)); - model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); - } - var cookieValue = Options.TicketDataFormat.Protect(model); - - Options.CookieManager.AppendResponseCookie( - Context, - Options.CookieName, - cookieValue, - signInContext.CookieOptions); - - var signedInContext = new CookieResponseSignedInContext( - Context, - Options, - Options.AuthenticationScheme, - signInContext.Principal, - signInContext.Properties); - - Options.Notifications.ResponseSignedIn(signedInContext); + signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); } - else if (shouldSignout) + + Options.Notifications.ResponseSignIn(signInContext); + + if (signInContext.Properties.IsPersistent) { - if (Options.SessionStore != null && _sessionKey != null) + var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); + signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + } + + model = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); + if (Options.SessionStore != null) + { + if (_sessionKey != null) { await Options.SessionStore.RemoveAsync(_sessionKey); } - - var context = new CookieResponseSignOutContext( - Context, - Options, - cookieOptions); - - Options.Notifications.ResponseSignOut(context); - - Options.CookieManager.DeleteCookie( - Context, - Options.CookieName, - context.CookieOptions); + _sessionKey = await Options.SessionStore.StoreAsync(model); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.ClaimsIssuer)); + model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); } - else if (_shouldRenew) - { - model.Properties.IssuedUtc = _renewIssuedUtc; - model.Properties.ExpiresUtc = _renewExpiresUtc; + var cookieValue = Options.TicketDataFormat.Protect(model); - if (Options.SessionStore != null && _sessionKey != null) - { - await Options.SessionStore.RenewAsync(_sessionKey, model); - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, - Options.AuthenticationScheme)); - model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); - } + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + signInContext.CookieOptions); - var cookieValue = Options.TicketDataFormat.Protect(model); + var signedInContext = new CookieResponseSignedInContext( + Context, + Options, + Options.AuthenticationScheme, + signInContext.Principal, + signInContext.Properties); - if (model.Properties.IsPersistent) - { - cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; - } - - Options.CookieManager.AppendResponseCookie( - Context, - Options.CookieName, - cookieValue, - cookieOptions); - } + Options.Notifications.ResponseSignedIn(signedInContext); Response.Headers.Set( HeaderNameCacheControl, @@ -275,10 +280,9 @@ namespace Microsoft.AspNet.Authentication.Cookies HeaderNameExpires, HeaderValueMinusOne); - var shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - var shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; + var shouldLoginRedirect = Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) + if ((shouldLoginRedirect) && Response.StatusCode == 200) { var query = Request.Query; var redirectUri = query.Get(Options.ReturnUrlParameter); @@ -302,6 +306,69 @@ namespace Microsoft.AspNet.Authentication.Cookies } } + protected override async Task HandleSignOutAsync(SignOutContext signOutContext) + { + var model = await AuthenticateAsync(); + try + { + var cookieOptions = BuildCookieOptions(); + + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + + var context = new CookieResponseSignOutContext( + Context, + Options, + cookieOptions); + + Options.Notifications.ResponseSignOut(context); + + Options.CookieManager.DeleteCookie( + Context, + Options.CookieName, + context.CookieOptions); + + Response.Headers.Set( + HeaderNameCacheControl, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNamePragma, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNameExpires, + HeaderValueMinusOne); + + var shouldLogoutRedirect = Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; + + if (shouldLogoutRedirect && Response.StatusCode == 200) + { + var query = Request.Query; + var redirectUri = query.Get(Options.ReturnUrlParameter); + if (!string.IsNullOrWhiteSpace(redirectUri) + && IsHostRelative(redirectUri)) + { + var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + } + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + + } + private static bool IsHostRelative(string path) { if (string.IsNullOrEmpty(path)) @@ -315,60 +382,49 @@ namespace Microsoft.AspNet.Authentication.Cookies return path[0] == '/' && path[1] != '/' && path[1] != '\\'; } - protected override void ApplyResponseChallenge() + protected override Task HandleForbiddenAsync(ChallengeContext context) { - if (ShouldConvertChallengeToForbidden()) + // HandleForbidden by redirecting to AccessDeniedPath if set + if (Options.AccessDeniedPath.HasValue) { - // Handle 403 by redirecting to AccessDeniedPath if set - if (Options.AccessDeniedPath.HasValue) + try { - try - { - var accessDeniedUri = - Request.Scheme + - "://" + - Request.Host + - Request.PathBase + - Options.AccessDeniedPath; + var accessDeniedUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase + + Options.AccessDeniedPath; - var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - catch (Exception exception) + var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); - Options.Notifications.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } + throw; } } - else - { - Response.StatusCode = 403; - } - return; + return Task.FromResult(true); } - - if (Response.StatusCode != 401 || !Options.LoginPath.HasValue ) + else { - return; + return base.HandleForbiddenAsync(context); } + } - // Automatic middleware should redirect on 401 even if there wasn't an explicit challenge. - if (ChallengeContext == null && !Options.AutomaticAuthentication) + protected override Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) + { + if (!Options.LoginPath.HasValue) { - return; - } - - var redirectUri = string.Empty; - if (ChallengeContext != null) - { - redirectUri = new AuthenticationProperties(ChallengeContext.Properties).RedirectUri; + return base.HandleUnauthorizedAsync(context); } + var redirectUri = new AuthenticationProperties(context.Properties).RedirectUri; try { if (string.IsNullOrWhiteSpace(redirectUri)) @@ -400,6 +456,7 @@ namespace Microsoft.AspNet.Authentication.Cookies throw; } } + return Task.FromResult(true); } } } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs index d9c2183443..c5a8b0fc7f 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs @@ -6,10 +6,11 @@ using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Newtonsoft.Json.Linq; @@ -37,7 +38,7 @@ namespace Microsoft.AspNet.Authentication.OAuth public async Task InvokeReturnPathAsync() { - AuthenticationTicket ticket = await AuthenticateAsync(); + var ticket = await AuthenticateAsync(); if (ticket == null) { Logger.LogWarning("Invalid return state, unable to redirect."); @@ -56,7 +57,7 @@ namespace Microsoft.AspNet.Authentication.OAuth if (context.SignInScheme != null && context.Principal != null) { - Context.Authentication.SignIn(context.SignInScheme, context.Principal, context.Properties); + await Context.Authentication.SignInAsync(context.SignInScheme, context.Principal, context.Properties); } if (!context.IsRequestCompleted && context.RedirectUri != null) @@ -73,17 +74,12 @@ namespace Microsoft.AspNet.Authentication.OAuth return context.IsRequestCompleted; } - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); - } - - protected override async Task AuthenticateCoreAsync() + public override async Task AuthenticateAsync() { AuthenticationProperties properties = null; try { - IReadableStringCollection query = Request.Query; + var query = Request.Query; // TODO: Is this a standard error returned by servers? var value = query.Get("error"); @@ -94,8 +90,8 @@ namespace Microsoft.AspNet.Authentication.OAuth return null; } - string code = query.Get("code"); - string state = query.Get("state"); + var code = query.Get("code"); + var state = query.Get("state"); properties = Options.StateDataFormat.Unprotect(state); if (properties == null) @@ -115,8 +111,8 @@ namespace Microsoft.AspNet.Authentication.OAuth return new AuthenticationTicket(properties, Options.AuthenticationScheme); } - string requestPrefix = Request.Scheme + "://" + Request.Host; - string redirectUri = requestPrefix + RequestPathBase + Options.CallbackPath; + var requestPrefix = Request.Scheme + "://" + Request.Host; + var redirectUri = requestPrefix + RequestPathBase + Options.CallbackPath; var tokens = await ExchangeCodeAsync(code, redirectUri); @@ -151,11 +147,11 @@ namespace Microsoft.AspNet.Authentication.OAuth var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = requestContent; - HttpResponseMessage response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); + var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); response.EnsureSuccessStatusCode(); - string oauthTokenResponse = await response.Content.ReadAsStringAsync(); + var oauthTokenResponse = await response.Content.ReadAsStringAsync(); - JObject oauth2Token = JObject.Parse(oauthTokenResponse); + var oauth2Token = JObject.Parse(oauthTokenResponse); return new TokenResponse(oauth2Token); } @@ -169,40 +165,13 @@ namespace Microsoft.AspNet.Authentication.OAuth return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); } - protected override void ApplyResponseChallenge() + protected override Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) { - if (ShouldConvertChallengeToForbidden()) - { - Response.StatusCode = 403; - return; - } + var baseUri = Request.Scheme + "://" + Request.Host + Request.PathBase; + var currentUri = baseUri + Request.Path + Request.QueryString; + var redirectUri = baseUri + Options.CallbackPath; - if (Response.StatusCode != 401) - { - return; - } - - // When Automatic should redirect on 401 even if there wasn't an explicit challenge. - if (ChallengeContext == null && !Options.AutomaticAuthentication) - { - return; - } - - string baseUri = Request.Scheme + "://" + Request.Host + Request.PathBase; - - string currentUri = baseUri + Request.Path + Request.QueryString; - - string redirectUri = baseUri + Options.CallbackPath; - - AuthenticationProperties properties; - if (ChallengeContext == null) - { - properties = new AuthenticationProperties(); - } - else - { - properties = new AuthenticationProperties(ChallengeContext.Properties); - } + var properties = new AuthenticationProperties(context.Properties); if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = currentUri; @@ -211,19 +180,35 @@ namespace Microsoft.AspNet.Authentication.OAuth // OAuth2 10.12 CSRF GenerateCorrelationId(properties); - string authorizationEndpoint = BuildChallengeUrl(properties, redirectUri); + var authorizationEndpoint = BuildChallengeUrl(properties, redirectUri); var redirectContext = new OAuthApplyRedirectContext( Context, Options, properties, authorizationEndpoint); Options.Notifications.ApplyRedirect(redirectContext); + return Task.FromResult(true); + } + + protected override Task HandleSignOutAsync(SignOutContext context) + { + throw new NotSupportedException(); + } + + protected override Task HandleSignInAsync(SignInContext context) + { + throw new NotSupportedException(); + } + + protected override Task HandleForbiddenAsync(ChallengeContext context) + { + throw new NotSupportedException(); } protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { - string scope = FormatScope(); + var scope = FormatScope(); - string state = Options.StateDataFormat.Protect(properties); + var state = Options.StateDataFormat.Protect(properties); var queryBuilder = new QueryBuilder() { @@ -241,10 +226,5 @@ namespace Microsoft.AspNet.Authentication.OAuth // OAuth2 3.3 space separated return string.Join(" ", Options.Scope); } - - protected override void ApplyResponseGrant() - { - // N/A - No SignIn or SignOut support. - } } } diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs index b088519509..6357a35af9 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -2,14 +2,13 @@ // 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; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.Notifications; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols; @@ -19,16 +18,11 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer { private OpenIdConnectConfiguration _configuration; - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); - } - /// /// 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 AuthenticateCoreAsync() + public override async Task AuthenticateAsync() { string token = null; try @@ -179,30 +173,21 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer } } - protected override void ApplyResponseChallenge() + protected override async Task HandleUnauthorizedAsync(ChallengeContext context) { - ApplyResponseChallengeAsync().GetAwaiter().GetResult(); - } - - protected override async Task ApplyResponseChallengeAsync() - { - if (ShouldConvertChallengeToForbidden()) - { - Response.StatusCode = 403; - return; - } - - if ((Response.StatusCode != 401) || (ChallengeContext == null && !Options.AutomaticAuthentication)) - { - return; - } - + Response.StatusCode = 401; await Options.Notifications.ApplyChallenge(new AuthenticationChallengeNotification(Context, Options)); + return false; } - protected override void ApplyResponseGrant() + protected override Task HandleSignOutAsync(SignOutContext context) { - // N/A + throw new NotSupportedException(); + } + + protected override Task HandleSignInAsync(SignInContext context) + { + throw new NotSupportedException(); } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 7e97628e14..b542c2ef81 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using Microsoft.AspNet.Authentication.Notifications; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols; @@ -38,18 +40,12 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } - protected override void ApplyResponseGrant() - { - ApplyResponseGrantAsync().GetAwaiter().GetResult(); - } - /// /// Handles Signout /// /// - protected override async Task ApplyResponseGrantAsync() + protected override async Task HandleSignOutAsync(SignOutContext signout) { - var signout = SignOutContext; if (signout != null) { if (_configuration == null && Options.ConfigurationManager != null) @@ -96,52 +92,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } - protected override void ApplyResponseChallenge() - { - ApplyResponseChallengeAsync().GetAwaiter().GetResult(); - } - /// /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. /// /// /// Uses log id's OIDCH-0026 - OIDCH-0050, next num: 37 - protected override async Task ApplyResponseChallengeAsync() + protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) { Logger.LogDebug(Resources.OIDCH_0026_ApplyResponseChallengeAsync, this.GetType()); - if (ShouldConvertChallengeToForbidden()) - { - Logger.LogDebug(Resources.OIDCH_0027_401_ConvertedTo_403); - Response.StatusCode = 403; - return; - } - - if (Response.StatusCode != 401) - { - Logger.LogDebug(Resources.OIDCH_0028_StatusCodeNot401, Response.StatusCode); - return; - } - - // When Automatic should redirect on 401 even if there wasn't an explicit challenge. - if (ChallengeContext == null && !Options.AutomaticAuthentication) - { - Logger.LogDebug(Resources.OIDCH_0029_ChallengeContextEqualsNull); - return; - } - // order for local RedirectUri // 1. challenge.Properties.RedirectUri // 2. CurrentUri if Options.DefaultToCurrentUriOnRedirect is true) - AuthenticationProperties properties; - if (ChallengeContext == null) - { - properties = new AuthenticationProperties(); - } - else - { - properties = new AuthenticationProperties(ChallengeContext.Properties); - } + AuthenticationProperties properties = new AuthenticationProperties(context.Properties); if (!string.IsNullOrWhiteSpace(properties.RedirectUri)) { @@ -209,12 +172,12 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (redirectToIdentityProviderNotification.HandledResponse) { Logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); - return; + return true; // REVIEW: Make sure this should stop all other handlers } else if (redirectToIdentityProviderNotification.Skipped) { Logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); - return; + return false; // REVIEW: Make sure this should not stop all other handlers } var redirectUri = redirectToIdentityProviderNotification.ProtocolMessage.CreateAuthenticationRequestUrl(); @@ -224,11 +187,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } Response.Redirect(redirectUri); - } - - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); + return true; } /// @@ -236,7 +195,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// An if successful. /// Uses log id's OIDCH-0000 - OIDCH-0025 - protected override async Task AuthenticateCoreAsync() + public override async Task AuthenticateAsync() { Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); @@ -632,7 +591,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { if (ticket.Principal != null) { - Request.HttpContext.Authentication.SignIn(Options.SignInScheme, ticket.Principal, ticket.Properties); + await Request.HttpContext.Authentication.SignInAsync(Options.SignInScheme, ticket.Principal, ticket.Properties); } // Redirect back to the original secured resource, if any. diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs index eecc1a0434..659b8b6aad 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs @@ -12,8 +12,10 @@ using System.Threading.Tasks; using Microsoft.AspNet.Authentication.Twitter.Messages; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Authentication.Twitter @@ -42,12 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter return false; } - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); - } - - protected override async Task AuthenticateCoreAsync() + public override async Task AuthenticateAsync() { AuthenticationProperties properties = null; try @@ -121,49 +118,18 @@ namespace Microsoft.AspNet.Authentication.Twitter return new AuthenticationTicket(properties, Options.AuthenticationScheme); } } - protected override void ApplyResponseChallenge() + protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) { - ApplyResponseChallengeAsync().GetAwaiter().GetResult(); - } - - protected override async Task ApplyResponseChallengeAsync() - { - if (ShouldConvertChallengeToForbidden()) - { - Response.StatusCode = 403; - return; - } - - if (Response.StatusCode != 401) - { - return; - } - - // When Automatic should redirect on 401 even if there wasn't an explicit challenge. - if (ChallengeContext == null && !Options.AutomaticAuthentication) - { - return; - } - var requestPrefix = Request.Scheme + "://" + Request.Host; var callBackUrl = requestPrefix + RequestPathBase + Options.CallbackPath; - AuthenticationProperties properties; - if (ChallengeContext == null) - { - properties = new AuthenticationProperties(); - } - else - { - properties = new AuthenticationProperties(ChallengeContext.Properties); - } + var properties = new AuthenticationProperties(context.Properties); if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = requestPrefix + Request.PathBase + Request.Path + Request.QueryString; } var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, callBackUrl, properties); - if (requestToken.CallbackConfirmed) { var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; @@ -180,11 +146,13 @@ namespace Microsoft.AspNet.Authentication.Twitter Context, Options, properties, twitterAuthenticationEndpoint); Options.Notifications.ApplyRedirect(redirectContext); + return true; } else { Logger.LogError("requestToken CallbackConfirmed!=true"); } + return false; // REVIEW: Make sure this should not stop other handlers } public async Task InvokeReturnPathAsync() @@ -208,7 +176,7 @@ namespace Microsoft.AspNet.Authentication.Twitter if (context.SignInScheme != null && context.Principal != null) { - Context.Authentication.SignIn(context.SignInScheme, context.Principal, context.Properties); + await Context.Authentication.SignInAsync(context.SignInScheme, context.Principal, context.Properties); } if (!context.IsRequestCompleted && context.RedirectUri != null) @@ -225,6 +193,21 @@ namespace Microsoft.AspNet.Authentication.Twitter return context.IsRequestCompleted; } + protected override Task HandleSignOutAsync(SignOutContext context) + { + throw new NotSupportedException(); + } + + protected override Task HandleSignInAsync(SignInContext context) + { + throw new NotSupportedException(); + } + + protected override Task HandleForbiddenAsync(ChallengeContext context) + { + throw new NotSupportedException(); + } + private async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) { Logger.LogVerbose("ObtainRequestToken"); @@ -380,10 +363,5 @@ namespace Microsoft.AspNet.Authentication.Twitter return Convert.ToBase64String(hash); } } - - protected override void ApplyResponseGrant() - { - // N/A - No SignIn or SignOut support. - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index fd6950ff5b..d9933f5f74 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -3,7 +3,6 @@ using System; using System.Security.Cryptography; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.DataHandler.Encoder; using Microsoft.AspNet.Http; @@ -22,19 +21,12 @@ namespace Microsoft.AspNet.Authentication { private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); - private Task _authenticate; - private bool _authenticateInitialized; - private object _authenticateSyncLock; - - private Task _applyResponse; - private bool _applyResponseInitialized; - private object _applyResponseSyncLock; - + private bool _finishCalled; private AuthenticationOptions _baseOptions; - protected ChallengeContext ChallengeContext { get; set; } - protected SignInContext SignInContext { get; set; } - protected SignOutContext SignOutContext { get; set; } + protected bool SignInAccepted { get; set; } + protected bool SignOutAccepted { get; set; } + protected bool ChallengeCalled { get; set; } protected HttpContext Context { get; private set; } @@ -59,13 +51,8 @@ namespace Microsoft.AspNet.Authentication get { return _baseOptions; } } - // REVIEW: Overriding Authenticate and not calling base requires manually calling this for 401-403 to work - protected bool AuthenticateCalled { get; set; } - public IAuthenticationHandler PriorHandler { get; set; } - public bool Faulted { get; set; } - protected async Task BaseInitializeAsync([NotNull] AuthenticationOptions options, [NotNull] HttpContext context, [NotNull] ILogger logger, [NotNull] IUrlEncoder encoder) { _baseOptions = options; @@ -76,9 +63,7 @@ namespace Microsoft.AspNet.Authentication RegisterAuthenticationHandler(); - Response.OnResponseStarting(OnSendingHeaderCallback, this); - - await InitializeCoreAsync(); + Response.OnStarting(OnStartingCallback, this); if (BaseOptions.AutomaticAuthentication) { @@ -90,50 +75,49 @@ namespace Microsoft.AspNet.Authentication } } - private static void OnSendingHeaderCallback(object state) + private static async Task OnStartingCallback(object state) { - AuthenticationHandler handler = (AuthenticationHandler)state; - handler.ApplyResponse(); + var handler = (AuthenticationHandler)state; + await handler.FinishResponseOnce(); } - protected virtual Task InitializeCoreAsync() + private async Task FinishResponseOnce() + { + if (!_finishCalled) + { + _finishCalled = true; + await FinishResponseAsync(); + await HandleAutomaticChallengeIfNeeded(); + } + } + + /// + /// Hook that is called when the response about to be sent + /// + /// + protected virtual Task FinishResponseAsync() { return Task.FromResult(0); } + private async Task HandleAutomaticChallengeIfNeeded() + { + if (!ChallengeCalled && BaseOptions.AutomaticAuthentication && Response.StatusCode == 401) + { + await HandleUnauthorizedAsync(new ChallengeContext(BaseOptions.AuthenticationScheme)); + } + } + /// - /// Called once per request after Initialize and Invoke. + /// Called once after Invoke by AuthenticationMiddleware. /// /// async completion internal async Task TeardownAsync() { - try - { - await ApplyResponseAsync(); - } - catch (Exception) - { - try - { - await TeardownCoreAsync(); - } - catch (Exception) - { - // Don't mask the original exception - } - UnregisterAuthenticationHandler(); - throw; - } - - await TeardownCoreAsync(); + await FinishResponseOnce(); UnregisterAuthenticationHandler(); } - protected virtual Task TeardownCoreAsync() - { - return Task.FromResult(0); - } - /// /// Called once by common code after initialization. If an authentication middleware responds directly to /// specifically known paths it must override this virtual, compare the request path to it's known paths, @@ -147,7 +131,7 @@ namespace Microsoft.AspNet.Authentication return Task.FromResult(false); } - public virtual void GetDescriptions(DescribeSchemesContext describeContext) + public void GetDescriptions(DescribeSchemesContext describeContext) { describeContext.Accept(BaseOptions.Description.Items); @@ -157,36 +141,13 @@ namespace Microsoft.AspNet.Authentication } } - public virtual void Authenticate(AuthenticateContext context) - { - if (ShouldHandleScheme(context.AuthenticationScheme)) - { - var ticket = Authenticate(); - if (ticket?.Principal != null) - { - AuthenticateCalled = true; - context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); - } - else - { - context.NotAuthenticated(); - } - } - - if (PriorHandler != null) - { - PriorHandler.Authenticate(context); - } - } - - public virtual async Task AuthenticateAsync(AuthenticateContext context) + public async Task AuthenticateAsync(AuthenticateContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { - AuthenticateCalled = true; context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); } else @@ -201,193 +162,66 @@ namespace Microsoft.AspNet.Authentication } } - public AuthenticationTicket Authenticate() - { - return LazyInitializer.EnsureInitialized( - ref _authenticate, - ref _authenticateInitialized, - ref _authenticateSyncLock, - () => - { - return Task.FromResult(AuthenticateCore()); - }).GetAwaiter().GetResult(); - } - - protected abstract AuthenticationTicket AuthenticateCore(); - /// - /// Causes the authentication logic in AuthenticateCore to be performed for the current request - /// at most once and returns the results. Calling Authenticate more than once will always return - /// the original value. - /// - /// This method should always be called instead of calling AuthenticateCore directly. + /// Calling Authenticate more than once should always return the original value. /// /// The ticket data provided by the authentication logic - public Task AuthenticateAsync() - { - return LazyInitializer.EnsureInitialized( - ref _authenticate, - ref _authenticateInitialized, - ref _authenticateSyncLock, - AuthenticateCoreAsync); - } + public abstract Task AuthenticateAsync(); - /// - /// The core authentication logic which must be provided by the handler. Will be invoked at most - /// once per request. Do not call directly, call the wrapping Authenticate method instead. - /// - /// The ticket data provided by the authentication logic - protected virtual Task AuthenticateCoreAsync() - { - return Task.FromResult(AuthenticateCore()); - } - - private void ApplyResponse() - { - // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a - // failed task is cached. If called again the same error will be re-thrown. This breaks error handling - // scenarios like the ability to display the error page or re-execute the request. - try - { - if (!Faulted) - { - LazyInitializer.EnsureInitialized( - ref _applyResponse, - ref _applyResponseInitialized, - ref _applyResponseSyncLock, - () => - { - ApplyResponseCore(); - return Task.FromResult(0); - }).GetAwaiter().GetResult(); // Block if the async version is in progress. - } - } - catch (Exception) - { - Faulted = true; - throw; - } - } - - protected virtual void ApplyResponseCore() - { - ApplyResponseGrant(); - ApplyResponseChallenge(); - } - - /// - /// Causes the ApplyResponseCore to be invoked at most once per request. This method will be - /// invoked either earlier, when the response headers are sent as a result of a response write or flush, - /// or later, as the last step when the original async call to the middleware is returning. - /// - /// - private async Task ApplyResponseAsync() - { - // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a - // failed task is cached. If called again the same error will be re-thrown. This breaks error handling - // scenarios like the ability to display the error page or re-execute the request. - try - { - if (!Faulted) - { - await LazyInitializer.EnsureInitialized( - ref _applyResponse, - ref _applyResponseInitialized, - ref _applyResponseSyncLock, - ApplyResponseCoreAsync); - } - } - catch (Exception) - { - Faulted = true; - throw; - } - } - - /// - /// Core method that may be overridden by handler. The default behavior is to call two common response - /// activities, one that deals with sign-in/sign-out concerns, and a second to deal with 401 challenges. - /// - /// - protected virtual async Task ApplyResponseCoreAsync() - { - await ApplyResponseGrantAsync(); - await ApplyResponseChallengeAsync(); - } - - protected abstract void ApplyResponseGrant(); - - /// - /// Override this method to dela with sign-in/sign-out concerns, if an authentication scheme in question - /// deals with grant/revoke as part of it's request flow. (like setting/deleting cookies) - /// - /// - protected virtual Task ApplyResponseGrantAsync() - { - ApplyResponseGrant(); - return Task.FromResult(0); - } - - public virtual void SignIn(SignInContext context) - { - if (ShouldHandleScheme(context.AuthenticationScheme)) - { - SignInContext = context; - SignOutContext = null; - context.Accept(); - } - - if (PriorHandler != null) - { - PriorHandler.SignIn(context); - } - } - - public virtual void SignOut(SignOutContext context) - { - if (ShouldHandleScheme(context.AuthenticationScheme)) - { - SignInContext = null; - SignOutContext = context; - context.Accept(); - } - - if (PriorHandler != null) - { - PriorHandler.SignOut(context); - } - } - - public virtual void Challenge(ChallengeContext context) - { - if (ShouldHandleScheme(context.AuthenticationScheme)) - { - ChallengeContext = context; - context.Accept(); - } - - if (PriorHandler != null) - { - PriorHandler.Challenge(context); - } - } - - protected abstract void ApplyResponseChallenge(); - - public virtual bool ShouldHandleScheme(string authenticationScheme) + public bool ShouldHandleScheme(string authenticationScheme) { return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); } - public virtual bool ShouldConvertChallengeToForbidden() + public async Task SignInAsync(SignInContext context) { - // Return 403 iff 401 and this handler's authenticate was called - // and the challenge is for the authentication type - return Response.StatusCode == 401 && - AuthenticateCalled && - ChallengeContext != null && - ShouldHandleScheme(ChallengeContext.AuthenticationScheme); + if (ShouldHandleScheme(context.AuthenticationScheme)) + { + SignInAccepted = true; + await HandleSignInAsync(context); + context.Accept(); + } + + if (PriorHandler != null) + { + await PriorHandler.SignInAsync(context); + } + } + + protected virtual Task HandleSignInAsync(SignInContext context) + { + return Task.FromResult(0); + } + + public async Task SignOutAsync(SignOutContext context) + { + if (ShouldHandleScheme(context.AuthenticationScheme)) + { + SignOutAccepted = true; + await HandleSignOutAsync(context); + context.Accept(); + } + + if (PriorHandler != null) + { + await PriorHandler.SignOutAsync(context); + } + } + + protected virtual Task HandleSignOutAsync(SignOutContext context) + { + return Task.FromResult(0); + } + + /// + /// + /// + /// True if no other handlers should be called + protected virtual Task HandleForbiddenAsync(ChallengeContext context) + { + Response.StatusCode = 403; + return Task.FromResult(true); } /// @@ -395,11 +229,48 @@ namespace Microsoft.AspNet.Authentication /// 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.) /// - /// - protected virtual Task ApplyResponseChallengeAsync() + /// + /// True if no other handlers should be called + protected virtual Task HandleUnauthorizedAsync(ChallengeContext context) { - ApplyResponseChallenge(); - return Task.FromResult(0); + Response.StatusCode = 401; + return Task.FromResult(false); + } + + public async Task ChallengeAsync(ChallengeContext context) + { + bool handled = false; + ChallengeCalled = true; + if (ShouldHandleScheme(context.AuthenticationScheme)) + { + switch (context.Behavior) + { + case ChallengeBehavior.Automatic: + // If there is a principal already, invoke the forbidden code path + var ticket = await AuthenticateAsync(); + if (ticket?.Principal != null) + { + handled = await HandleForbiddenAsync(context); + } + else + { + handled = await HandleUnauthorizedAsync(context); + } + break; + case ChallengeBehavior.Unauthorized: + handled = await HandleUnauthorizedAsync(context); + break; + case ChallengeBehavior.Forbidden: + handled = await HandleForbiddenAsync(context); + break; + } + context.Accept(); + } + + if (!handled && PriorHandler != null) + { + await PriorHandler.ChallengeAsync(context); + } } protected void GenerateCorrelationId([NotNull] AuthenticationProperties properties) diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs index 188f903c49..b8b57d7472 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs @@ -67,7 +67,6 @@ namespace Microsoft.AspNet.Authentication { try { - handler.Faulted = true; await handler.TeardownAsync(); } catch (Exception) diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs index 0b6cfdb9f5..fb62323545 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNet.Authentication; using Microsoft.Framework.Internal; @@ -24,7 +25,12 @@ namespace Microsoft.Framework.DependencyInjection public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Func transform) { - return services.Configure(o => o.Transformation = transform); + return services.Configure(o => o.Transformer = new ClaimsTransformer { TransformSyncDelegate = transform }); + } + + public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Func> asyncTransform) + { + return services.Configure(o => o.Transformer = new ClaimsTransformer { TransformAsyncDelegate = asyncTransform }); } } diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs index 4d896e152a..ebc04cb696 100644 --- a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs @@ -1,8 +1,6 @@ // 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.AspNet.Http.Features.Authentication; @@ -13,56 +11,37 @@ namespace Microsoft.AspNet.Authentication /// public class ClaimsTransformationAuthenticationHandler : IAuthenticationHandler { - private readonly Func _transform; + private readonly IClaimsTransformer _transform; - public ClaimsTransformationAuthenticationHandler(Func transform) + public ClaimsTransformationAuthenticationHandler(IClaimsTransformer transform) { _transform = transform; } public IAuthenticationHandler PriorHandler { get; set; } - private void ApplyTransform(AuthenticateContext context) - { - if (_transform != null) - { - // REVIEW: this cast seems really bad (missing interface way to get the result back out?) - var authContext = context as AuthenticateContext; - if (authContext?.Principal != null) - { - context.Authenticated( - _transform.Invoke(authContext.Principal), - authContext.Properties, - authContext.Description); - } - } - - } - - public void Authenticate(AuthenticateContext context) - { - if (PriorHandler != null) - { - PriorHandler.Authenticate(context); - ApplyTransform(context); - } - } - public async Task AuthenticateAsync(AuthenticateContext context) { if (PriorHandler != null) { await PriorHandler.AuthenticateAsync(context); - ApplyTransform(context); + if (_transform != null && context?.Principal != null) + { + context.Authenticated( + await _transform.TransformAsync(context.Principal), + context.Properties, + context.Description); + } } } - public void Challenge(ChallengeContext context) + public Task ChallengeAsync(ChallengeContext context) { if (PriorHandler != null) { - PriorHandler.Challenge(context); + return PriorHandler.ChallengeAsync(context); } + return Task.FromResult(0); } public void GetDescriptions(DescribeSchemesContext context) @@ -73,20 +52,22 @@ namespace Microsoft.AspNet.Authentication } } - public void SignIn(SignInContext context) + public Task SignInAsync(SignInContext context) { if (PriorHandler != null) { - PriorHandler.SignIn(context); + return PriorHandler.SignInAsync(context); } + return Task.FromResult(0); } - public void SignOut(SignOutContext context) + public Task SignOutAsync(SignOutContext context) { if (PriorHandler != null) { - PriorHandler.SignOut(context); + return PriorHandler.SignOutAsync(context); } + return Task.FromResult(0); } public void RegisterAuthenticationHandler(IHttpAuthenticationFeature auth) diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs index 0f785161c4..eb616d9fb9 100644 --- a/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs @@ -34,12 +34,12 @@ namespace Microsoft.AspNet.Authentication public async Task Invoke(HttpContext context) { - var handler = new ClaimsTransformationAuthenticationHandler(Options.Transformation); + var handler = new ClaimsTransformationAuthenticationHandler(Options.Transformer); handler.RegisterAuthenticationHandler(context.GetAuthentication()); try { - if (Options.Transformation != null) + if (Options.Transformer != null) { - context.User = Options.Transformation.Invoke(context.User); + context.User = await Options.Transformer.TransformAsync(context.User); } await _next(context); } diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs index 1eb76c82ac..acc1a5e499 100644 --- a/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs @@ -3,11 +3,42 @@ using System; using System.Security.Claims; +using System.Threading.Tasks; namespace Microsoft.AspNet.Authentication { public class ClaimsTransformationOptions { - public Func Transformation { get; set; } + public IClaimsTransformer Transformer { get; set; } + } + + public interface IClaimsTransformer + { + Task TransformAsync(ClaimsPrincipal principal); + ClaimsPrincipal Transform(ClaimsPrincipal principal); + } + + public class ClaimsTransformer : IClaimsTransformer + { + public Func> TransformAsyncDelegate { get; set; } + public Func TransformSyncDelegate { get; set; } + + public virtual ClaimsPrincipal Transform(ClaimsPrincipal principal) + { + if (TransformSyncDelegate != null) + { + return TransformSyncDelegate(principal); + } + return principal; + } + + public virtual Task TransformAsync(ClaimsPrincipal principal) + { + if (TransformAsyncDelegate != null) + { + return TransformAsyncDelegate(principal); + } + return Task.FromResult(Transform(principal)); + } } } diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs b/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs index b117f8851d..54f6b6bbc4 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs @@ -9,7 +9,7 @@ using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Authorization { /// - /// Contains authorization information used by . + /// Contains authorization information used by . /// public class AuthorizationContext { @@ -28,9 +28,9 @@ namespace Microsoft.AspNet.Authorization _pendingRequirements = new HashSet(requirements); } - public IEnumerable Requirements { get; private set; } - public ClaimsPrincipal User { get; private set; } - public object Resource { get; private set; } + public IEnumerable Requirements { get; } + public ClaimsPrincipal User { get; } + public object Resource { get; } public IEnumerable PendingRequirements { get { return _pendingRequirements; } } diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs b/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs index b682e43ffd..f5418841ab 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs @@ -17,14 +17,21 @@ namespace Microsoft.AspNet.Authorization } } - public virtual Task HandleAsync(AuthorizationContext context) + public virtual async Task HandleAsync(AuthorizationContext context) { - Handle(context); - return Task.FromResult(0); + foreach (var req in context.Requirements.OfType()) + { + await HandleAsync(context, req); + } } - // REVIEW: do we need an async hook too? - public abstract void Handle(AuthorizationContext context, TRequirement requirement); + protected abstract void Handle(AuthorizationContext context, TRequirement requirement); + + protected virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement) + { + Handle(context, requirement); + return Task.FromResult(0); + } } public abstract class AuthorizationHandler : IAuthorizationHandler @@ -34,17 +41,13 @@ namespace Microsoft.AspNet.Authorization public virtual async Task HandleAsync(AuthorizationContext context) { var resource = context.Resource as TResource; - // REVIEW: should we allow null resources? - if (resource != null) + foreach (var req in context.Requirements.OfType()) { - foreach (var req in context.Requirements.OfType()) - { - await HandleAsync(context, req, resource); - } + await HandleAsync(context, req, resource); } } - public virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement, TResource resource) + protected virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement, TResource resource) { Handle(context, requirement, resource); return Task.FromResult(0); @@ -63,6 +66,6 @@ namespace Microsoft.AspNet.Authorization } } - public abstract void Handle(AuthorizationContext context, TRequirement requirement, TResource resource); + protected abstract void Handle(AuthorizationContext context, TRequirement requirement, TResource resource); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs b/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs index 17d879329b..14da299d91 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs @@ -9,8 +9,7 @@ namespace Microsoft.AspNet.Authorization { public class AuthorizationOptions { - // TODO: make this case insensitive - private IDictionary PolicyMap { get; } = new Dictionary(); + private IDictionary PolicyMap { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public void AddPolicy([NotNull] string name, [NotNull] AuthorizationPolicy policy) { diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs index 3799264508..6f2ecc79e9 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs @@ -20,15 +20,14 @@ namespace Microsoft.AspNet.Authorization ActiveAuthenticationSchemes = new List(activeAuthenticationSchemes).AsReadOnly(); } - public IReadOnlyList Requirements { get; private set; } - public IReadOnlyList ActiveAuthenticationSchemes { get; private set; } + public IReadOnlyList Requirements { get; } + public IReadOnlyList ActiveAuthenticationSchemes { get; } public static AuthorizationPolicy Combine([NotNull] params AuthorizationPolicy[] policies) { return Combine((IEnumerable)policies); } - // TODO: Add unit tests public static AuthorizationPolicy Combine([NotNull] IEnumerable policies) { var builder = new AuthorizationPolicyBuilder(); diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs index f41fe73692..7cc20d06b3 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs @@ -1,9 +1,9 @@ // 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 Microsoft.Framework.Internal; namespace Microsoft.AspNet.Authorization @@ -88,6 +88,12 @@ namespace Microsoft.AspNet.Authorization return this; } + public AuthorizationPolicyBuilder RequireDelegate([NotNull] Action handler) + { + Requirements.Add(new DelegateRequirement(handler)); + return this; + } + public AuthorizationPolicy Build() { return new AuthorizationPolicy(Requirements, ActiveAuthenticationSchemes.Distinct()); diff --git a/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs b/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs index 42d757d8e6..cffc17dbd5 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs @@ -6,7 +6,7 @@ using System; namespace Microsoft.AspNet.Authorization { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class AuthorizeAttribute : Attribute + public class AuthorizeAttribute : Attribute, IAuthorizeData { public AuthorizeAttribute() { } diff --git a/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs index c5af9449b1..fa061d28c8 100644 --- a/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs +++ b/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Authorization public string ClaimType { get; } public IEnumerable AllowedValues { get; } - public override void Handle(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) + protected override void Handle(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) { if (context.User != null) { diff --git a/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs index e66721a5c1..bb3d888298 100644 --- a/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs +++ b/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs @@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Authorization foreach (var handler in _handlers) { handler.Handle(authContext); + //REVIEW: Do we want to consider short circuiting on failure } return authContext.HasSucceeded; } diff --git a/src/Microsoft.AspNet.Authorization/DelegateRequirement.cs b/src/Microsoft.AspNet.Authorization/DelegateRequirement.cs new file mode 100644 index 0000000000..11e30c3e6b --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/DelegateRequirement.cs @@ -0,0 +1,22 @@ +// 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.AspNet.Authorization +{ + public class DelegateRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public Action Handler { get; } + + public DelegateRequirement(Action handleMe) + { + Handler = handleMe; + } + + protected override void Handle(AuthorizationContext context, DelegateRequirement requirement) + { + Handler.Invoke(context, requirement); + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs index 92f48c1fe4..8dcc9b1eee 100644 --- a/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs +++ b/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Authorization { public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement { - public override void Handle(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) + protected override void Handle(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) { var user = context.User; var userIsAnonymous = diff --git a/src/Microsoft.AspNet.Authorization/IAuthorizeData.cs b/src/Microsoft.AspNet.Authorization/IAuthorizeData.cs new file mode 100644 index 0000000000..2571fb28e7 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/IAuthorizeData.cs @@ -0,0 +1,14 @@ +// 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.AspNet.Authorization +{ + public interface IAuthorizeData + { + string Policy { get; set; } + + string Roles { get; set; } + + string ActiveAuthenticationSchemes { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs index 7d64f1b71c..5798a0efd0 100644 --- a/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs +++ b/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Authorization public string RequiredName { get; } - public override void Handle(AuthorizationContext context, NameAuthorizationRequirement requirement) + protected override void Handle(AuthorizationContext context, NameAuthorizationRequirement requirement) { if (context.User != null) { diff --git a/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs index f3336237ea..8521c43c12 100644 --- a/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs +++ b/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Authorization public IEnumerable AllowedRoles { get; } - public override void Handle(AuthorizationContext context, RolesAuthorizationRequirement requirement) + protected override void Handle(AuthorizationContext context, RolesAuthorizationRequirement requirement) { if (context.User != null) { diff --git a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs index c531c17caa..e12d4e533c 100644 --- a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs +++ b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs @@ -2,6 +2,8 @@ // 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.AspNet.Http.Features.Authentication; using Microsoft.AspNet.Http.Internal; using Microsoft.Framework.Logging; using Xunit; @@ -59,17 +61,7 @@ namespace Microsoft.AspNet.Authentication Options.AuthenticationScheme = scheme; } - protected override void ApplyResponseChallenge() - { - throw new NotImplementedException(); - } - - protected override void ApplyResponseGrant() - { - throw new NotImplementedException(); - } - - protected override AuthenticationTicket AuthenticateCore() + public override Task AuthenticateAsync() { throw new NotImplementedException(); } @@ -94,17 +86,7 @@ namespace Microsoft.AspNet.Authentication Options.AutomaticAuthentication = auto; } - protected override void ApplyResponseChallenge() - { - throw new NotImplementedException(); - } - - protected override void ApplyResponseGrant() - { - throw new NotImplementedException(); - } - - protected override AuthenticationTicket AuthenticateCore() + public override Task AuthenticateAsync() { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index a2628f0224..9b8686c92d 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -15,6 +15,7 @@ using System.Xml.Linq; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; using Shouldly; @@ -50,7 +51,7 @@ namespace Microsoft.AspNet.Authentication.Cookies transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); if (auto) { - Uri location = transaction.Response.Headers.Location; + var location = transaction.Response.Headers.Location; location.LocalPath.ShouldBe("/login"); location.Query.ShouldBe("?ReturnUrl=%2Fprotected"); } @@ -69,34 +70,32 @@ namespace Microsoft.AspNet.Authentication.Cookies var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); - transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); + // REVIEW: Now when Cookies are not in auto, noone handles the challenge so the Status stays OK, is that reasonable?? + transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.OK); if (auto) { - Uri location = transaction.Response.Headers.Location; + var location = transaction.Response.Headers.Location; location.ToString().ShouldBe("http://example.com/login?ReturnUrl=%2FCustomRedirect"); } } private Task SignInAsAlice(HttpContext context) { - context.Authentication.SignIn("Cookies", + return context.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties()); - return Task.FromResult(null); } private Task SignInAsWrong(HttpContext context) { - context.Authentication.SignIn("Oops", + return context.Authentication.SignInAsync("Oops", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties()); - return Task.FromResult(null); } private Task SignOutAsWrong(HttpContext context) { - context.Authentication.SignOut("Oops"); - return Task.FromResult(null); + return context.Authentication.SignOutAsync("Oops"); } [Fact] @@ -241,17 +240,30 @@ namespace Microsoft.AspNet.Authentication.Cookies }, SignInAsAlice, baseAddress: null, - claimsTransform: o => o.Transformation = (p => + claimsTransform: o => o.Transformer = new ClaimsTransformer { - if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + TransformSyncDelegate = p => { - // REVIEW: Xform runs twice, once on Authenticate, and then once from the middleware - var id = new ClaimsIdentity("xform"); - id.AddClaim(new Claim("xform", "yup")); - p.AddIdentity(id); + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("sync", "no")); + p.AddIdentity(id); + } + return p; + }, + TransformAsyncDelegate = p => + { + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + // REVIEW: Xform runs twice, once on Authenticate, and then once from the middleware + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return Task.FromResult(p); } - return p; - })); + }); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -259,6 +271,7 @@ namespace Microsoft.AspNet.Authentication.Cookies FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); FindClaimValue(transaction2, "xform").ShouldBe("yup"); + FindClaimValue(transaction2, "sync").ShouldBe(null); } @@ -304,12 +317,9 @@ namespace Microsoft.AspNet.Authentication.Cookies options.SlidingExpiration = false; }, context => - { - context.Authentication.SignIn("Cookies", + context.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), - new AuthenticationProperties() { ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)) }); - return Task.FromResult(null); - }); + new AuthenticationProperties() { ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)) })); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -433,9 +443,8 @@ namespace Microsoft.AspNet.Authentication.Cookies context => { Assert.Equal(new PathString("/base"), context.Request.PathBase); - context.Authentication.SignIn("Cookies", + return context.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); - return Task.FromResult(null); }, new Uri("http://example.com/base")); @@ -446,7 +455,7 @@ namespace Microsoft.AspNet.Authentication.Cookies [Theory] [InlineData(true)] [InlineData(false)] - public async Task CookieTurns401To403IfAuthenticated(bool automatic) + public async Task CookieTurns401To403WithCookie(bool automatic) { var clock = new TestClock(); var server = CreateServer(options => @@ -458,18 +467,56 @@ namespace Microsoft.AspNet.Authentication.Cookies var transaction1 = await SendAsync(server, "http://example.com/testpath"); - var url = "http://example.com/unauthorized"; - if (automatic) - { - url += "auto"; - } + var url = "http://example.com/challenge"; var transaction2 = await SendAsync(server, url, transaction1.CookieNameValue); transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CookieChallengeRedirectsToLoginWithoutCookie(bool automatic) + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = automatic; + options.AccessDeniedPath = new PathString("/accessdenied"); + options.SystemClock = clock; + }, + SignInAsAlice); + + var url = "http://example.com/challenge"; + var transaction = await SendAsync(server, url); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/login"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CookieForbidTurns401To403WithoutCookie(bool automatic) + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.AutomaticAuthentication = automatic; + options.SystemClock = clock; + }, + SignInAsAlice); + + var url = "http://example.com/forbid"; + var transaction = await SendAsync(server, url); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + [Fact] - public async Task CookieTurns401ToAccessDeniedWhenSetAndIfAuthenticated() + public async Task CookieTurns401ToAccessDeniedWhenSetWithCookie() { var clock = new TestClock(); var server = CreateServer(options => @@ -481,7 +528,7 @@ namespace Microsoft.AspNet.Authentication.Cookies var transaction1 = await SendAsync(server, "http://example.com/testpath"); - var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue); transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); @@ -490,7 +537,23 @@ namespace Microsoft.AspNet.Authentication.Cookies } [Fact] - public async Task CookieDoesNothingTo401IfNotAuthenticated() + public async Task CookieChallengeDoesNothingIfNotAuthenticated() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + }); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated() { var clock = new TestClock(); var server = CreateServer(options => @@ -549,29 +612,33 @@ namespace Microsoft.AspNet.Authentication.Cookies { res.StatusCode = 401; } + else if (req.Path == new PathString("/forbid")) // Simulate forbidden + { + await context.Authentication.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/challenge")) + { + await context.Authentication.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } else if (req.Path == new PathString("/unauthorized")) { - // Simulate Authorization failure - var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); - context.Authentication.Challenge(CookieAuthenticationDefaults.AuthenticationScheme); - } - else if (req.Path == new PathString("/unauthorizedauto")) - { - // Simulate Authorization failure - context.Authentication.Challenge(CookieAuthenticationDefaults.AuthenticationScheme); + await context.Authentication.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties(), ChallengeBehavior.Unauthorized); } else if (req.Path == new PathString("/protected/CustomRedirect")) { - context.Authentication.Challenge(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); + await context.Authentication.ChallengeAsync(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); } else if (req.Path == new PathString("/me")) { - Describe(res, new AuthenticationResult(context.User, new AuthenticationProperties(), new AuthenticationDescription())); + var authContext = new AuthenticateContext(CookieAuthenticationDefaults.AuthenticationScheme); + authContext.Authenticated(context.User, properties: null, description: null); + Describe(res, authContext); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { - var result = await context.Authentication.AuthenticateAsync(remainder.Value.Substring(1)); - Describe(res, result); + var authContext = new AuthenticateContext(remainder.Value.Substring(1)); + await context.Authentication.AuthenticateAsync(authContext); + Describe(res, authContext); } else if (req.Path == new PathString("/testpath") && testpath != null) { @@ -595,7 +662,7 @@ namespace Microsoft.AspNet.Authentication.Cookies return server; } - private static void Describe(HttpResponse res, AuthenticationResult result) + private static void Describe(HttpResponse res, AuthenticateContext result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -606,7 +673,7 @@ namespace Microsoft.AspNet.Authentication.Cookies } if (result != null && result.Properties != null) { - xml.Add(result.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + xml.Add(result.Properties.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); } using (var memory = new MemoryStream()) { diff --git a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs index 6279c0a8ea..7e01755674 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs @@ -51,7 +51,8 @@ namespace Microsoft.AspNet.Authentication.Facebook }, context => { - context.Authentication.Challenge("Facebook"); + // REVIEW: Gross. + context.Authentication.ChallengeAsync("Facebook").GetAwaiter().GetResult(); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -88,7 +89,8 @@ namespace Microsoft.AspNet.Authentication.Facebook }, context => { - context.Authentication.Challenge("Facebook"); + // REVIEW: gross + context.Authentication.ChallengeAsync("Facebook").GetAwaiter().GetResult(); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs index 0bbd57213c..c48409eedf 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -46,6 +46,42 @@ namespace Microsoft.AspNet.Authentication.Google location.ShouldNotContain("login_hint="); } + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + [Fact] public async Task Challenge401WillTriggerRedirection() { @@ -134,7 +170,7 @@ namespace Microsoft.AspNet.Authentication.Google var res = context.Response; if (req.Path == new PathString("/challenge2")) { - context.Authentication.Challenge("Google", new AuthenticationProperties( + return context.Authentication.ChallengeAsync("Google", new AuthenticationProperties( new Dictionary() { { "scope", "https://www.googleapis.com/auth/plus.login" }, @@ -142,7 +178,6 @@ namespace Microsoft.AspNet.Authentication.Google { "approval_prompt", "force" }, { "login_hint", "test@example.com" } })); - res.StatusCode = 401; } return Task.FromResult(null); @@ -177,35 +212,6 @@ namespace Microsoft.AspNet.Authentication.Google query.ShouldContain("custom=test"); } - // TODO: Fix these tests to path (Need some test logic for Authenticate("Google") to return a ticket still - //[Fact] - //public async Task GoogleTurns401To403WhenAuthenticated() - //{ - // TestServer server = CreateServer(options => - // { - // options.ClientId = "Test Id"; - // options.ClientSecret = "Test Secret"; - // }); - - // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorized"); - // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); - //} - - //[Fact] - //public async Task GoogleTurns401To403WhenAutomatic() - //{ - // TestServer server = CreateServer(options => - // { - // options.ClientId = "Test Id"; - // options.ClientSecret = "Test Secret"; - // options.AutomaticAuthentication = true; - // }); - - // Debugger.Launch(); - // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorizedAuto"); - // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); - //} - [Fact] public async Task ReplyPathWithoutStateQueryStringWillBeRejected() { @@ -218,8 +224,6 @@ namespace Microsoft.AspNet.Authentication.Google transaction.Response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); } - - [Theory] [InlineData(null)] [InlineData("CustomIssuer")] @@ -459,24 +463,14 @@ namespace Microsoft.AspNet.Authentication.Google options.AutomaticAuthentication = true; }); app.UseGoogleAuthentication(configureOptions); - app.UseClaimsTransformation(o => - { - o.Transformation = p => - { - var id = new ClaimsIdentity("xform"); - id.AddClaim(new Claim("xform", "yup")); - p.AddIdentity(id); - return p; - }; - }); + app.UseClaimsTransformation(); app.Use(async (context, next) => { var req = context.Request; var res = context.Response; if (req.Path == new PathString("/challenge")) { - context.Authentication.Challenge("Google"); - res.StatusCode = 401; + await context.Authentication.ChallengeAsync("Google"); } else if (req.Path == new PathString("/me")) { @@ -486,18 +480,29 @@ namespace Microsoft.AspNet.Authentication.Google { // Simulate Authorization failure var result = await context.Authentication.AuthenticateAsync("Google"); - context.Authentication.Challenge("Google"); + await context.Authentication.ChallengeAsync("Google"); } else if (req.Path == new PathString("/unauthorizedAuto")) { var result = await context.Authentication.AuthenticateAsync("Google"); - res.StatusCode = 401; - context.Authentication.Challenge(); + await context.Authentication.ChallengeAsync(); } else if (req.Path == new PathString("/401")) { res.StatusCode = 401; } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Google", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Google")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Google")); + } else if (testpath != null) { await testpath(context); @@ -515,8 +520,14 @@ namespace Microsoft.AspNet.Authentication.Google { options.SignInScheme = TestExtensions.CookieAuthenticationScheme; }); + services.ConfigureClaimsTransformation(p => + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + return p; + }); }); } - } } diff --git a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs index 2d6baba35c..253263fb54 100644 --- a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -45,6 +45,42 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount query.ShouldContain("custom=test"); } + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + [Fact] public async Task ChallengeWillTriggerRedirection() { @@ -155,13 +191,24 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount var res = context.Response; if (req.Path == new PathString("/challenge")) { - context.Authentication.Challenge("Microsoft"); - res.StatusCode = 401; + await context.Authentication.ChallengeAsync("Microsoft"); } else if (req.Path == new PathString("/me")) { res.Describe(context.User); } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Microsoft", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Microsoft")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Microsoft")); + } else { await next(); diff --git a/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs index f21b00f79b..ce481bf6a1 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -40,6 +40,29 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); } + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] public async Task CustomHeaderReceived() { @@ -326,9 +349,17 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer { // Simulate Authorization failure var result = await context.Authentication.AuthenticateAsync(OAuthBearerAuthenticationDefaults.AuthenticationScheme); - context.Authentication.Challenge(OAuthBearerAuthenticationDefaults.AuthenticationScheme); + await context.Authentication.ChallengeAsync(OAuthBearerAuthenticationDefaults.AuthenticationScheme); } + else if (context.Request.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignInAsync(OAuthBearerAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal())); + } + else if (context.Request.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync(OAuthBearerAuthenticationDefaults.AuthenticationScheme)); + } else { await next(); diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs index 183ae60181..13357a162a 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -549,26 +549,14 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect await base.BaseInitializeAsync(options, context, logger, encoder); } - public override bool ShouldHandleScheme(string authenticationScheme) - { - return true; - } - - public override void Challenge(ChallengeContext context) - { - } - - protected override void ApplyResponseChallenge() - { - } - - protected override async Task ApplyResponseChallengeAsync() + protected override async Task HandleUnauthorizedAsync(ChallengeContext context) { var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) { }; await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + return true; } } diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index 32aa028f03..e56da3b907 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -196,7 +196,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { app.UseCookieAuthentication(options => { - options.AuthenticationScheme = "OpenIdConnect"; + options.AuthenticationScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; }); app.UseOpenIdConnectAuthentication(configureOptions); app.Use(async (context, next) => @@ -205,21 +205,19 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect var res = context.Response; if (req.Path == new PathString("/challenge")) { - context.Authentication.Challenge("OpenIdConnect"); - res.StatusCode = 401; + await context.Authentication.ChallengeAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/signin")) { - // REVIEW: this used to just be res.SignIn() - context.Authentication.SignIn("OpenIdConnect", new ClaimsPrincipal()); + await context.Authentication.SignInAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal()); } else if (req.Path == new PathString("/signout")) { - context.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); + await context.Authentication.SignOutAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) { - context.Authentication.SignOut( + await context.Authentication.SignOutAsync( OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); } diff --git a/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs index abbe3d37fa..eb5b00554d 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs @@ -3,6 +3,7 @@ using System; using System.Net; using System.Net.Http; +using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; @@ -19,8 +20,7 @@ namespace Microsoft.AspNet.Authentication.Twitter [Fact] public async Task ChallengeWillTriggerApplyRedirectEvent() { - var server = CreateServer( - app => app.UseTwitterAuthentication(options => + var server = CreateServer(options => { options.ConsumerKey = "Test Consumer Key"; options.ConsumerSecret = "Test Consumer Secret"; @@ -49,10 +49,11 @@ namespace Microsoft.AspNet.Authentication.Twitter } }; options.BackchannelCertificateValidator = null; - }), - context => + }, + context => { - context.Authentication.Challenge("Twitter"); + // REVIEW: Gross + context.Authentication.ChallengeAsync("Twitter").GetAwaiter().GetResult(); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -61,11 +62,47 @@ namespace Microsoft.AspNet.Authentication.Twitter query.ShouldContain("custom=test"); } + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] public async Task ChallengeWillTriggerRedirection() { - var server = CreateServer( - app => app.UseTwitterAuthentication(options => + var server = CreateServer(options => { options.ConsumerKey = "Test Consumer Key"; options.ConsumerSecret = "Test Consumer Secret"; @@ -87,10 +124,11 @@ namespace Microsoft.AspNet.Authentication.Twitter } }; options.BackchannelCertificateValidator = null; - }), + }, context => { - context.Authentication.Challenge("Twitter"); + // REVIEW: gross + context.Authentication.ChallengeAsync("Twitter").GetAwaiter().GetResult(); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -99,7 +137,7 @@ namespace Microsoft.AspNet.Authentication.Twitter location.ShouldContain("https://twitter.com/oauth/authenticate?oauth_token="); } - private static TestServer CreateServer(Action configure, Func handler) + private static TestServer CreateServer(Action configure, Func handler = null) { return TestServer.Create(app => { @@ -107,13 +145,24 @@ namespace Microsoft.AspNet.Authentication.Twitter { options.AuthenticationScheme = "External"; }); - if (configure != null) - { - configure(app); - } + app.UseTwitterAuthentication(configure); app.Use(async (context, next) => { - if (handler == null || !handler(context)) + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Twitter", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Twitter")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Twitter")); + } + else if (handler == null || !handler(context)) { await next(); } diff --git a/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs index d5491164d8..a3d9c21b14 100644 --- a/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs +++ b/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs @@ -582,7 +582,7 @@ namespace Microsoft.AspNet.Authorization.Test public class CustomRequirement : IAuthorizationRequirement { } public class CustomHandler : AuthorizationHandler { - public override void Handle(AuthorizationContext context, CustomRequirement requirement) + protected override void Handle(AuthorizationContext context, CustomRequirement requirement) { context.Succeed(requirement); } @@ -638,7 +638,7 @@ namespace Microsoft.AspNet.Authorization.Test public bool Succeed { get; set; } - public override void Handle(AuthorizationContext context, PassThroughRequirement requirement) + protected override void Handle(AuthorizationContext context, PassThroughRequirement requirement) { if (Succeed) { context.Succeed(requirement); @@ -668,6 +668,7 @@ namespace Microsoft.AspNet.Authorization.Test Assert.Equal(shouldSucceed, allowed); } + [Fact] public async Task CanCombinePolicies() { // Arrange @@ -695,6 +696,7 @@ namespace Microsoft.AspNet.Authorization.Test Assert.True(allowed); } + [Fact] public async Task CombinePoliciesWillFailIfBasePolicyFails() { // Arrange @@ -721,6 +723,7 @@ namespace Microsoft.AspNet.Authorization.Test Assert.False(allowed); } + [Fact] public async Task CombinedPoliciesWillFailIfExtraRequirementFails() { // Arrange @@ -765,7 +768,7 @@ namespace Microsoft.AspNet.Authorization.Test private IEnumerable _allowed; - public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) + protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) { if (_allowed.Contains(requirement)) { @@ -776,7 +779,7 @@ namespace Microsoft.AspNet.Authorization.Test public class SuperUserHandler : AuthorizationHandler { - public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement) + protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement) { if (context.User.HasClaim("SuperUser", "yes")) { @@ -785,6 +788,7 @@ namespace Microsoft.AspNet.Authorization.Test } } + [Fact] public async Task CanAuthorizeAllSuperuserOperations() { // Arrange @@ -808,6 +812,7 @@ namespace Microsoft.AspNet.Authorization.Test Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); } + [Fact] public async Task CanAuthorizeOnlyAllowedOperations() { // Arrange @@ -824,5 +829,24 @@ namespace Microsoft.AspNet.Authorization.Test Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); } + + [Fact] + public void CanAuthorizeWithDelegateRequirement() + { + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireDelegate((context, req) => context.Succeed(req))); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = authorizationService.Authorize(user, "Basic"); + + // Assert + Assert.True(allowed); + } } } \ No newline at end of file