diff --git a/samples/CookieSample/Startup.cs b/samples/CookieSample/Startup.cs index b21a6a85b9..2d77ae1bb4 100644 --- a/samples/CookieSample/Startup.cs +++ b/samples/CookieSample/Startup.cs @@ -21,7 +21,7 @@ namespace CookieSample app.UseCookieAuthentication(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); app.Run(async context => diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs index 3e89f67ca6..5affb5afaf 100644 --- a/samples/CookieSessionSample/Startup.cs +++ b/samples/CookieSessionSample/Startup.cs @@ -22,7 +22,7 @@ namespace CookieSessionSample app.UseCookieAuthentication(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.SessionStore = new MemoryCacheTicketStore(); }); diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 2001cd979a..546d1f7640 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -23,7 +23,7 @@ namespace OpenIdConnectSample app.UseCookieAuthentication(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); app.UseOpenIdConnectAuthentication(options => diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 9915784d72..4fd444ea89 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -29,7 +29,8 @@ namespace CookieSample app.UseCookieAuthentication(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; + options.AutomaticChallenge = true; options.LoginPath = new PathString("/login"); }); diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index 5ea99ad2b0..f135c3f01f 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -101,43 +101,28 @@ namespace Microsoft.AspNet.Authentication.Cookies return ticket; } - protected override async Task HandleAuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { - AuthenticationTicket ticket = null; - try + var ticket = await EnsureCookieTicket(); + if (ticket == null) { - ticket = await EnsureCookieTicket(); - if (ticket == null) - { - return null; - } - - var context = new CookieValidatePrincipalContext(Context, ticket, Options); - await Options.Events.ValidatePrincipal(context); - - if (context.Principal == null) - { - return null; - } - - if (context.ShouldRenew) - { - _shouldRenew = true; - } - - return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + return AuthenticateResult.Failed("No ticket."); } - catch (Exception exception) + + var context = new CookieValidatePrincipalContext(Context, ticket, Options); + await Options.Events.ValidatePrincipal(context); + + if (context.Principal == null) { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.Authenticate, exception, ticket); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } - return exceptionContext.Ticket; + return AuthenticateResult.Failed("No principal."); } + + if (context.ShouldRenew) + { + _shouldRenew = true; + } + + return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme)); } private CookieOptions BuildCookieOptions() @@ -167,8 +152,9 @@ namespace Microsoft.AspNet.Authentication.Cookies return; } - var ticket = await HandleAuthenticateOnceAsync(); - try + // REVIEW: Should this check if there was an error, and then if that error was already handled?? + var ticket = (await HandleAuthenticateOnceAsync())?.Ticket; + if (ticket != null) { if (_renewIssuedUtc.HasValue) { @@ -205,141 +191,105 @@ namespace Microsoft.AspNet.Authentication.Cookies await ApplyHeaders(shouldRedirectToReturnUrl: false); } - catch (Exception exception) - { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.FinishResponse, exception, ticket); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } - } } protected override async Task HandleSignInAsync(SignInContext signin) { var ticket = await EnsureCookieTicket(); - try + var cookieOptions = BuildCookieOptions(); + + var signInContext = new CookieSigningInContext( + Context, + Options, + Options.AuthenticationScheme, + signin.Principal, + new AuthenticationProperties(signin.Properties), + cookieOptions); + + DateTimeOffset issuedUtc; + if (signInContext.Properties.IssuedUtc.HasValue) { - var cookieOptions = BuildCookieOptions(); - - var signInContext = new CookieSigningInContext( - 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); - } - - await Options.Events.SigningIn(signInContext); - - if (signInContext.Properties.IsPersistent) - { - var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); - signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; - } - - ticket = 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(ticket); - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, - Options.ClaimsIssuer)); - ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); - } - var cookieValue = Options.TicketDataFormat.Protect(ticket); - - Options.CookieManager.AppendResponseCookie( - Context, - Options.CookieName, - cookieValue, - signInContext.CookieOptions); - - var signedInContext = new CookieSignedInContext( - Context, - Options, - Options.AuthenticationScheme, - signInContext.Principal, - signInContext.Properties); - - await Options.Events.SignedIn(signedInContext); - - // Only redirect on the login path - var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; - await ApplyHeaders(shouldRedirect); + issuedUtc = signInContext.Properties.IssuedUtc.Value; } - catch (Exception exception) + else { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.SignIn, exception, ticket); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } + issuedUtc = Options.SystemClock.UtcNow; + signInContext.Properties.IssuedUtc = issuedUtc; } + + if (!signInContext.Properties.ExpiresUtc.HasValue) + { + signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); + } + + await Options.Events.SigningIn(signInContext); + + if (signInContext.Properties.IsPersistent) + { + var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); + signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + } + + ticket = 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(ticket); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.ClaimsIssuer)); + ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + } + var cookieValue = Options.TicketDataFormat.Protect(ticket); + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + signInContext.CookieOptions); + + var signedInContext = new CookieSignedInContext( + Context, + Options, + Options.AuthenticationScheme, + signInContext.Principal, + signInContext.Properties); + + await Options.Events.SignedIn(signedInContext); + + // Only redirect on the login path + var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; + await ApplyHeaders(shouldRedirect); } protected override async Task HandleSignOutAsync(SignOutContext signOutContext) { var ticket = await EnsureCookieTicket(); - try + var cookieOptions = BuildCookieOptions(); + if (Options.SessionStore != null && _sessionKey != null) { - var cookieOptions = BuildCookieOptions(); - if (Options.SessionStore != null && _sessionKey != null) - { - await Options.SessionStore.RemoveAsync(_sessionKey); - } - - var context = new CookieSigningOutContext( - Context, - Options, - cookieOptions); - - await Options.Events.SigningOut(context); - - Options.CookieManager.DeleteCookie( - Context, - Options.CookieName, - context.CookieOptions); - - // Only redirect on the logout path - var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath; - await ApplyHeaders(shouldRedirect); - } - catch (Exception exception) - { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.SignOut, exception, ticket); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } + await Options.SessionStore.RemoveAsync(_sessionKey); } + + var context = new CookieSigningOutContext( + Context, + Options, + cookieOptions); + + await Options.Events.SigningOut(context); + + Options.CookieManager.DeleteCookie( + Context, + Options.CookieName, + context.CookieOptions); + + // Only redirect on the logout path + var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath; + await ApplyHeaders(shouldRedirect); } private async Task ApplyHeaders(bool shouldRedirectToReturnUrl) @@ -374,30 +324,17 @@ namespace Microsoft.AspNet.Authentication.Cookies return path[0] == '/' && path[1] != '/' && path[1] != '\\'; } - protected async override Task HandleForbiddenAsync(ChallengeContext context) + protected override async Task HandleForbiddenAsync(ChallengeContext context) { - try - { - var accessDeniedUri = - Request.Scheme + - "://" + - Request.Host + - OriginalPathBase + - Options.AccessDeniedPath; + var accessDeniedUri = + Request.Scheme + + "://" + + Request.Host + + OriginalPathBase + + Options.AccessDeniedPath; - var redirectContext = new CookieRedirectContext(Context, Options, accessDeniedUri); - await Options.Events.RedirectToAccessDenied(redirectContext); - } - catch (Exception exception) - { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.Forbidden, exception, ticket: null); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } - } + var redirectContext = new CookieRedirectContext(Context, Options, accessDeniedUri); + await Options.Events.RedirectToAccessDenied(redirectContext); return true; } @@ -409,28 +346,16 @@ namespace Microsoft.AspNet.Authentication.Cookies } var redirectUri = new AuthenticationProperties(context.Properties).RedirectUri; - try + if (string.IsNullOrEmpty(redirectUri)) { - if (string.IsNullOrEmpty(redirectUri)) - { - redirectUri = OriginalPathBase + Request.Path + Request.QueryString; - } + redirectUri = OriginalPathBase + Request.Path + Request.QueryString; + } - var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); - var redirectContext = new CookieRedirectContext(Context, Options, BuildRedirectUri(loginUri)); - await Options.Events.RedirectToLogin(redirectContext); - } - catch (Exception exception) - { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.Unauthorized, exception, ticket: null); - await Options.Events.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } - } + var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); + var redirectContext = new CookieRedirectContext(Context, Options, BuildRedirectUri(loginUri)); + await Options.Events.RedirectToLogin(redirectContext); return true; + } } } diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/BaseCookieContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/BaseCookieContext.cs new file mode 100644 index 0000000000..6a437551fe --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/BaseCookieContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + public class BaseCookieContext : BaseContext + { + public BaseCookieContext( + HttpContext context, + CookieAuthenticationOptions options) + : base(context) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + public CookieAuthenticationOptions Options { get; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieAuthenticationEvents.cs index b8b88060b5..43ad5b0e70 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieAuthenticationEvents.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieAuthenticationEvents.cs @@ -42,11 +42,6 @@ namespace Microsoft.AspNet.Authentication.Cookies return Task.FromResult(0); }; - /// - /// A delegate assigned to this property will be invoked when the related method is called. - /// - public Func OnException { get; set; } = context => Task.FromResult(0); - /// /// Implements the interface method by invoking the related delegate method. /// @@ -95,11 +90,5 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Contains information about the event public virtual Task RedirectToAccessDenied(CookieRedirectContext context) => OnRedirect(context); - - /// - /// Implements the interface method by invoking the related delegate method. - /// - /// Contains information about the event - public virtual Task Exception(CookieExceptionContext context) => OnException(context); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieExceptionContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieExceptionContext.cs deleted file mode 100644 index 62ec2eb934..0000000000 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieExceptionContext.cs +++ /dev/null @@ -1,98 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Authentication.Cookies -{ - /// - /// Context object passed to the ICookieAuthenticationProvider method Exception. - /// - public class CookieExceptionContext : BaseContext - { - /// - /// Creates a new instance of the context object. - /// - /// The HTTP request context - /// The middleware options - /// The location of the exception - /// The exception thrown. - /// The current ticket, if any. - public CookieExceptionContext( - HttpContext context, - CookieAuthenticationOptions options, - ExceptionLocation location, - Exception exception, - AuthenticationTicket ticket) - : base(context, options) - { - Location = location; - Exception = exception; - Rethrow = true; - Ticket = ticket; - } - - /// - /// The code paths where exceptions may be reported. - /// - [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", - Target = "Microsoft.Owin.Security.Cookies.CookieExceptionContext+ExceptionLocation", Justification = "It is a directly related option.")] - public enum ExceptionLocation - { - /// - /// The exception was reported in the Authenticate code path. - /// - Authenticate, - - /// - /// The exception was reported in the FinishResponse code path, during sign-in, sign-out, or refresh. - /// - FinishResponse, - - /// - /// The exception was reported in the Unauthorized code path, during redirect generation. - /// - Unauthorized, - - /// - /// The exception was reported in the Forbidden code path, during redirect generation. - /// - Forbidden, - - /// - /// The exception was reported in the SignIn code path - /// - SignIn, - - /// - /// The exception was reported in the SignOut code path - /// - SignOut, - - } - - /// - /// The code path the exception occurred in. - /// - public ExceptionLocation Location { get; private set; } - - /// - /// The exception thrown. - /// - public Exception Exception { get; private set; } - - /// - /// True if the exception should be re-thrown (default), false if it should be suppressed. - /// - public bool Rethrow { get; set; } - - /// - /// The current authentication ticket, if any. - /// In the AuthenticateAsync code path, if the given exception is not re-thrown then this ticket - /// will be returned to the application. The ticket may be replaced if needed. - /// - public AuthenticationTicket Ticket { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieRedirectContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieRedirectContext.cs index 0687b9d280..07a69dc358 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieRedirectContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieRedirectContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Context passed when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware /// - public class CookieRedirectContext : BaseContext + public class CookieRedirectContext : BaseCookieContext { /// /// Creates a new context object. diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSignedInContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSignedInContext.cs index d8ac62a24e..838f73e621 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSignedInContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSignedInContext.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Context object passed to the ICookieAuthenticationEvents method SignedIn. /// - public class CookieSignedInContext : BaseContext + public class CookieSignedInContext : BaseCookieContext { /// /// Creates a new instance of the context object. diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningInContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningInContext.cs index 9bac7dc534..cf630fc31d 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningInContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningInContext.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Context object passed to the ICookieAuthenticationEvents method SigningIn. /// - public class CookieSigningInContext : BaseContext + public class CookieSigningInContext : BaseCookieContext { /// /// Creates a new instance of the context object. diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningOutContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningOutContext.cs index a529060ada..55a9c762d8 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningOutContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieSigningOutContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Context object passed to the ICookieAuthenticationEvents method SigningOut /// - public class CookieSigningOutContext : BaseContext + public class CookieSigningOutContext : BaseCookieContext { /// /// diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs index a37f776d9e..af06533855 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Context object passed to the ICookieAuthenticationProvider method ValidatePrincipal. /// - public class CookieValidatePrincipalContext : BaseContext + public class CookieValidatePrincipalContext : BaseCookieContext { /// /// Creates a new instance of the context object. diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs index 9458b4f243..9c3cc912dd 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs @@ -60,11 +60,5 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Contains information about the login session as well as information about the authentication cookie. Task SigningOut(CookieSigningOutContext context); - - /// - /// Called when an exception occurs during request or response processing. - /// - /// Contains information about the exception that occurred - Task Exception(CookieExceptionContext context); } } diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs index 2ac80c4f73..f8848d210d 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs @@ -6,7 +6,7 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication.JwtBearer { - public class AuthenticationFailedContext : BaseControlContext + public class AuthenticationFailedContext : BaseJwtBearerContext { public AuthenticationFailedContext(HttpContext context, JwtBearerOptions options) : base(context, options) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs new file mode 100644 index 0000000000..91dd8cea22 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.JwtBearer +{ + public class BaseJwtBearerContext : BaseControlContext + { + public BaseJwtBearerContext(HttpContext context, JwtBearerOptions options) + : base(context) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + public JwtBearerOptions Options { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs index b67c5b9e55..7a6ce6991a 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs @@ -5,7 +5,7 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication.JwtBearer { - public class JwtBearerChallengeContext : BaseControlContext + public class JwtBearerChallengeContext : BaseJwtBearerContext { public JwtBearerChallengeContext(HttpContext context, JwtBearerOptions options) : base(context, options) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivedTokenContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivedTokenContext.cs index 1cf0eefb1b..0aadaf2a99 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivedTokenContext.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivedTokenContext.cs @@ -5,7 +5,7 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication.JwtBearer { - public class ReceivedTokenContext : BaseControlContext + public class ReceivedTokenContext : BaseJwtBearerContext { public ReceivedTokenContext(HttpContext context, JwtBearerOptions options) : base(context, options) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivingTokenContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivingTokenContext.cs index 3f85c55214..b0d824f3f7 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivingTokenContext.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/ReceivingTokenContext.cs @@ -5,7 +5,7 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication.JwtBearer { - public class ReceivingTokenContext : BaseControlContext + public class ReceivingTokenContext : BaseJwtBearerContext { public ReceivingTokenContext(HttpContext context, JwtBearerOptions options) : base(context, options) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/TokenValidatedContext.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/TokenValidatedContext.cs index 3d95e4acc0..9ae2fa68aa 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/TokenValidatedContext.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/TokenValidatedContext.cs @@ -5,7 +5,7 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication.JwtBearer { - public class ValidatedTokenContext : BaseControlContext + public class ValidatedTokenContext : BaseJwtBearerContext { public ValidatedTokenContext(HttpContext context, JwtBearerOptions options) : base(context, options) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs index 9627c0e1f0..79ca1a084d 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. /// /// - protected override async Task HandleAuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { string token = null; try @@ -32,12 +32,12 @@ namespace Microsoft.AspNet.Authentication.JwtBearer await Options.Events.ReceivingToken(receivingTokenContext); if (receivingTokenContext.HandledResponse) { - return receivingTokenContext.AuthenticationTicket; + return AuthenticateResult.Success(receivingTokenContext.AuthenticationTicket); } if (receivingTokenContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } // If application retrieved token from somewhere else, use that. @@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer // If no authorization header found, nothing to process further if (string.IsNullOrEmpty(authorization)) { - return null; + return AuthenticateResult.Failed("No authorization header."); } if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) @@ -61,7 +61,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer // If no token found, no further work possible if (string.IsNullOrEmpty(token)) { - return null; + return AuthenticateResult.Failed("No bearer token."); } } @@ -74,12 +74,12 @@ namespace Microsoft.AspNet.Authentication.JwtBearer await Options.Events.ReceivedToken(receivedTokenContext); if (receivedTokenContext.HandledResponse) { - return receivedTokenContext.AuthenticationTicket; + return AuthenticateResult.Success(receivedTokenContext.AuthenticationTicket); } if (receivedTokenContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } if (_configuration == null && Options.ConfigurationManager != null) @@ -118,18 +118,19 @@ namespace Microsoft.AspNet.Authentication.JwtBearer await Options.Events.ValidatedToken(validatedTokenContext); if (validatedTokenContext.HandledResponse) { - return validatedTokenContext.AuthenticationTicket; + return AuthenticateResult.Success(validatedTokenContext.AuthenticationTicket); } if (validatedTokenContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } - return ticket; + return AuthenticateResult.Success(ticket); } } + // REVIEW: this maybe return an error instead? throw new InvalidOperationException("No SecurityTokenValidator available for token: " + token ?? "null"); } catch (Exception ex) @@ -150,12 +151,11 @@ namespace Microsoft.AspNet.Authentication.JwtBearer await Options.Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.HandledResponse) { - return authenticationFailedContext.AuthenticationTicket; + return AuthenticateResult.Success(authenticationFailedContext.AuthenticationTicket); } - if (authenticationFailedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } throw; diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingContext.cs deleted file mode 100644 index 7ce31e63f8..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingContext.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Authentication.OAuth -{ - /// - /// Base class used for certain event contexts - /// - public abstract class BaseValidatingContext : BaseContext - { - /// - /// Initializes base class used for certain event contexts - /// - protected BaseValidatingContext( - HttpContext context, - TOptions options) - : base(context, options) - { - } - - /// - /// True if application code has called any of the Validate methods on this context. - /// - public bool IsValidated { get; private set; } - - /// - /// True if application code has called any of the SetError methods on this context. - /// - public bool HasError { get; private set; } - - /// - /// The error argument provided when SetError was called on this context. This is eventually - /// returned to the client app as the OAuth "error" parameter. - /// - public string Error { get; private set; } - - /// - /// The optional errorDescription argument provided when SetError was called on this context. This is eventually - /// returned to the client app as the OAuth "error_description" parameter. - /// - public string ErrorDescription { get; private set; } - - /// - /// The optional errorUri argument provided when SetError was called on this context. This is eventually - /// returned to the client app as the OAuth "error_uri" parameter. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "error_uri is a string value in the protocol")] - public string ErrorUri { get; private set; } - - /// - /// Marks this context as validated by the application. IsValidated becomes true and HasError becomes false as a result of calling. - /// - /// True if the validation has taken effect. - public virtual bool Validated() - { - IsValidated = true; - HasError = false; - return true; - } - - /// - /// Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling. - /// - public virtual void Rejected() - { - IsValidated = false; - HasError = false; - } - - /// - /// Marks this context as not validated by the application and assigns various error information properties. - /// HasError becomes true and IsValidated becomes false as a result of calling. - /// - /// Assigned to the Error property - public void SetError(string error) - { - SetError(error, null); - } - - /// - /// Marks this context as not validated by the application and assigns various error information properties. - /// HasError becomes true and IsValidated becomes false as a result of calling. - /// - /// Assigned to the Error property - /// Assigned to the ErrorDescription property - public void SetError(string error, - string errorDescription) - { - SetError(error, errorDescription, null); - } - - /// - /// Marks this context as not validated by the application and assigns various error information properties. - /// HasError becomes true and IsValidated becomes false as a result of calling. - /// - /// Assigned to the Error property - /// Assigned to the ErrorDescription property - /// Assigned to the ErrorUri property - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "error_uri is a string value in the protocol")] - public void SetError(string error, - string errorDescription, - string errorUri) - { - Error = error; - ErrorDescription = errorDescription; - ErrorUri = errorUri; - Rejected(); - HasError = true; - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingTicketContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingTicketContext.cs deleted file mode 100644 index 6d088c8531..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/BaseValidatingTicketContext.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Security.Claims; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Authentication; - -namespace Microsoft.AspNet.Authentication.OAuth -{ - /// - /// Base class used for certain event contexts - /// - public abstract class BaseValidatingTicketContext : BaseValidatingContext - { - /// - /// Initializes base class used for certain event contexts - /// - protected BaseValidatingTicketContext( - HttpContext context, - TOptions options, - AuthenticationTicket ticket) - : base(context, options) - { - Ticket = ticket; - } - - /// - /// Contains the identity and properties for the application to authenticate. If the Validated method - /// is invoked with an AuthenticationTicket or ClaimsIdentity argument, that new value is assigned to - /// this property in addition to changing IsValidated to true. - /// - public AuthenticationTicket Ticket { get; private set; } - - /// - /// Replaces the ticket information on this context and marks it as as validated by the application. - /// IsValidated becomes true and HasError becomes false as a result of calling. - /// - /// Assigned to the Ticket property - /// True if the validation has taken effect. - public bool Validated(AuthenticationTicket ticket) - { - Ticket = ticket; - return Validated(); - } - - /// - /// Alters the ticket information on this context and marks it as as validated by the application. - /// IsValidated becomes true and HasError becomes false as a result of calling. - /// - /// Assigned to the Ticket.Identity property - /// True if the validation has taken effect. - public bool Validated(ClaimsPrincipal principal) - { - AuthenticationProperties properties = Ticket != null ? Ticket.Properties : new AuthenticationProperties(); - // TODO: Ticket can be null, need to revisit - return Validated(new AuthenticationTicket(principal, properties, Ticket.AuthenticationScheme)); - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/IOAuthEvents.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/IOAuthEvents.cs index 82f4bc4155..4c800c9844 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/IOAuthEvents.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Events/IOAuthEvents.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// - public interface IOAuthEvents + public interface IOAuthEvents : IRemoteAuthenticationEvents { /// /// Invoked after the provider successfully authenticates a user. This can be used to retrieve user information. @@ -18,13 +18,6 @@ namespace Microsoft.AspNet.Authentication.OAuth /// A representing the completed operation. Task CreatingTicket(OAuthCreatingTicketContext context); - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - Task SigningIn(SigningInContext context); - /// /// Called when a Challenge causes a redirect to the authorize endpoint. /// diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthChallengeContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthChallengeContext.cs deleted file mode 100644 index b9eccd02f1..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthChallengeContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Authentication.OAuth -{ - /// - /// Specifies the HTTP response header for the bearer authentication scheme. - /// - public class OAuthChallengeContext : BaseContext - { - /// - /// Initializes a new - /// - /// HTTP environment - /// The www-authenticate header value. - public OAuthChallengeContext( - HttpContext context, - string challenge) - : base(context) - { - Challenge = challenge; - } - - /// - /// The www-authenticate header value. - /// - public string Challenge { get; protected set; } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs index 315f89225b..05961d5ee5 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Contains information about the login session as well as the user . /// - public class OAuthCreatingTicketContext : BaseContext + public class OAuthCreatingTicketContext : BaseContext { /// /// Initializes a new . @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Authentication.OAuth HttpClient backchannel, OAuthTokenResponse tokens, JObject user) - : base(context, options) + : base(context) { if (context == null) { @@ -76,8 +76,11 @@ namespace Microsoft.AspNet.Authentication.OAuth TokenResponse = tokens; Backchannel = backchannel; User = user; + Options = options; } + public OAuthOptions Options { get; } + /// /// Gets the JSON-serialized user or an empty /// if it is not available. diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthEvents.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthEvents.cs index 424c982805..07de38640c 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthEvents.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthEvents.cs @@ -9,18 +9,13 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Default implementation. /// - public class OAuthEvents : IOAuthEvents + public class OAuthEvents : RemoteAuthenticationEvents, IOAuthEvents { /// /// Gets or sets the function that is invoked when the CreatingTicket method is invoked. /// public Func OnCreatingTicket { get; set; } = context => Task.FromResult(0); - /// - /// Gets or sets the function that is invoked when the SigningIn method is invoked. - /// - public Func OnSigningIn { get; set; } = context => Task.FromResult(0); - /// /// Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked. /// @@ -37,17 +32,10 @@ namespace Microsoft.AspNet.Authentication.OAuth /// A representing the completed operation. public virtual Task CreatingTicket(OAuthCreatingTicketContext context) => OnCreatingTicket(context); - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// Contains information about the login session as well as the user - /// A representing the completed operation. - public virtual Task SigningIn(SigningInContext context) => OnSigningIn(context); - /// /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware. /// /// Contains redirect URI and of the challenge. public virtual Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context) => OnRedirectToAuthorizationEndpoint(context); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs index 636cb933fe..7cc85f11a1 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Context passed when a Challenge causes a redirect to authorize endpoint in the middleware. /// - public class OAuthRedirectToAuthorizationContext : BaseContext + public class OAuthRedirectToAuthorizationContext : BaseContext { /// /// Creates a new context object. @@ -18,12 +18,15 @@ namespace Microsoft.AspNet.Authentication.OAuth /// The authentication properties of the challenge. /// The initial redirect URI. public OAuthRedirectToAuthorizationContext(HttpContext context, OAuthOptions options, AuthenticationProperties properties, string redirectUri) - : base(context, options) + : base(context) { RedirectUri = redirectUri; Properties = properties; + Options = options; } + public OAuthOptions Options { get; } + /// /// Gets the URI used for the redirect operation. /// diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs index ea6803b496..5db46991bb 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs @@ -12,14 +12,13 @@ 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.Extensions.Logging; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OAuth { - public class OAuthHandler : AuthenticationHandler where TOptions : OAuthOptions + public class OAuthHandler : RemoteAuthenticationHandler where TOptions : OAuthOptions { private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); @@ -30,131 +29,71 @@ namespace Microsoft.AspNet.Authentication.OAuth protected HttpClient Backchannel { get; private set; } - public override async Task InvokeAsync() - { - if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) - { - return await InvokeReturnPathAsync(); - } - return false; - } - - public async Task InvokeReturnPathAsync() - { - var ticket = await HandleAuthenticateOnceAsync(); - if (ticket == null) - { - Logger.LogWarning("Invalid return state, unable to redirect."); - Response.StatusCode = 500; - return true; - } - - var context = new SigningInContext(Context, ticket) - { - SignInScheme = Options.SignInScheme, - RedirectUri = ticket.Properties.RedirectUri, - }; - ticket.Properties.RedirectUri = null; - - await Options.Events.SigningIn(context); - - if (context.SignInScheme != null && context.Principal != null) - { - await Context.Authentication.SignInAsync(context.SignInScheme, context.Principal, context.Properties); - } - - if (!context.IsRequestCompleted && context.RedirectUri != null) - { - if (context.Principal == null) - { - // add a redirect hint that sign-in failed in some way - context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); - } - Response.Redirect(context.RedirectUri); - context.RequestCompleted(); - } - - return context.IsRequestCompleted; - } - - protected override async Task HandleAuthenticateAsync() + protected override async Task HandleRemoteAuthenticateAsync() { AuthenticationProperties properties = null; - try + var query = Request.Query; + + var error = query["error"]; + if (!StringValues.IsNullOrEmpty(error)) { - var query = Request.Query; + return AuthenticateResult.Failed(error); + } - // TODO: Is this a standard error returned by servers? - var value = query["error"]; - if (!StringValues.IsNullOrEmpty(value)) + var code = query["code"]; + var state = query["state"]; + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) + { + return AuthenticateResult.Failed("The oauth state was missing or invalid."); + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties)) + { + return AuthenticateResult.Failed("Correlation failed."); + } + + if (StringValues.IsNullOrEmpty(code)) + { + return AuthenticateResult.Failed("Code was not found."); + } + + var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)); + + if (string.IsNullOrEmpty(tokens.AccessToken)) + { + return AuthenticateResult.Failed("Access token was not found."); + } + + var identity = new ClaimsIdentity(Options.ClaimsIssuer); + + if (Options.SaveTokensAsClaims) + { + identity.AddClaim(new Claim("access_token", tokens.AccessToken, + ClaimValueTypes.String, Options.ClaimsIssuer)); + + if (!string.IsNullOrEmpty(tokens.RefreshToken)) { - Logger.LogVerbose("Remote server returned an error: " + Request.QueryString); - // TODO: Fail request rather than passing through? - return null; - } - - var code = query["code"]; - var state = query["state"]; - - properties = Options.StateDataFormat.Unprotect(state); - if (properties == null) - { - return null; - } - - // OAuth2 10.12 CSRF - if (!ValidateCorrelationId(properties)) - { - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - if (StringValues.IsNullOrEmpty(code)) - { - // Null if the remote server returns an error. - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)); - - if (string.IsNullOrEmpty(tokens.AccessToken)) - { - Logger.LogWarning("Access token was not found"); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - var identity = new ClaimsIdentity(Options.ClaimsIssuer); - - if (Options.SaveTokensAsClaims) - { - identity.AddClaim(new Claim("access_token", tokens.AccessToken, + identity.AddClaim(new Claim("refresh_token", tokens.RefreshToken, ClaimValueTypes.String, Options.ClaimsIssuer)); - - if (!string.IsNullOrEmpty(tokens.RefreshToken)) - { - identity.AddClaim(new Claim("refresh_token", tokens.RefreshToken, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - if (!string.IsNullOrEmpty(tokens.TokenType)) - { - identity.AddClaim(new Claim("token_type", tokens.TokenType, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - if (!string.IsNullOrEmpty(tokens.ExpiresIn)) - { - identity.AddClaim(new Claim("expires_in", tokens.ExpiresIn, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } } - return await CreateTicketAsync(identity, properties, tokens); - } - catch (Exception ex) - { - Logger.LogError("Authentication failed", ex); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); + if (!string.IsNullOrEmpty(tokens.TokenType)) + { + identity.AddClaim(new Claim("token_type", tokens.TokenType, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + if (!string.IsNullOrEmpty(tokens.ExpiresIn)) + { + identity.AddClaim(new Claim("expires_in", tokens.ExpiresIn, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } } + + return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, tokens)); } protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) @@ -176,7 +115,6 @@ namespace Microsoft.AspNet.Authentication.OAuth var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); response.EnsureSuccessStatusCode(); var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - return new OAuthTokenResponse(payload); } @@ -215,7 +153,6 @@ namespace Microsoft.AspNet.Authentication.OAuth GenerateCorrelationId(properties); var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath)); - var redirectContext = new OAuthRedirectToAuthorizationContext( Context, Options, properties, authorizationEndpoint); @@ -223,21 +160,6 @@ namespace Microsoft.AspNet.Authentication.OAuth return 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) { var scope = FormatScope(); diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthMiddleware.cs index d980a5e305..1b318a90a7 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthMiddleware.cs @@ -91,6 +91,11 @@ namespace Microsoft.AspNet.Authentication.OAuth throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.TokenEndpoint))); } + if (!Options.CallbackPath.HasValue) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.CallbackPath))); + } + if (Options.Events == null) { Options.Events = new OAuthEvents(); @@ -112,6 +117,10 @@ namespace Microsoft.AspNet.Authentication.OAuth { Options.SignInScheme = sharedOptions.Value.SignInScheme; } + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.SignInScheme))); + } } protected HttpClient Backchannel { get; private set; } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthOptions.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthOptions.cs index db29438cc0..fa1471bb21 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthOptions.cs @@ -12,8 +12,13 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Configuration options for . /// - public class OAuthOptions : AuthenticationOptions + public class OAuthOptions : RemoteAuthenticationOptions { + public OAuthOptions() + { + Events = new OAuthEvents(); + } + /// /// Gets or sets the provider-assigned client id. /// @@ -41,54 +46,20 @@ namespace Microsoft.AspNet.Authentication.OAuth /// public string UserInformationEndpoint { get; set; } - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string DisplayName - { - get { return Description.DisplayName; } - set { Description.DisplayName = value; } - } - - /// - /// Gets or sets timeout value in milliseconds for back channel communications with the auth provider. - /// - /// - /// The back channel timeout. - /// - public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); - - /// - /// The HttpMessageHandler used to communicate with the auth provider. - /// This cannot be set at the same time as BackchannelCertificateValidator unless the value - /// can be downcast to a WebRequestHandler. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - /// /// Gets or sets the used to handle authentication events. /// - public IOAuthEvents Events { get; set; } = new OAuthEvents(); + public new IOAuthEvents Events + { + get { return (IOAuthEvents)base.Events; } + set { base.Events = value; } + } /// /// A list of permissions to request. /// public IList Scope { get; } = new List(); - /// - /// The request path within the application's base path where the user-agent will be returned. - /// The middleware will process this request when it arrives. - /// - public PathString CallbackPath { get; set; } - - /// - /// Gets or sets the authentication scheme corresponding to the middleware - /// responsible of persisting user's identity after a successful authentication. - /// This value typically corresponds to a cookie middleware registered in the Startup class. - /// When omitted, is used as a fallback value. - /// - public string SignInScheme { get; set; } - /// /// Gets or sets the type used to secure data handled by the middleware. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationCompletedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationCompletedContext.cs deleted file mode 100644 index 820b4f948b..0000000000 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationCompletedContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Authentication.OpenIdConnect -{ - public class AuthenticationCompletedContext : BaseControlContext - { - public AuthenticationCompletedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs index ef5307c469..6c8edb9f41 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs @@ -7,7 +7,7 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNet.Authentication.OpenIdConnect { - public class AuthenticationFailedContext : BaseControlContext + public class AuthenticationFailedContext : BaseOpenIdConnectContext { public AuthenticationFailedContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) @@ -15,7 +15,5 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } public Exception Exception { get; set; } - - public OpenIdConnectMessage ProtocolMessage { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationValidatedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationValidatedContext.cs index 15d5f73046..e38b67b46b 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationValidatedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthenticationValidatedContext.cs @@ -6,15 +6,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNet.Authentication.OpenIdConnect { - public class AuthenticationValidatedContext : BaseControlContext + public class AuthenticationValidatedContext : BaseOpenIdConnectContext { public AuthenticationValidatedContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) { } - public OpenIdConnectMessage ProtocolMessage { get; set; } - public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs index 53e44cdf62..9489e5c25f 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// This Context can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol. /// - public class AuthorizationCodeReceivedContext : BaseControlContext + public class AuthorizationCodeReceivedContext : BaseOpenIdConnectContext { /// /// Creates a @@ -31,11 +31,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public JwtSecurityToken JwtSecurityToken { get; set; } - /// - /// Gets or sets the . - /// - public OpenIdConnectMessage ProtocolMessage { get; set; } - /// /// Gets or sets the 'redirect_uri'. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationResponseReceivedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationResponseReceivedContext.cs index cd2a3c65b9..e433d28744 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationResponseReceivedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/AuthorizationResponseReceivedContext.cs @@ -7,15 +7,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNet.Authentication.OpenIdConnect { - public class AuthorizationResponseReceivedContext : BaseControlContext + public class AuthorizationResponseReceivedContext : BaseOpenIdConnectContext { public AuthorizationResponseReceivedContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) { } - public OpenIdConnectMessage ProtocolMessage { get; set; } - public AuthenticationProperties Properties { get; set; } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs new file mode 100644 index 0000000000..76d63e27b1 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + public class BaseOpenIdConnectContext : BaseControlContext + { + public BaseOpenIdConnectContext(HttpContext context, OpenIdConnectOptions options) + : base(context) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + public OpenIdConnectOptions Options { get; } + + public OpenIdConnectMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs index a5953743be..b6bcfd57dc 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs @@ -8,13 +8,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Specifies events which the invokes to enable developer control over the authentication process. /// - public interface IOpenIdConnectEvents + public interface IOpenIdConnectEvents : IRemoteAuthenticationEvents { - /// - /// Invoked when the authentication process completes. - /// - Task AuthenticationCompleted(AuthenticationCompletedContext context); - /// /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs index ecbf082779..6439257cba 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs @@ -6,15 +6,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNet.Authentication.OpenIdConnect { - public class MessageReceivedContext : BaseControlContext + public class MessageReceivedContext : BaseOpenIdConnectContext { public MessageReceivedContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) { } - public OpenIdConnectMessage ProtocolMessage { get; set; } - /// /// Bearer Token. This will give application an opportunity to retrieve token from an alternation location. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs index a2d65c12b5..c84e5546a1 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs @@ -9,13 +9,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Specifies events which the invokes to enable developer control over the authentication process. /// - public class OpenIdConnectEvents : IOpenIdConnectEvents + public class OpenIdConnectEvents : RemoteAuthenticationEvents, IOpenIdConnectEvents { - /// - /// Invoked when the authentication process completes. - /// - public Func OnAuthenticationCompleted { get; set; } = context => Task.FromResult(0); - /// /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. /// @@ -61,8 +56,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public Func OnUserInformationReceived { get; set; } = context => Task.FromResult(0); - public virtual Task AuthenticationCompleted(AuthenticationCompletedContext context) => OnAuthenticationCompleted(context); - public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); public virtual Task AuthenticationValidated(AuthenticationValidatedContext context) => OnAuthenticationValidated(context); diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/RedirectContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/RedirectContext.cs index 14b168aaad..dcd06843c5 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/RedirectContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/RedirectContext.cs @@ -10,16 +10,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// When a user configures the to be notified prior to redirecting to an IdentityProvider /// an instance of is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events. /// - public class RedirectContext : BaseControlContext + public class RedirectContext : BaseOpenIdConnectContext { public RedirectContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) { } - - /// - /// Gets or sets the . - /// - public OpenIdConnectMessage ProtocolMessage { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs index e4f1bd7a0c..95751f4119 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. /// - public class TokenResponseReceivedContext : BaseControlContext + public class TokenResponseReceivedContext : BaseOpenIdConnectContext { /// /// Creates a @@ -20,11 +20,5 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// Gets or sets the that contains the tokens and json response received after redeeming the code at the token endpoint. /// public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; } - - /// - /// Gets or sets the . - /// - public OpenIdConnectMessage ProtocolMessage { get; set; } - } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs index 27c989bb6d..fa0bc92773 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs @@ -7,15 +7,13 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OpenIdConnect { - public class UserInformationReceivedContext : BaseControlContext + public class UserInformationReceivedContext : BaseOpenIdConnectContext { public UserInformationReceivedContext(HttpContext context, OpenIdConnectOptions options) : base(context, options) { } - public OpenIdConnectMessage ProtocolMessage { get; set; } - public JObject User { get; set; } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index 231c082137..9a62cba956 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. /// - public class OpenIdConnectHandler : AuthenticationHandler + public class OpenIdConnectHandler : RemoteAuthenticationHandler { private const string NonceProperty = "N"; private const string UriSchemeDelimiter = "://"; @@ -263,7 +263,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } Response.Redirect(redirectUri); - return true; } else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) @@ -292,12 +291,10 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect Response.Headers[HeaderNames.Expires] = "-1"; await Response.Body.WriteAsync(buffer, 0, buffer.Length); - return true; } - Logger.LogError("An unsupported authentication method has been configured: {0}", Options.AuthenticationMethod); - return false; + throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); } /// @@ -305,16 +302,10 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// An if successful. /// Uses log id's OIDCH-0000 - OIDCH-0025 - protected override async Task HandleAuthenticateAsync() + protected override async Task HandleRemoteAuthenticateAsync() { Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); - // Allow login to be constrained to a specific path. Need to make this runtime configurable. - if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path)) - { - return null; - } - OpenIdConnectMessage message = null; if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) @@ -326,9 +317,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security if (!string.IsNullOrEmpty(message.IdToken) || !string.IsNullOrEmpty(message.AccessToken)) { - Logger.LogError("An OpenID Connect response cannot contain an identity token " + - "or an access token when using response_mode=query"); - return null; + return AuthenticateResult.Failed("An OpenID Connect response cannot contain an " + + "identity token or an access token when using response_mode=query"); } } // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. @@ -344,7 +334,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (message == null) { - return null; + return AuthenticateResult.Failed("No message."); } try @@ -352,11 +342,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var messageReceivedContext = await RunMessageReceivedEventAsync(message); if (messageReceivedContext.HandledResponse) { - return messageReceivedContext.AuthenticationTicket; + return AuthenticateResult.Success(messageReceivedContext.AuthenticationTicket); } else if (messageReceivedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } message = messageReceivedContext.ProtocolMessage; @@ -365,7 +355,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { // This wasn't a valid ODIC message, it may not have been intended for us. Logger.LogVerbose(Resources.OIDCH_0004_MessageStateIsNullOrEmpty); - return null; + return AuthenticateResult.Failed(Resources.OIDCH_0004_MessageStateIsNullOrEmpty); } // if state exists and we failed to 'unprotect' this is not a message we should process. @@ -373,24 +363,24 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (properties == null) { Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); - return null; + return AuthenticateResult.Failed(Resources.OIDCH_0005_MessageStateIsInvalid); } // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(message.Error)) { + // REVIEW: this error formatting is pretty nuts Logger.LogError(Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"); - throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null")); + return AuthenticateResult.Failed(new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"))); } string userstate = null; properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out userstate); message.State = userstate; - if (!ValidateCorrelationId(properties)) { - return null; + return AuthenticateResult.Failed("Correlation failed."); } if (_configuration == null && Options.ConfigurationManager != null) @@ -409,12 +399,12 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (authorizationResponseReceivedContext.HandledResponse) { Logger.LogVerbose("AuthorizationResponseReceived.HandledResponse"); - return authorizationResponseReceivedContext.AuthenticationTicket; + return AuthenticateResult.Success(authorizationResponseReceivedContext.AuthenticationTicket); } else if (authorizationResponseReceivedContext.Skipped) { Logger.LogVerbose("AuthorizationResponseReceived.Skipped"); - return null; + return AuthenticateResult.Success(ticket: null); } message = authorizationResponseReceivedContext.ProtocolMessage; properties = authorizationResponseReceivedContext.Properties; @@ -430,7 +420,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect else { Logger.LogDebug(Resources.OIDCH_0045_Id_Token_Code_Missing); - return null; + return AuthenticateResult.Failed(Resources.OIDCH_0045_Id_Token_Code_Missing); } } catch (Exception exception) @@ -450,11 +440,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authenticationFailedContext = await RunAuthenticationFailedEventAsync(message, exception); if (authenticationFailedContext.HandledResponse) { - return authenticationFailedContext.AuthenticationTicket; + return AuthenticateResult.Success(authenticationFailedContext.AuthenticationTicket); } else if (authenticationFailedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } throw; @@ -462,7 +452,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } // Authorization Code Flow - private async Task HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties) + private async Task HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties) { AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; @@ -476,11 +466,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(message, properties, ticket, jwt); if (authorizationCodeReceivedContext.HandledResponse) { - return authorizationCodeReceivedContext.AuthenticationTicket; + return AuthenticateResult.Success(authorizationCodeReceivedContext.AuthenticationTicket); } else if (authorizationCodeReceivedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } message = authorizationCodeReceivedContext.ProtocolMessage; var code = authorizationCodeReceivedContext.Code; @@ -493,11 +483,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authorizationCodeRedeemedContext = await RunTokenResponseReceivedEventAsync(message, tokenEndpointResponse); if (authorizationCodeRedeemedContext.HandledResponse) { - return authorizationCodeRedeemedContext.AuthenticationTicket; + return AuthenticateResult.Success(authorizationCodeRedeemedContext.AuthenticationTicket); } else if (authorizationCodeRedeemedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } message = authorizationCodeRedeemedContext.ProtocolMessage; @@ -526,11 +516,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authenticationValidatedContext = await RunAuthenticationValidatedEventAsync(message, ticket, tokenEndpointResponse); if (authenticationValidatedContext.HandledResponse) { - return authenticationValidatedContext.AuthenticationTicket; + return AuthenticateResult.Success(authenticationValidatedContext.AuthenticationTicket); } else if (authenticationValidatedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } ticket = authenticationValidatedContext.AuthenticationTicket; @@ -546,11 +536,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect ticket = await GetUserInformationAsync(tokenEndpointResponse.ProtocolMessage, jwt, ticket); } - return ticket; + return AuthenticateResult.Success(ticket); } // Implicit Flow or Hybrid Flow - private async Task HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties) + private async Task HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties) { Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); @@ -575,11 +565,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authenticationValidatedContext = await RunAuthenticationValidatedEventAsync(message, ticket, tokenEndpointResponse: null); if (authenticationValidatedContext.HandledResponse) { - return authenticationValidatedContext.AuthenticationTicket; + return AuthenticateResult.Success(authenticationValidatedContext.AuthenticationTicket); } else if (authenticationValidatedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } message = authenticationValidatedContext.ProtocolMessage; ticket = authenticationValidatedContext.AuthenticationTicket; @@ -590,11 +580,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(message, properties, ticket, jwt); if (authorizationCodeReceivedContext.HandledResponse) { - return authorizationCodeReceivedContext.AuthenticationTicket; + return AuthenticateResult.Success(authorizationCodeReceivedContext.AuthenticationTicket); } else if (authorizationCodeReceivedContext.Skipped) { - return null; + return AuthenticateResult.Success(ticket: null); } message = authorizationCodeReceivedContext.ProtocolMessage; ticket = authorizationCodeReceivedContext.AuthenticationTicket; @@ -619,7 +609,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } - return ticket; + return AuthenticateResult.Success(ticket); } /// @@ -1135,54 +1125,5 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect return ticket; } - - /// - /// Calls InvokeReplyPathAsync - /// - /// True if the request was handled, false if the next middleware should be invoked. - public override Task InvokeAsync() - { - return InvokeReturnPathAsync(); - } - - private async Task InvokeReturnPathAsync() - { - var ticket = await HandleAuthenticateOnceAsync(); - if (ticket != null) - { - Logger.LogDebug("Authentication completed."); - - var authenticationCompletedContext = new AuthenticationCompletedContext(Context, Options) - { - AuthenticationTicket = ticket, - }; - await Options.Events.AuthenticationCompleted(authenticationCompletedContext); - if (authenticationCompletedContext.HandledResponse) - { - Logger.LogVerbose("The AuthenticationCompleted event returned Handled."); - return true; - } - else if (authenticationCompletedContext.Skipped) - { - Logger.LogVerbose("The AuthenticationCompleted event returned Skipped."); - return false; - } - ticket = authenticationCompletedContext.AuthenticationTicket; - - if (ticket.Principal != null) - { - await Request.HttpContext.Authentication.SignInAsync(Options.SignInScheme, ticket.Principal, ticket.Properties); - } - - // Redirect back to the original secured resource, if any. - if (!string.IsNullOrEmpty(ticket.Properties.RedirectUri)) - { - Response.Redirect(ticket.Properties.RedirectUri); - return true; - } - } - - return false; - } } } diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs index 17b2d3b594..82a393ae4b 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs @@ -77,10 +77,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect throw new ArgumentNullException(nameof(options)); } - if (string.IsNullOrEmpty(Options.SignInScheme) && !string.IsNullOrEmpty(sharedOptions.Value.SignInScheme)) + if (!Options.CallbackPath.HasValue) + { + throw new ArgumentException("Options.CallbackPath must be provided."); + } + + if (string.IsNullOrEmpty(Options.SignInScheme)) { Options.SignInScheme = sharedOptions.Value.SignInScheme; } + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + throw new ArgumentException("Options.SignInScheme is required."); + } if (Options.HtmlEncoder == null) { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectOptions.cs index 91f4e1089e..c79451d225 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.Extensions.WebEncoders; @@ -19,7 +17,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Configuration options for /// - public class OpenIdConnectOptions : AuthenticationOptions + public class OpenIdConnectOptions : RemoteAuthenticationOptions { /// /// Initializes a new @@ -50,6 +48,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { AuthenticationScheme = authenticationScheme; DisplayName = OpenIdConnectDefaults.Caption; + CallbackPath = new PathString("/signin-oidc"); + Events = new OpenIdConnectEvents(); } /// @@ -65,36 +65,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public string Authority { get; set; } - /// - /// The HttpMessageHandler used to retrieve metadata. - /// This cannot be set at the same time as BackchannelCertificateValidator unless the value - /// is a WebRequestHandler. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - - /// - /// Gets or sets the timeout when using the backchannel to make an http call. - /// - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "By design we use the property name in the exception")] - public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); - - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string DisplayName - { - get { return Description.DisplayName; } - set { Description.DisplayName = value; } - } - - /// - /// An optional constrained path on which to process the authentication callback. - /// If not provided and RedirectUri is available, this value will be generated from RedirectUri. - /// - /// If you set this value, then the will only listen for posts at this address. - /// If the IdentityProvider does not post to this address, you may end up in a 401 -> IdentityProvider -> Client -> 401 -> ... - public PathString CallbackPath { get; set; } - /// /// Gets or sets the 'client_id'. /// @@ -136,7 +106,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// Gets or sets the to notify when processing OpenIdConnect messages. /// - public IOpenIdConnectEvents Events { get; set; } = new OpenIdConnectEvents(); + public new IOpenIdConnectEvents Events + { + get { return (IOpenIdConnectEvents)base.Events; } + set { base.Events = value; } + } /// /// Gets or sets the that is used to ensure that the 'id_token' received @@ -194,11 +168,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public IList Scope { get; } = new List { "openid", "profile" }; - /// - /// Gets or sets the SignInScheme which will be used to set the . - /// - public string SignInScheme { get; set; } - /// /// Gets or sets the type used to secure data handled by the middleware. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/Resources.Designer.cs index 5e7b7154ca..f034114845 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/Resources.Designer.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } /// - /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate /// internal static string OIDCH_0029_ChallengContextEqualsNull { @@ -115,7 +115,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } /// - /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate /// internal static string FormatOIDCH_0029_ChallengContextEqualsNull() { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx index 64de5d6a23..4c8eeebd7e 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -136,7 +136,7 @@ OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'. - OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate OIDCH_0030: Using properties.RedirectUri for 'local redirect' post authentication: '{0}'. diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Events/BaseTwitterContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Events/BaseTwitterContext.cs new file mode 100644 index 0000000000..5a2f337581 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Events/BaseTwitterContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Base class for other Twitter contexts. + /// + public class BaseTwitterContext : BaseContext + { + /// + /// Initializes a + /// + /// The HTTP environment + /// The options for Twitter + public BaseTwitterContext(HttpContext context, TwitterOptions options) + : base(context) + { + Options = options; + } + + public TwitterOptions Options { get; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Events/ITwitterEvents.cs b/src/Microsoft.AspNet.Authentication.Twitter/Events/ITwitterEvents.cs index cb793eb994..cea4a99bf6 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/Events/ITwitterEvents.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/Events/ITwitterEvents.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> /// - public interface ITwitterEvents + public interface ITwitterEvents : IRemoteAuthenticationEvents { /// /// Invoked whenever Twitter succesfully authenticates a user @@ -17,13 +17,6 @@ namespace Microsoft.AspNet.Authentication.Twitter /// A representing the completed operation. Task CreatingTicket(TwitterCreatingTicketContext context); - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - Task SigningIn(SigningInContext context); - /// /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware /// diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs index d002554cda..d727eeb152 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Contains information about the login session as well as the user . /// - public class TwitterCreatingTicketContext : BaseContext + public class TwitterCreatingTicketContext : BaseTwitterContext { /// /// Initializes a @@ -22,11 +22,12 @@ namespace Microsoft.AspNet.Authentication.Twitter /// Twitter access token secret public TwitterCreatingTicketContext( HttpContext context, + TwitterOptions options, string userId, string screenName, string accessToken, string accessTokenSecret) - : base(context) + : base(context, options) { UserId = userId; ScreenName = screenName; diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterEvents.cs b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterEvents.cs index f73e4b617f..1941e7990f 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterEvents.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterEvents.cs @@ -9,18 +9,13 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Default implementation. /// - public class TwitterEvents : ITwitterEvents + public class TwitterEvents : RemoteAuthenticationEvents, ITwitterEvents { /// /// Gets or sets the function that is invoked when the Authenticated method is invoked. /// public Func OnCreatingTicket { get; set; } = context => Task.FromResult(0); - /// - /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. - /// - public Func OnSigningIn { get; set; } = context => Task.FromResult(0); - /// /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. /// @@ -37,13 +32,6 @@ namespace Microsoft.AspNet.Authentication.Twitter /// A representing the completed operation. public virtual Task CreatingTicket(TwitterCreatingTicketContext context) => OnCreatingTicket(context); - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - public virtual Task SigningIn(SigningInContext context) => OnSigningIn(context); - /// /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware /// diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs index 4a26ce6ebe..455f522029 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware. /// - public class TwitterRedirectToAuthorizationEndpointContext : BaseContext + public class TwitterRedirectToAuthorizationEndpointContext : BaseTwitterContext { /// /// Creates a new context object. diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs index 38c0897d1d..da92af5485 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNet.Authentication.Twitter { - internal class TwitterHandler : AuthenticationHandler + internal class TwitterHandler : RemoteAuthenticationHandler { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private const string StateCookie = "__TwitterState"; @@ -34,89 +34,70 @@ namespace Microsoft.AspNet.Authentication.Twitter _httpClient = httpClient; } - public override async Task InvokeAsync() - { - if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) - { - return await InvokeReturnPathAsync(); - } - return false; - } - - protected override async Task HandleAuthenticateAsync() + protected override async Task HandleRemoteAuthenticateAsync() { AuthenticationProperties properties = null; - try + var query = Request.Query; + var protectedRequestToken = Request.Cookies[StateCookie]; + + var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); + + if (requestToken == null) { - var query = Request.Query; - var protectedRequestToken = Request.Cookies[StateCookie]; - - var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); - - if (requestToken == null) - { - Logger.LogWarning("Invalid state"); - return null; - } - - properties = requestToken.Properties; - - var returnedToken = query["oauth_token"]; - if (StringValues.IsNullOrEmpty(returnedToken)) - { - Logger.LogWarning("Missing oauth_token"); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) - { - Logger.LogWarning("Unmatched token"); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - var oauthVerifier = query["oauth_verifier"]; - if (StringValues.IsNullOrEmpty(oauthVerifier)) - { - Logger.LogWarning("Missing or blank oauth_verifier"); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; - - Response.Cookies.Delete(StateCookie, cookieOptions); - - var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); - - var identity = new ClaimsIdentity(new[] - { - new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer) - }, - Options.ClaimsIssuer); - - if (Options.SaveTokensAsClaims) - { - identity.AddClaim(new Claim("access_token", accessToken.Token, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - return await CreateTicketAsync(identity, properties, accessToken); + return AuthenticateResult.Failed("Invalid state cookie."); } - catch (Exception ex) + + properties = requestToken.Properties; + + // REVIEW: see which of these are really errors + + var returnedToken = query["oauth_token"]; + if (StringValues.IsNullOrEmpty(returnedToken)) { - Logger.LogError("Authentication failed", ex); - return new AuthenticationTicket(properties, Options.AuthenticationScheme); + return AuthenticateResult.Failed("Missing oauth_token"); } + + if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) + { + return AuthenticateResult.Failed("Unmatched token"); + } + + var oauthVerifier = query["oauth_verifier"]; + if (StringValues.IsNullOrEmpty(oauthVerifier)) + { + return AuthenticateResult.Failed("Missing or blank oauth_verifier"); + } + + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }; + + Response.Cookies.Delete(StateCookie, cookieOptions); + + var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); + + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer) + }, + Options.ClaimsIssuer); + + if (Options.SaveTokensAsClaims) + { + identity.AddClaim(new Claim("access_token", accessToken.Token, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, accessToken)); } protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token) { - var context = new TwitterCreatingTicketContext(Context, token.UserId, token.ScreenName, token.Token, token.TokenSecret) + var context = new TwitterCreatingTicketContext(Context, Options, token.UserId, token.ScreenName, token.Token, token.TokenSecret) { Principal = new ClaimsPrincipal(identity), Properties = properties @@ -145,83 +126,23 @@ namespace Microsoft.AspNet.Authentication.Twitter properties.RedirectUri = CurrentUri; } + // If CallbackConfirmed is false, this will throw var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, BuildRedirectUri(Options.CallbackPath), properties); - if (requestToken.CallbackConfirmed) + var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + + var cookieOptions = new CookieOptions { - var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; - - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; - - Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); - - var redirectContext = new TwitterRedirectToAuthorizationEndpointContext( - Context, Options, - properties, twitterAuthenticationEndpoint); - await Options.Events.RedirectToAuthorizationEndpoint(redirectContext); - return true; - } - else - { - Logger.LogError("requestToken CallbackConfirmed!=true"); - } - return false; // REVIEW: Make sure this should not stop other handlers - } - - public async Task InvokeReturnPathAsync() - { - var model = await HandleAuthenticateOnceAsync(); - if (model == null) - { - Logger.LogWarning("Invalid return state, unable to redirect."); - Response.StatusCode = 500; - return true; - } - - var context = new SigningInContext(Context, model) - { - SignInScheme = Options.SignInScheme, - RedirectUri = model.Properties.RedirectUri + HttpOnly = true, + Secure = Request.IsHttps }; - model.Properties.RedirectUri = null; - await Options.Events.SigningIn(context); + Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); - if (context.SignInScheme != null && context.Principal != null) - { - await Context.Authentication.SignInAsync(context.SignInScheme, context.Principal, context.Properties); - } - - if (!context.IsRequestCompleted && context.RedirectUri != null) - { - if (context.Principal == null) - { - // add a redirect hint that sign-in failed in some way - context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); - } - Response.Redirect(context.RedirectUri); - context.RequestCompleted(); - } - - 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(); + var redirectContext = new TwitterRedirectToAuthorizationEndpointContext( + Context, Options, + properties, twitterAuthenticationEndpoint); + await Options.Events.RedirectToAuthorizationEndpoint(redirectContext); + return true; } private async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) @@ -275,12 +196,12 @@ namespace Microsoft.AspNet.Authentication.Twitter string responseText = await response.Content.ReadAsStringAsync(); var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); - if (string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) + if (!string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) { - return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; + throw new Exception("Twitter oauth_callback_confirmed is not true."); } - return new RequestToken(); + return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; } private async Task ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier) diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterMiddleware.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterMiddleware.cs index ec75f449ec..2eeb010c53 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterMiddleware.cs @@ -78,6 +78,10 @@ namespace Microsoft.AspNet.Authentication.Twitter { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ConsumerKey))); } + if (!Options.CallbackPath.HasValue) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.CallbackPath))); + } if (Options.Events == null) { diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterOptions.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterOptions.cs index f057de36a8..dae6fa6b20 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterOptions.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Options for the Twitter authentication middleware. /// - public class TwitterOptions : AuthenticationOptions + public class TwitterOptions : RemoteAuthenticationOptions { /// /// Initializes a new instance of the class. @@ -21,6 +21,7 @@ namespace Microsoft.AspNet.Authentication.Twitter DisplayName = AuthenticationScheme; CallbackPath = new PathString("/signin-twitter"); BackchannelTimeout = TimeSpan.FromSeconds(60); + Events = new TwitterEvents(); } /// @@ -35,45 +36,6 @@ namespace Microsoft.AspNet.Authentication.Twitter /// The consumer secret used to sign requests to Twitter. public string ConsumerSecret { get; set; } - /// - /// Gets or sets timeout value in milliseconds for back channel communications with Twitter. - /// - /// - /// The back channel timeout. - /// - public TimeSpan BackchannelTimeout { get; set; } - - /// - /// The HttpMessageHandler used to communicate with Twitter. - /// This cannot be set at the same time as BackchannelCertificateValidator unless the value - /// can be downcast to a WebRequestHandler. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string DisplayName - { - get { return Description.DisplayName; } - set { Description.DisplayName = value; } - } - - /// - /// The request path within the application's base path where the user-agent will be returned. - /// The middleware will process this request when it arrives. - /// Default value is "/signin-twitter". - /// - public PathString CallbackPath { get; set; } - - /// - /// Gets or sets the authentication scheme corresponding to the middleware - /// responsible of persisting user's identity after a successful authentication. - /// This value typically corresponds to a cookie middleware registered in the Startup class. - /// When omitted, is used as a fallback value. - /// - public string SignInScheme { get; set; } - /// /// Gets or sets the type used to secure data handled by the middleware. /// @@ -82,7 +44,11 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Gets or sets the used to handle authentication events. /// - public ITwitterEvents Events { get; set; } + public new ITwitterEvents Events + { + get { return (ITwitterEvents)base.Events; } + set { base.Events = value; } + } /// /// Defines whether access tokens should be stored in the diff --git a/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs b/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs new file mode 100644 index 0000000000..4f733fe7fd --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Contains the result of an Authenticate call + /// + public class AuthenticateResult + { + private AuthenticateResult() { } + + /// + /// If a ticket was produced, authenticate was successful. + /// + public bool Succeeded + { + get + { + return Ticket != null; + } + } + + /// + /// The authentication ticket. + /// + public AuthenticationTicket Ticket { get; private set; } + + /// + /// Holds error information caused by authentication. + /// + public Exception Error { get; private set; } + + public static AuthenticateResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new AuthenticateResult() { Ticket = ticket }; + } + + public static AuthenticateResult Failed(Exception error) + { + return new AuthenticateResult() { Error = error }; + } + + public static AuthenticateResult Failed(string errorMessage) + { + return new AuthenticateResult() { Error = new Exception(errorMessage) }; + } + + } +} diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index 2b256ee2bc..c55dc50d77 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -17,7 +18,7 @@ namespace Microsoft.AspNet.Authentication /// Specifies which type for of AuthenticationOptions property public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationOptions { - private Task _authenticateTask; + private Task _authenticateTask; private bool _finishCalled; protected bool SignInAccepted { get; set; } @@ -96,10 +97,10 @@ namespace Microsoft.AspNet.Authentication Response.OnStarting(OnStartingCallback, this); - // Automatic authentication is the empty scheme - if (ShouldHandleScheme(string.Empty)) + if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) { - var ticket = await HandleAuthenticateOnceAsync(); + var result = await HandleAuthenticateOnceAsync(); + var ticket = result?.Ticket; if (ticket?.Principal != null) { Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal); @@ -139,7 +140,7 @@ namespace Microsoft.AspNet.Authentication private async Task HandleAutomaticChallengeIfNeeded() { - if (!ChallengeCalled && Options.AutomaticAuthentication && Response.StatusCode == 401) + if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401) { await HandleUnauthorizedAsync(new ChallengeContext(Options.AuthenticationScheme)); } @@ -169,7 +170,7 @@ namespace Microsoft.AspNet.Authentication /// Returning false will cause the common code to call the next middleware in line. Returning true will /// cause the common code to begin the async completion journey without calling the rest of the middleware /// pipeline. - public virtual Task InvokeAsync() + public virtual Task HandleRequestAsync() { return Task.FromResult(false); } @@ -184,35 +185,46 @@ namespace Microsoft.AspNet.Authentication } } - public bool ShouldHandleScheme(string authenticationScheme) + public bool ShouldHandleScheme(string authenticationScheme, bool handleAutomatic) { return string.Equals(Options.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || - (Options.AutomaticAuthentication && string.IsNullOrEmpty(authenticationScheme)); + (handleAutomatic && string.Equals(authenticationScheme, AuthenticationManager.AutomaticScheme, StringComparison.Ordinal)); } public async Task AuthenticateAsync(AuthenticateContext context) { - if (ShouldHandleScheme(context.AuthenticationScheme)) + var handled = false; + if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticAuthenticate)) { // Calling Authenticate more than once should always return the original value. - var ticket = await HandleAuthenticateOnceAsync(); - if (ticket?.Principal != null) + var result = await HandleAuthenticateOnceAsync(); + + if (result?.Error != null) { - context.Authenticated(ticket.Principal, ticket.Properties.Items, Options.Description.Items); + context.Failed(result.Error); } else { - context.NotAuthenticated(); + var ticket = result?.Ticket; + if (ticket?.Principal != null) + { + context.Authenticated(ticket.Principal, ticket.Properties.Items, Options.Description.Items); + handled = true; + } + else + { + context.NotAuthenticated(); + } } } - if (PriorHandler != null) + if (PriorHandler != null && !handled) { await PriorHandler.AuthenticateAsync(context); } } - protected Task HandleAuthenticateOnceAsync() + protected Task HandleAuthenticateOnceAsync() { if (_authenticateTask == null) { @@ -221,18 +233,17 @@ namespace Microsoft.AspNet.Authentication return _authenticateTask; } - protected abstract Task HandleAuthenticateAsync(); + protected abstract Task HandleAuthenticateAsync(); public async Task SignInAsync(SignInContext context) { - if (ShouldHandleScheme(context.AuthenticationScheme)) + if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) { SignInAccepted = true; await HandleSignInAsync(context); context.Accept(); } - - if (PriorHandler != null) + else if (PriorHandler != null) { await PriorHandler.SignInAsync(context); } @@ -245,14 +256,13 @@ namespace Microsoft.AspNet.Authentication public async Task SignOutAsync(SignOutContext context) { - if (ShouldHandleScheme(context.AuthenticationScheme)) + if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) { SignOutAccepted = true; await HandleSignOutAsync(context); context.Accept(); } - - if (PriorHandler != null) + else if (PriorHandler != null) { await PriorHandler.SignOutAsync(context); } @@ -263,10 +273,6 @@ namespace Microsoft.AspNet.Authentication return Task.FromResult(0); } - /// - /// - /// - /// True if no other handlers should be called protected virtual Task HandleForbiddenAsync(ChallengeContext context) { Response.StatusCode = 403; @@ -288,24 +294,20 @@ namespace Microsoft.AspNet.Authentication public async Task ChallengeAsync(ChallengeContext context) { - bool handled = false; ChallengeCalled = true; - if (ShouldHandleScheme(context.AuthenticationScheme)) + var handled = false; + if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticChallenge)) { switch (context.Behavior) { case ChallengeBehavior.Automatic: // If there is a principal already, invoke the forbidden code path - var ticket = await HandleAuthenticateOnceAsync(); - if (ticket?.Principal != null) + var result = await HandleAuthenticateOnceAsync(); + if (result?.Ticket?.Principal != null) { - handled = await HandleForbiddenAsync(context); + goto case ChallengeBehavior.Forbidden; } - else - { - handled = await HandleUnauthorizedAsync(context); - } - break; + goto case ChallengeBehavior.Unauthorized; case ChallengeBehavior.Unauthorized: handled = await HandleUnauthorizedAsync(context); break; diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs index ebf25c2174..0f26048bbc 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNet.Authentication await handler.InitializeAsync(Options, context, Logger, UrlEncoder); try { - if (!await handler.InvokeAsync()) + if (!await handler.HandleRequestAsync()) { await _next(context); } @@ -84,7 +84,6 @@ namespace Microsoft.AspNet.Authentication } throw; } - await handler.TeardownAsync(); } protected abstract AuthenticationHandler CreateHandler(); diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs index 54f8a7a352..7583642443 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs @@ -27,11 +27,16 @@ namespace Microsoft.AspNet.Authentication } /// - /// If true the authentication middleware alter the request user coming in and - /// alter 401 Unauthorized responses going out. If false the authentication middleware will only provide - /// identity and alter responses when explicitly indicated by the AuthenticationScheme. + /// If true the authentication middleware alter the request user coming in. If false the authentication middleware will only provide + /// identity when explicitly indicated by the AuthenticationScheme. /// - public bool AutomaticAuthentication { get; set; } + public bool AutomaticAuthenticate { get; set; } + + /// + /// If true the authentication middleware should handle automatic challenge. + /// If false the authentication middleware will only alter responses when explicitly indicated by the AuthenticationScheme. + /// + public bool AutomaticChallenge { get; set; } /// /// Gets or sets the issuer that should be used for any claims that are created diff --git a/src/Microsoft.AspNet.Authentication/Events/BaseContext`1.cs b/src/Microsoft.AspNet.Authentication/Events/BaseContext`1.cs deleted file mode 100644 index 24a8ef53d4..0000000000 --- a/src/Microsoft.AspNet.Authentication/Events/BaseContext`1.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Authentication -{ - /// - /// Base class used for certain event contexts - /// - public abstract class BaseContext - { - protected BaseContext(HttpContext context, TOptions options) - { - HttpContext = context; - Options = options; - } - - public HttpContext HttpContext { get; private set; } - - public TOptions Options { get; private set; } - - public HttpRequest Request - { - get { return HttpContext.Request; } - } - - public HttpResponse Response - { - get { return HttpContext.Response; } - } - } -} diff --git a/src/Microsoft.AspNet.Authentication/Events/BaseControlContext.cs b/src/Microsoft.AspNet.Authentication/Events/BaseControlContext.cs index 1f83f062e1..5e28dfc6c1 100644 --- a/src/Microsoft.AspNet.Authentication/Events/BaseControlContext.cs +++ b/src/Microsoft.AspNet.Authentication/Events/BaseControlContext.cs @@ -5,9 +5,9 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Authentication { - public class BaseControlContext : BaseContext + public class BaseControlContext : BaseContext { - protected BaseControlContext(HttpContext context, TOptions options) : base(context, options) + protected BaseControlContext(HttpContext context) : base(context) { } diff --git a/src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs b/src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs new file mode 100644 index 0000000000..a8ef4b5944 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Provides error context information to middleware providers. + /// + public class ErrorContext : BaseControlContext + { + public ErrorContext(HttpContext context, Exception error) + : base(context) + { + Error = error; + } + + /// + /// User friendly error message for the error. + /// + public Exception Error { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs new file mode 100644 index 0000000000..e19fd10b28 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication +{ + public interface IRemoteAuthenticationEvents + { + /// + /// Invoked when the remote authentication process has an error. + /// + Task RemoteError(ErrorContext context); + + /// + /// Invoked before sign in. + /// + Task TicketReceived(TicketReceivedContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs new file mode 100644 index 0000000000..fce53b9927 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication +{ + public class RemoteAuthenticationEvents : IRemoteAuthenticationEvents + { + public Func OnRemoteError { get; set; } = context => Task.FromResult(0); + + public Func OnTicketReceived { get; set; } = context => Task.FromResult(0); + + /// + /// Invoked when there is a remote error + /// + public virtual Task RemoteError(ErrorContext context) => OnRemoteError(context); + + /// + /// Invoked after the remote ticket has been recieved. + /// + public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Events/SigningInContext.cs b/src/Microsoft.AspNet.Authentication/Events/TicketReceivedContext.cs similarity index 56% rename from src/Microsoft.AspNet.Authentication/Events/SigningInContext.cs rename to src/Microsoft.AspNet.Authentication/Events/TicketReceivedContext.cs index 30d38ae7f7..2021209820 100644 --- a/src/Microsoft.AspNet.Authentication/Events/SigningInContext.cs +++ b/src/Microsoft.AspNet.Authentication/Events/TicketReceivedContext.cs @@ -1,7 +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.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; @@ -11,13 +10,12 @@ namespace Microsoft.AspNet.Authentication /// /// Provides context information to middleware providers. /// - public class SigningInContext : BaseContext + public class TicketReceivedContext : BaseControlContext { - public SigningInContext( - HttpContext context, - AuthenticationTicket ticket) + public TicketReceivedContext(HttpContext context, RemoteAuthenticationOptions options, AuthenticationTicket ticket) : base(context) { + Options = options; if (ticket != null) { Principal = ticket.Principal; @@ -27,17 +25,8 @@ namespace Microsoft.AspNet.Authentication public ClaimsPrincipal Principal { get; set; } public AuthenticationProperties Properties { get; set; } + public RemoteAuthenticationOptions Options { get; set; } - public bool IsRequestCompleted { get; private set; } - - public void RequestCompleted() - { - IsRequestCompleted = true; - } - - public string SignInScheme { get; set; } - - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "By design")] - public string RedirectUri { get; set; } + public string ReturnUri { get; set; } } } diff --git a/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs new file mode 100644 index 0000000000..d9563e628f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Authentication +{ + public abstract class RemoteAuthenticationHandler : AuthenticationHandler where TOptions : RemoteAuthenticationOptions + { + public override async Task HandleRequestAsync() + { + if (Options.CallbackPath == Request.Path) + { + return await HandleRemoteCallbackAsync(); + } + return false; + } + + protected virtual async Task HandleRemoteCallbackAsync() + { + var authResult = await HandleRemoteAuthenticateAsync(); + if (authResult == null || !authResult.Succeeded) + { + var errorContext = new ErrorContext(Context, authResult?.Error ?? new Exception("Invalid return state, unable to redirect.")); + Logger.LogInformation("Error from RemoteAuthentication: " + errorContext.Error.Message); + await Options.Events.RemoteError(errorContext); + if (errorContext.HandledResponse) + { + return true; + } + if (errorContext.Skipped) + { + return false; + } + + Context.Response.StatusCode = 500; + return true; + } + + // We have a ticket if we get here + var ticket = authResult.Ticket; + var context = new TicketReceivedContext(Context, Options, ticket) + { + ReturnUri = ticket.Properties.RedirectUri, + }; + // REVIEW: is this safe or good? + ticket.Properties.RedirectUri = null; + + await Options.Events.TicketReceived(context); + + if (context.HandledResponse) + { + Logger.LogVerbose("The SigningIn event returned Handled."); + return true; + } + else if (context.Skipped) + { + Logger.LogVerbose("The SigningIn event returned Skipped."); + return false; + } + + if (context.Principal != null) + { + await Context.Authentication.SignInAsync(Options.SignInScheme, context.Principal, context.Properties); + } + + if (context.ReturnUri != null) + { + Response.Redirect(context.ReturnUri); + return true; + } + + return false; + } + + protected abstract Task HandleRemoteAuthenticateAsync(); + + protected override Task HandleAuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.Failed("Remote authentication does not support authenticate")); + } + + 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(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/RemoteAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationOptions.cs new file mode 100644 index 0000000000..f5dad7267f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationOptions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication +{ + public class RemoteAuthenticationOptions : AuthenticationOptions + { + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Twitter. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// The HttpMessageHandler used to communicate with Twitter. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the authentication scheme corresponding to the middleware + /// responsible of persisting user's identity after a successful authentication. + /// This value typically corresponds to a cookie middleware registered in the Startup class. + /// When omitted, is used as a fallback value. + /// + public string SignInScheme { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string DisplayName + { + get { return Description.DisplayName; } + set { Description.DisplayName = value; } + } + + public IRemoteAuthenticationEvents Events = new RemoteAuthenticationEvents(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/project.json b/src/Microsoft.AspNet.Authentication/project.json index c4132c0be1..d82988e82b 100644 --- a/src/Microsoft.AspNet.Authentication/project.json +++ b/src/Microsoft.AspNet.Authentication/project.json @@ -18,7 +18,15 @@ "Microsoft.Extensions.WebEncoders": "1.0.0-*" }, "frameworks": { - "dnx451": { }, - "dnxcore50": { } + "dnx451": { + "frameworkAssemblies": { + "System.Net.Http": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Net.Http": "4.0.1-beta-*" + } + } } } diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs index bb7af889bc..2d8cf315d4 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs @@ -9,16 +9,16 @@ namespace Microsoft.AspNet.Authorization { public class AuthorizationPolicy { - public AuthorizationPolicy(IEnumerable requirements, IEnumerable activeAuthenticationSchemes) + public AuthorizationPolicy(IEnumerable requirements, IEnumerable authenticationSchemes) { if (requirements == null) { throw new ArgumentNullException(nameof(requirements)); } - if (activeAuthenticationSchemes == null) + if (authenticationSchemes == null) { - throw new ArgumentNullException(nameof(activeAuthenticationSchemes)); + throw new ArgumentNullException(nameof(authenticationSchemes)); } if (requirements.Count() == 0) @@ -26,11 +26,11 @@ namespace Microsoft.AspNet.Authorization throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty); } Requirements = new List(requirements).AsReadOnly(); - ActiveAuthenticationSchemes = new List(activeAuthenticationSchemes).AsReadOnly(); + AuthenticationSchemes = new List(authenticationSchemes).AsReadOnly(); } public IReadOnlyList Requirements { get; } - public IReadOnlyList ActiveAuthenticationSchemes { get; } + public IReadOnlyList AuthenticationSchemes { get; } public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies) { @@ -96,7 +96,7 @@ namespace Microsoft.AspNet.Authorization { foreach (var authType in authTypesSplit) { - policyBuilder.ActiveAuthenticationSchemes.Add(authType); + policyBuilder.AuthenticationSchemes.Add(authType); } } if (useDefaultPolicy) diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs index 331317fef6..aebd49e662 100644 --- a/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs @@ -9,9 +9,9 @@ namespace Microsoft.AspNet.Authorization { public class AuthorizationPolicyBuilder { - public AuthorizationPolicyBuilder(params string[] activeAuthenticationSchemes) + public AuthorizationPolicyBuilder(params string[] authenticationSchemes) { - AddAuthenticationSchemes(activeAuthenticationSchemes); + AddAuthenticationSchemes(authenticationSchemes); } public AuthorizationPolicyBuilder(AuthorizationPolicy policy) @@ -20,13 +20,13 @@ namespace Microsoft.AspNet.Authorization } public IList Requirements { get; set; } = new List(); - public IList ActiveAuthenticationSchemes { get; set; } = new List(); + public IList AuthenticationSchemes { get; set; } = new List(); - public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] activeAuthTypes) + public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes) { - foreach (var authType in activeAuthTypes) + foreach (var authType in schemes) { - ActiveAuthenticationSchemes.Add(authType); + AuthenticationSchemes.Add(authType); } return this; } @@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Authorization throw new ArgumentNullException(nameof(policy)); } - AddAuthenticationSchemes(policy.ActiveAuthenticationSchemes.ToArray()); + AddAuthenticationSchemes(policy.AuthenticationSchemes.ToArray()); AddRequirements(policy.Requirements.ToArray()); return this; } @@ -135,7 +135,7 @@ namespace Microsoft.AspNet.Authorization public AuthorizationPolicy Build() { - return new AuthorizationPolicy(Requirements, ActiveAuthenticationSchemes.Distinct()); + return new AuthorizationPolicy(Requirements, AuthenticationSchemes.Distinct()); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs index b39693103f..e8423dbba1 100644 --- a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs +++ b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.Http.Internal; @@ -19,10 +20,10 @@ namespace Microsoft.AspNet.Authentication public async Task ShouldHandleSchemeAreDeterminedOnlyByMatchingAuthenticationScheme() { var handler = await TestHandler.Create("Alpha"); - var passiveNoMatch = handler.ShouldHandleScheme("Beta"); + var passiveNoMatch = handler.ShouldHandleScheme("Beta", handleAutomatic: false); handler = await TestHandler.Create("Alpha"); - var passiveWithMatch = handler.ShouldHandleScheme("Alpha"); + var passiveWithMatch = handler.ShouldHandleScheme("Alpha", handleAutomatic: false); Assert.False(passiveNoMatch); Assert.True(passiveWithMatch); @@ -32,47 +33,37 @@ namespace Microsoft.AspNet.Authentication public async Task AutomaticHandlerInAutomaticModeHandlesEmptyChallenges() { var handler = await TestAutoHandler.Create("ignored", true); - Assert.True(handler.ShouldHandleScheme("")); + Assert.True(handler.ShouldHandleScheme(AuthenticationManager.AutomaticScheme, handleAutomatic: true)); } - [Fact] - public async Task AutomaticHandlerHandlesNullScheme() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("notmatched")] + public async Task AutomaticHandlerDoesNotHandleSchemes(string scheme) { var handler = await TestAutoHandler.Create("ignored", true); - Assert.True(handler.ShouldHandleScheme(null)); - } - - [Fact] - public async Task AutomaticHandlerIgnoresWhitespaceScheme() - { - var handler = await TestAutoHandler.Create("ignored", true); - Assert.False(handler.ShouldHandleScheme(" ")); + Assert.False(handler.ShouldHandleScheme(scheme, handleAutomatic: true)); } [Fact] public async Task AutomaticHandlerShouldHandleSchemeWhenSchemeMatches() { var handler = await TestAutoHandler.Create("Alpha", true); - Assert.True(handler.ShouldHandleScheme("Alpha")); - } - - [Fact] - public async Task AutomaticHandlerShouldNotHandleChallengeWhenSchemeDoesNotMatches() - { - var handler = await TestAutoHandler.Create("Dog", true); - Assert.False(handler.ShouldHandleScheme("Alpha")); + Assert.True(handler.ShouldHandleScheme("Alpha", handleAutomatic: true)); } [Fact] public async Task AutomaticHandlerShouldNotHandleChallengeWhenSchemesNotEmpty() { var handler = await TestAutoHandler.Create(null, true); - Assert.False(handler.ShouldHandleScheme("Alpha")); + Assert.False(handler.ShouldHandleScheme("Alpha", handleAutomatic: true)); } [Theory] [InlineData("Alpha")] - [InlineData("")] + [InlineData("Automatic")] public async Task AuthHandlerAuthenticateCachesTicket(string scheme) { var handler = await CountHandler.Create(scheme); @@ -100,14 +91,14 @@ namespace Microsoft.AspNet.Authentication new LoggerFactory().CreateLogger("CountHandler"), Extensions.WebEncoders.UrlEncoder.Default); handler.Options.AuthenticationScheme = scheme; - handler.Options.AutomaticAuthentication = true; + handler.Options.AutomaticAuthenticate = true; return handler; } - protected override Task HandleAuthenticateAsync() + protected override Task HandleAuthenticateAsync() { AuthCount++; - return Task.FromResult(new AuthenticationTicket(null, null)); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new Http.Authentication.AuthenticationProperties(), "whatever"))); } } @@ -129,9 +120,9 @@ namespace Microsoft.AspNet.Authentication return handler; } - protected override Task HandleAuthenticateAsync() + protected override Task HandleAuthenticateAsync() { - return Task.FromResult(null); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new Http.Authentication.AuthenticationProperties(), "whatever"))); } } @@ -141,7 +132,7 @@ namespace Microsoft.AspNet.Authentication { public TestAutoOptions() { - AutomaticAuthentication = true; + AutomaticAuthenticate = true; } } @@ -159,13 +150,13 @@ namespace Microsoft.AspNet.Authentication new LoggerFactory().CreateLogger("TestAutoHandler"), Extensions.WebEncoders.UrlEncoder.Default); handler.Options.AuthenticationScheme = scheme; - handler.Options.AutomaticAuthentication = auto; + handler.Options.AutomaticAuthenticate = auto; return handler; } - protected override Task HandleAuthenticateAsync() + protected override Task HandleAuthenticateAsync() { - return Task.FromResult(null); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new Http.Authentication.AuthenticationProperties(), "whatever"))); } } diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index 5a38ca1ad1..82bb7a90fc 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Authentication.Cookies var server = CreateServer(options => { options.LoginPath = new PathString("/login"); - options.AutomaticAuthentication = auto; + options.AutomaticChallenge = auto; }); var transaction = await SendAsync(server, "http://example.com/protected"); @@ -60,7 +60,7 @@ namespace Microsoft.AspNet.Authentication.Cookies [Fact] public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() { - var server = CreateServer(options => options.AutomaticAuthentication = true); + var server = CreateServer(options => options.AutomaticChallenge = true); var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); @@ -573,7 +573,7 @@ namespace Microsoft.AspNet.Authentication.Cookies var clock = new TestClock(); var server = CreateServer(options => { - options.AutomaticAuthentication = automatic; + options.AutomaticAuthenticate = automatic; options.SystemClock = clock; }, SignInAsAlice); @@ -596,7 +596,7 @@ namespace Microsoft.AspNet.Authentication.Cookies var clock = new TestClock(); var server = CreateServer(options => { - options.AutomaticAuthentication = automatic; + options.AutomaticAuthenticate = automatic; options.SystemClock = clock; }, SignInAsAlice); @@ -617,7 +617,7 @@ namespace Microsoft.AspNet.Authentication.Cookies var clock = new TestClock(); var server = CreateServer(options => { - options.AutomaticAuthentication = automatic; + options.AutomaticAuthenticate = automatic; options.SystemClock = clock; }, SignInAsAlice); @@ -1002,10 +1002,7 @@ namespace Microsoft.AspNet.Authentication.Cookies } }); }, - services => - { - services.AddAuthentication(); - }); + services => services.AddAuthentication()); server.BaseAddress = baseAddress; return server; } diff --git a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs index efab3c54d1..ae7e6ce652 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs @@ -18,6 +18,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.WebEncoders; using Newtonsoft.Json; using Xunit; +using System.Diagnostics; +using Microsoft.AspNet.Authentication.Cookies; namespace Microsoft.AspNet.Authentication.Facebook { @@ -45,7 +47,7 @@ namespace Microsoft.AspNet.Authentication.Facebook app.UseCookieAuthentication(options => { options.AuthenticationScheme = "External"; - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); }, services => @@ -158,11 +160,12 @@ namespace Microsoft.AspNet.Authentication.Facebook public async Task CustomUserInfoEndpointHasValidGraphQuery() { var customUserInfoEndpoint = "https://graph.facebook.com/me?fields=email,timezone,picture"; - string finalUserInfoEndpoint = string.Empty; + var finalUserInfoEndpoint = string.Empty; var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("FacebookTest")); var server = CreateServer( app => { + app.UseCookieAuthentication(); app.UseFacebookAuthentication(options => { options.AppId = "Test App Id"; @@ -200,11 +203,10 @@ namespace Microsoft.AspNet.Authentication.Facebook } }; }); - app.UseCookieAuthentication(); }, services => { - services.AddAuthentication(); + services.AddAuthentication(options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); }, handler: null); var properties = new AuthenticationProperties(); diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs index b6335093c1..48d513ad7b 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -13,6 +13,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.WebEncoders; @@ -88,7 +89,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; }); var transaction = await server.SendAsync("https://example.com/401"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); @@ -119,7 +120,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; }); var transaction = await server.SendAsync("https://example.com/401"); Assert.Contains(".AspNet.Correlation.Google=", transaction.SetCookie.Single()); @@ -146,7 +147,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; }); var transaction = await server.SendAsync("https://example.com/401"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); @@ -161,7 +162,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; }, context => { @@ -212,6 +213,29 @@ namespace Microsoft.AspNet.Authentication.Google Assert.Contains("custom=test", query); } + [Fact] + public async Task AuthenticateWillFail() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }, + async context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/auth")) + { + var auth = new AuthenticateContext("Google"); + await context.Authentication.AuthenticateAsync(auth); + Assert.NotNull(auth.Error); + } + }); + var transaction = await server.SendAsync("https://example.com/auth"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + [Fact] public async Task ReplyPathWithoutStateQueryStringWillBeRejected() { @@ -224,6 +248,40 @@ namespace Microsoft.AspNet.Authentication.Google Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWithErrorFails(bool redirect) + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + if (redirect) + { + options.Events = new OAuthEvents() + { + OnRemoteError = ctx => + { + ctx.Response.Redirect("/error?ErrorMessage=" + ctx.Error.Message); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + } + }); + var transaction = await server.SendAsync("https://example.com/signin-google?error=OMG"); + if (redirect) + { + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?ErrorMessage=OMG", transaction.Response.Headers.GetValues("Location").First()); + } + else + { + Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); + } + } + [Theory] [InlineData(null)] [InlineData("CustomIssuer")] @@ -305,8 +363,11 @@ namespace Microsoft.AspNet.Authentication.Google Assert.Equal("yup", transaction.FindClaimValue("xform")); } - [Fact] - public async Task ReplyPathWillRejectIfCodeIsInvalid() + // REVIEW: Fix this once we revisit error handling to not blow up + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect) { var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); var server = CreateServer(options => @@ -321,22 +382,50 @@ namespace Microsoft.AspNet.Authentication.Google return new HttpResponseMessage(HttpStatusCode.BadRequest); } }; + if (redirect) + { + options.Events = new OAuthEvents() + { + OnRemoteError = ctx => + { + ctx.Response.Redirect("/error?ErrorMessage=" + ctx.Error.Message); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + } }); var properties = new AuthenticationProperties(); var correlationKey = ".AspNet.Correlation.Google"; var correlationValue = "TestCorrelationId"; properties.Items.Add(correlationKey, correlationValue); properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( + + await Assert.ThrowsAsync(() => server.SendAsync( "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), - correlationKey + "=" + correlationValue); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Contains("error=access_denied", transaction.Response.Headers.Location.ToString()); + correlationKey + "=" + correlationValue)); + + //var transaction = await server.SendAsync( + // "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + // correlationKey + "=" + correlationValue); + //if (redirect) + //{ + // Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + // Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.UrlEncode("Access token was not found."), + // transaction.Response.Headers.GetValues("Location").First()); + //} + //else + //{ + // Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); + //} } - [Fact] - public async Task ReplyPathWillRejectIfAccessTokenIsMissing() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect) { var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); var server = CreateServer(options => @@ -351,6 +440,18 @@ namespace Microsoft.AspNet.Authentication.Google return ReturnJsonResponse(new object()); } }; + if (redirect) + { + options.Events = new OAuthEvents() + { + OnRemoteError = ctx => + { + ctx.Response.Redirect("/error?ErrorMessage=" + ctx.Error.Message); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + } }); var properties = new AuthenticationProperties(); var correlationKey = ".AspNet.Correlation.Google"; @@ -361,8 +462,16 @@ namespace Microsoft.AspNet.Authentication.Google var transaction = await server.SendAsync( "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), correlationKey + "=" + correlationValue); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Contains("error=access_denied", transaction.Response.Headers.Location.ToString()); + if (redirect) + { + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.UrlEncode("Access token was not found."), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); + } } [Fact] @@ -420,7 +529,7 @@ namespace Microsoft.AspNet.Authentication.Google { var refreshToken = context.RefreshToken; context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); - return Task.FromResult(null); + return Task.FromResult(0); } }; }); @@ -529,6 +638,50 @@ namespace Microsoft.AspNet.Authentication.Google Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First()); } + [Fact] + public async Task NoStateCauses500() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode"); + + Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); + } + + [Fact] + public async Task CanRedirectOnError() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.Events = new OAuthEvents() + { + OnRemoteError = ctx => + { + ctx.Response.Redirect("/error?ErrorMessage=" + ctx.Error.Message); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + }); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.UrlEncode("The oauth state was missing or invalid."), + transaction.Response.Headers.GetValues("Location").First()); + } private static HttpResponseMessage ReturnJsonResponse(object content) { @@ -545,7 +698,7 @@ namespace Microsoft.AspNet.Authentication.Google app.UseCookieAuthentication(options => { options.AuthenticationScheme = TestExtensions.CookieAuthenticationScheme; - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); app.UseGoogleAuthentication(configureOptions); app.UseClaimsTransformation(p => diff --git a/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs index 4728ef7515..17e14d449d 100644 --- a/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; @@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); var transaction = await server.SendAsync("https://example.com/signIn"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); @@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); var transaction = await server.SendAsync("https://example.com/signOut"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); @@ -67,7 +67,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.Events = new JwtBearerEvents() { @@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.Events = new JwtBearerEvents() { @@ -151,7 +151,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.Events = new JwtBearerEvents() { @@ -188,7 +188,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer { var server = CreateServer(options => { - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; options.Events = new JwtBearerEvents() { diff --git a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs index a7d8a8b095..f30474f522 100644 --- a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -182,7 +182,7 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount app.UseCookieAuthentication(options => { options.AuthenticationScheme = TestExtensions.CookieAuthenticationScheme; - options.AutomaticAuthentication = true; + options.AutomaticAuthenticate = true; }); app.UseMicrosoftAccountAuthentication(configureOptions); diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerForTestingAuthenticate.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerForTestingAuthenticate.cs index 79e96d7d1c..ccdd74e3ff 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerForTestingAuthenticate.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerForTestingAuthenticate.cs @@ -5,8 +5,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.OpenIdConnect; -using Microsoft.AspNet.Http.Authentication; -using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Newtonsoft.Json.Linq; @@ -17,16 +15,10 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect /// public class OpenIdConnectHandlerForTestingAuthenticate : OpenIdConnectHandler { - public OpenIdConnectHandlerForTestingAuthenticate() - : base(null) + public OpenIdConnectHandlerForTestingAuthenticate() : base(null) { } - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) - { - return await base.HandleUnauthorizedAsync(context); - } - protected override Task RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri) { var jsonResponse = new JObject(); diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs index d01f3e6d63..1f695d97de 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -98,6 +98,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; options.ClientId = Guid.NewGuid().ToString(); options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + options.SignInScheme = "Cookies"; options.Events = new OpenIdConnectEvents() { OnTokenResponseReceived = context => diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index 8b6eaf4d44..0e7cf68e16 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -154,7 +154,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect var mockOpenIdConnectMessage = new Mock(); mockOpenIdConnectMessage.Setup(m => m.CreateAuthenticationRequestUrl()).Returns(ExpectedAuthorizeRequest); mockOpenIdConnectMessage.Setup(m => m.CreateLogoutRequestUrl()).Returns(ExpectedLogoutRequest); - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; options.Events = new OpenIdConnectEvents() { OnRedirectToAuthenticationEndpoint = (context) => @@ -191,7 +191,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect var server = CreateServer(options => { SetOptions(options, DefaultParameters(new string[] { OpenIdConnectParameterNames.State }), queryValues, stateDataFormat); - options.AutomaticAuthentication = challenge.Equals(ChallengeWithOutContext); + options.AutomaticChallenge = challenge.Equals(ChallengeWithOutContext); options.Events = new OpenIdConnectEvents() { OnRedirectToAuthenticationEndpoint = context => @@ -306,7 +306,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void DefaultChallengeOptions(OpenIdConnectOptions options) { options.AuthenticationScheme = "OpenIdConnectHandlerTest"; - options.AutomaticAuthentication = true; + options.AutomaticChallenge = true; options.ClientId = Guid.NewGuid().ToString(); options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); diff --git a/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs index d64474ae0a..aa060482b6 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Security.Claims; @@ -10,6 +11,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.WebEncoders; using Xunit; namespace Microsoft.AspNet.Authentication.Twitter @@ -61,6 +63,22 @@ namespace Microsoft.AspNet.Authentication.Twitter Assert.Contains("custom=test", query); } + [Fact] + public async Task BadSignInWill500() + { + var server = CreateServer(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + }); + + // Send a bogus sign in + var transaction = await server.SendAsync( + "https://example.com/signin-twitter"); + + Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); + } + [Fact] public async Task SignInThrows() { diff --git a/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs b/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs index 5f04c2b78f..93aaeb6156 100644 --- a/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs +++ b/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs @@ -34,9 +34,9 @@ namespace Microsoft.AspNet.Authroization.Test var combined = AuthorizationPolicy.Combine(options, attributes); // Assert - Assert.Equal(2, combined.ActiveAuthenticationSchemes.Count()); - Assert.True(combined.ActiveAuthenticationSchemes.Contains("dupe")); - Assert.True(combined.ActiveAuthenticationSchemes.Contains("roles")); + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.True(combined.AuthenticationSchemes.Contains("dupe")); + Assert.True(combined.AuthenticationSchemes.Contains("roles")); Assert.Equal(4, combined.Requirements.Count()); Assert.True(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); Assert.Equal(2, combined.Requirements.OfType().Count()); @@ -59,9 +59,9 @@ namespace Microsoft.AspNet.Authroization.Test var combined = AuthorizationPolicy.Combine(options, attributes); // Assert - Assert.Equal(2, combined.ActiveAuthenticationSchemes.Count()); - Assert.True(combined.ActiveAuthenticationSchemes.Contains("dupe")); - Assert.True(combined.ActiveAuthenticationSchemes.Contains("default")); + Assert.Equal(2, combined.AuthenticationSchemes.Count()); + Assert.True(combined.AuthenticationSchemes.Contains("dupe")); + Assert.True(combined.AuthenticationSchemes.Contains("default")); Assert.Equal(2, combined.Requirements.Count()); Assert.False(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); Assert.Equal(2, combined.Requirements.OfType().Count());