Add RemoteAuthenticationHandler base/error handling logic

This commit is contained in:
Hao Kung 2015-10-14 23:08:43 -07:00
parent bfc1fcf421
commit 409b50269a
82 changed files with 1095 additions and 1347 deletions

View File

@ -21,7 +21,7 @@ namespace CookieSample
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthentication = true;
options.AutomaticAuthenticate = true;
});
app.Run(async context =>

View File

@ -22,7 +22,7 @@ namespace CookieSessionSample
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthentication = true;
options.AutomaticAuthenticate = true;
options.SessionStore = new MemoryCacheTicketStore();
});

View File

@ -23,7 +23,7 @@ namespace OpenIdConnectSample
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthentication = true;
options.AutomaticAuthenticate = true;
});
app.UseOpenIdConnectAuthentication(options =>

View File

@ -29,7 +29,8 @@ namespace CookieSample
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthentication = true;
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.LoginPath = new PathString("/login");
});

View File

@ -101,43 +101,28 @@ namespace Microsoft.AspNet.Authentication.Cookies
return ticket;
}
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> 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<bool> HandleForbiddenAsync(ChallengeContext context)
protected override async Task<bool> 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;
}
}
}

View File

@ -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; }
}
}

View File

@ -42,11 +42,6 @@ namespace Microsoft.AspNet.Authentication.Cookies
return Task.FromResult(0);
};
/// <summary>
/// A delegate assigned to this property will be invoked when the related method is called.
/// </summary>
public Func<CookieExceptionContext, Task> OnException { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Implements the interface method by invoking the related delegate method.
/// </summary>
@ -95,11 +90,5 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// </summary>
/// <param name="context">Contains information about the event</param>
public virtual Task RedirectToAccessDenied(CookieRedirectContext context) => OnRedirect(context);
/// <summary>
/// Implements the interface method by invoking the related delegate method.
/// </summary>
/// <param name="context">Contains information about the event</param>
public virtual Task Exception(CookieExceptionContext context) => OnException(context);
}
}

View File

@ -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
{
/// <summary>
/// Context object passed to the ICookieAuthenticationProvider method Exception.
/// </summary>
public class CookieExceptionContext : BaseContext<CookieAuthenticationOptions>
{
/// <summary>
/// Creates a new instance of the context object.
/// </summary>
/// <param name="context">The HTTP request context</param>
/// <param name="options">The middleware options</param>
/// <param name="location">The location of the exception</param>
/// <param name="exception">The exception thrown.</param>
/// <param name="ticket">The current ticket, if any.</param>
public CookieExceptionContext(
HttpContext context,
CookieAuthenticationOptions options,
ExceptionLocation location,
Exception exception,
AuthenticationTicket ticket)
: base(context, options)
{
Location = location;
Exception = exception;
Rethrow = true;
Ticket = ticket;
}
/// <summary>
/// The code paths where exceptions may be reported.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type",
Target = "Microsoft.Owin.Security.Cookies.CookieExceptionContext+ExceptionLocation", Justification = "It is a directly related option.")]
public enum ExceptionLocation
{
/// <summary>
/// The exception was reported in the Authenticate code path.
/// </summary>
Authenticate,
/// <summary>
/// The exception was reported in the FinishResponse code path, during sign-in, sign-out, or refresh.
/// </summary>
FinishResponse,
/// <summary>
/// The exception was reported in the Unauthorized code path, during redirect generation.
/// </summary>
Unauthorized,
/// <summary>
/// The exception was reported in the Forbidden code path, during redirect generation.
/// </summary>
Forbidden,
/// <summary>
/// The exception was reported in the SignIn code path
/// </summary>
SignIn,
/// <summary>
/// The exception was reported in the SignOut code path
/// </summary>
SignOut,
}
/// <summary>
/// The code path the exception occurred in.
/// </summary>
public ExceptionLocation Location { get; private set; }
/// <summary>
/// The exception thrown.
/// </summary>
public Exception Exception { get; private set; }
/// <summary>
/// True if the exception should be re-thrown (default), false if it should be suppressed.
/// </summary>
public bool Rethrow { get; set; }
/// <summary>
/// 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.
/// </summary>
public AuthenticationTicket Ticket { get; set; }
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Context passed when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware
/// </summary>
public class CookieRedirectContext : BaseContext<CookieAuthenticationOptions>
public class CookieRedirectContext : BaseCookieContext
{
/// <summary>
/// Creates a new context object.

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Context object passed to the ICookieAuthenticationEvents method SignedIn.
/// </summary>
public class CookieSignedInContext : BaseContext<CookieAuthenticationOptions>
public class CookieSignedInContext : BaseCookieContext
{
/// <summary>
/// Creates a new instance of the context object.

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Context object passed to the ICookieAuthenticationEvents method SigningIn.
/// </summary>
public class CookieSigningInContext : BaseContext<CookieAuthenticationOptions>
public class CookieSigningInContext : BaseCookieContext
{
/// <summary>
/// Creates a new instance of the context object.

View File

@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Context object passed to the ICookieAuthenticationEvents method SigningOut
/// </summary>
public class CookieSigningOutContext : BaseContext<CookieAuthenticationOptions>
public class CookieSigningOutContext : BaseCookieContext
{
/// <summary>
///

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Context object passed to the ICookieAuthenticationProvider method ValidatePrincipal.
/// </summary>
public class CookieValidatePrincipalContext : BaseContext<CookieAuthenticationOptions>
public class CookieValidatePrincipalContext : BaseCookieContext
{
/// <summary>
/// Creates a new instance of the context object.

View File

@ -60,11 +60,5 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// </summary>
/// <param name="context">Contains information about the login session as well as information about the authentication cookie.</param>
Task SigningOut(CookieSigningOutContext context);
/// <summary>
/// Called when an exception occurs during request or response processing.
/// </summary>
/// <param name="context">Contains information about the exception that occurred</param>
Task Exception(CookieExceptionContext context);
}
}

View File

@ -6,7 +6,7 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication.JwtBearer
{
public class AuthenticationFailedContext : BaseControlContext<JwtBearerOptions>
public class AuthenticationFailedContext : BaseJwtBearerContext
{
public AuthenticationFailedContext(HttpContext context, JwtBearerOptions options)
: base(context, options)

View File

@ -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; }
}
}

View File

@ -5,7 +5,7 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication.JwtBearer
{
public class JwtBearerChallengeContext : BaseControlContext<JwtBearerOptions>
public class JwtBearerChallengeContext : BaseJwtBearerContext
{
public JwtBearerChallengeContext(HttpContext context, JwtBearerOptions options)
: base(context, options)

View File

@ -5,7 +5,7 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication.JwtBearer
{
public class ReceivedTokenContext : BaseControlContext<JwtBearerOptions>
public class ReceivedTokenContext : BaseJwtBearerContext
{
public ReceivedTokenContext(HttpContext context, JwtBearerOptions options)
: base(context, options)

View File

@ -5,7 +5,7 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication.JwtBearer
{
public class ReceivingTokenContext : BaseControlContext<JwtBearerOptions>
public class ReceivingTokenContext : BaseJwtBearerContext
{
public ReceivingTokenContext(HttpContext context, JwtBearerOptions options)
: base(context, options)

View File

@ -5,7 +5,7 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication.JwtBearer
{
public class ValidatedTokenContext : BaseControlContext<JwtBearerOptions>
public class ValidatedTokenContext : BaseJwtBearerContext
{
public ValidatedTokenContext(HttpContext context, JwtBearerOptions options)
: base(context, options)

View File

@ -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 <see cref="TokenValidationParameters"/> set in the options.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> 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;

View File

@ -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
{
/// <summary>
/// Base class used for certain event contexts
/// </summary>
public abstract class BaseValidatingContext<TOptions> : BaseContext<TOptions>
{
/// <summary>
/// Initializes base class used for certain event contexts
/// </summary>
protected BaseValidatingContext(
HttpContext context,
TOptions options)
: base(context, options)
{
}
/// <summary>
/// True if application code has called any of the Validate methods on this context.
/// </summary>
public bool IsValidated { get; private set; }
/// <summary>
/// True if application code has called any of the SetError methods on this context.
/// </summary>
public bool HasError { get; private set; }
/// <summary>
/// The error argument provided when SetError was called on this context. This is eventually
/// returned to the client app as the OAuth "error" parameter.
/// </summary>
public string Error { get; private set; }
/// <summary>
/// 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.
/// </summary>
public string ErrorDescription { get; private set; }
/// <summary>
/// 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.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "error_uri is a string value in the protocol")]
public string ErrorUri { get; private set; }
/// <summary>
/// Marks this context as validated by the application. IsValidated becomes true and HasError becomes false as a result of calling.
/// </summary>
/// <returns>True if the validation has taken effect.</returns>
public virtual bool Validated()
{
IsValidated = true;
HasError = false;
return true;
}
/// <summary>
/// Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling.
/// </summary>
public virtual void Rejected()
{
IsValidated = false;
HasError = false;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="error">Assigned to the Error property</param>
public void SetError(string error)
{
SetError(error, null);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="error">Assigned to the Error property</param>
/// <param name="errorDescription">Assigned to the ErrorDescription property</param>
public void SetError(string error,
string errorDescription)
{
SetError(error, errorDescription, null);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="error">Assigned to the Error property</param>
/// <param name="errorDescription">Assigned to the ErrorDescription property</param>
/// <param name="errorUri">Assigned to the ErrorUri property</param>
[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;
}
}
}

View File

@ -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
{
/// <summary>
/// Base class used for certain event contexts
/// </summary>
public abstract class BaseValidatingTicketContext<TOptions> : BaseValidatingContext<TOptions>
{
/// <summary>
/// Initializes base class used for certain event contexts
/// </summary>
protected BaseValidatingTicketContext(
HttpContext context,
TOptions options,
AuthenticationTicket ticket)
: base(context, options)
{
Ticket = ticket;
}
/// <summary>
/// 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.
/// </summary>
public AuthenticationTicket Ticket { get; private set; }
/// <summary>
/// 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.
/// </summary>
/// <param name="ticket">Assigned to the Ticket property</param>
/// <returns>True if the validation has taken effect.</returns>
public bool Validated(AuthenticationTicket ticket)
{
Ticket = ticket;
return Validated();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="identity">Assigned to the Ticket.Identity property</param>
/// <returns>True if the validation has taken effect.</returns>
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));
}
}
}

View File

@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <summary>
/// Specifies callback methods which the <see cref="OAuthMiddleware"/> invokes to enable developer control over the authentication process.
/// </summary>
public interface IOAuthEvents
public interface IOAuthEvents : IRemoteAuthenticationEvents
{
/// <summary>
/// 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
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task CreatingTicket(OAuthCreatingTicketContext context);
/// <summary>
/// Invoked prior to the <see cref="ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task SigningIn(SigningInContext context);
/// <summary>
/// Called when a Challenge causes a redirect to the authorize endpoint.
/// </summary>

View File

@ -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
{
/// <summary>
/// Specifies the HTTP response header for the bearer authentication scheme.
/// </summary>
public class OAuthChallengeContext : BaseContext
{
/// <summary>
/// Initializes a new <see cref="OAuthRequestTokenContext"/>
/// </summary>
/// <param name="context">HTTP environment</param>
/// <param name="challenge">The www-authenticate header value.</param>
public OAuthChallengeContext(
HttpContext context,
string challenge)
: base(context)
{
Challenge = challenge;
}
/// <summary>
/// The www-authenticate header value.
/// </summary>
public string Challenge { get; protected set; }
}
}

View File

@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class OAuthCreatingTicketContext : BaseContext<OAuthOptions>
public class OAuthCreatingTicketContext : BaseContext
{
/// <summary>
/// Initializes a new <see cref="OAuthCreatingTicketContext"/>.
@ -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; }
/// <summary>
/// Gets the JSON-serialized user or an empty
/// <see cref="JObject"/> if it is not available.

View File

@ -9,18 +9,13 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <summary>
/// Default <see cref="IOAuthEvents"/> implementation.
/// </summary>
public class OAuthEvents : IOAuthEvents
public class OAuthEvents : RemoteAuthenticationEvents, IOAuthEvents
{
/// <summary>
/// Gets or sets the function that is invoked when the CreatingTicket method is invoked.
/// </summary>
public Func<OAuthCreatingTicketContext, Task> OnCreatingTicket { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Gets or sets the function that is invoked when the SigningIn method is invoked.
/// </summary>
public Func<SigningInContext, Task> OnSigningIn { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked.
/// </summary>
@ -37,17 +32,10 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task CreatingTicket(OAuthCreatingTicketContext context) => OnCreatingTicket(context);
/// <summary>
/// Invoked prior to the <see cref="ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="ClaimsIdentity"/></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task SigningIn(SigningInContext context) => OnSigningIn(context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
public virtual Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context) => OnRedirectToAuthorizationEndpoint(context);
}
}
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <summary>
/// Context passed when a Challenge causes a redirect to authorize endpoint in the middleware.
/// </summary>
public class OAuthRedirectToAuthorizationContext : BaseContext<OAuthOptions>
public class OAuthRedirectToAuthorizationContext : BaseContext
{
/// <summary>
/// Creates a new context object.
@ -18,12 +18,15 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <param name="properties">The authentication properties of the challenge.</param>
/// <param name="redirectUri">The initial redirect URI.</param>
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; }
/// <summary>
/// Gets the URI used for the redirect operation.
/// </summary>

View File

@ -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<TOptions> : AuthenticationHandler<TOptions> where TOptions : OAuthOptions
public class OAuthHandler<TOptions> : RemoteAuthenticationHandler<TOptions> 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<bool> InvokeAsync()
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
return await InvokeReturnPathAsync();
}
return false;
}
public async Task<bool> 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<AuthenticationTicket> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> 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<OAuthTokenResponse> 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<bool> HandleForbiddenAsync(ChallengeContext context)
{
throw new NotSupportedException();
}
protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
var scope = FormatScope();

View File

@ -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; }

View File

@ -12,8 +12,13 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <summary>
/// Configuration options for <see cref="OAuthMiddleware"/>.
/// </summary>
public class OAuthOptions : AuthenticationOptions
public class OAuthOptions : RemoteAuthenticationOptions
{
public OAuthOptions()
{
Events = new OAuthEvents();
}
/// <summary>
/// Gets or sets the provider-assigned client id.
/// </summary>
@ -41,54 +46,20 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// </summary>
public string UserInformationEndpoint { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string DisplayName
{
get { return Description.DisplayName; }
set { Description.DisplayName = value; }
}
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with the auth provider.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// 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.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Gets or sets the <see cref="IOAuthEvents"/> used to handle authentication events.
/// </summary>
public IOAuthEvents Events { get; set; } = new OAuthEvents();
public new IOAuthEvents Events
{
get { return (IOAuthEvents)base.Events; }
set { base.Events = value; }
}
/// <summary>
/// A list of permissions to request.
/// </summary>
public IList<string> Scope { get; } = new List<string>();
/// <summary>
/// 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.
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// 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, <see cref="SharedAuthenticationOptions.SignInScheme"/> is used as a fallback value.
/// </summary>
public string SignInScheme { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>

View File

@ -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<OpenIdConnectOptions>
{
public AuthenticationCompletedContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public class AuthenticationFailedContext : BaseControlContext<OpenIdConnectOptions>
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; }
}
}

View File

@ -6,15 +6,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public class AuthenticationValidatedContext : BaseControlContext<OpenIdConnectOptions>
public class AuthenticationValidatedContext : BaseOpenIdConnectContext
{
public AuthenticationValidatedContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
public OpenIdConnectMessage ProtocolMessage { get; set; }
public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; }
}
}

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// This Context can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol.
/// </summary>
public class AuthorizationCodeReceivedContext : BaseControlContext<OpenIdConnectOptions>
public class AuthorizationCodeReceivedContext : BaseOpenIdConnectContext
{
/// <summary>
/// Creates a <see cref="AuthorizationCodeReceivedContext"/>
@ -31,11 +31,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
public JwtSecurityToken JwtSecurityToken { get; set; }
/// <summary>
/// Gets or sets the <see cref="OpenIdConnectMessage"/>.
/// </summary>
public OpenIdConnectMessage ProtocolMessage { get; set; }
/// <summary>
/// Gets or sets the 'redirect_uri'.
/// </summary>

View File

@ -7,15 +7,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public class AuthorizationResponseReceivedContext : BaseControlContext<OpenIdConnectOptions>
public class AuthorizationResponseReceivedContext : BaseOpenIdConnectContext
{
public AuthorizationResponseReceivedContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
public OpenIdConnectMessage ProtocolMessage { get; set; }
public AuthenticationProperties Properties { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -8,13 +8,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Specifies events which the <see cref="OpenIdConnectMiddleware" />invokes to enable developer control over the authentication process.
/// </summary>
public interface IOpenIdConnectEvents
public interface IOpenIdConnectEvents : IRemoteAuthenticationEvents
{
/// <summary>
/// Invoked when the authentication process completes.
/// </summary>
Task AuthenticationCompleted(AuthenticationCompletedContext context);
/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>

View File

@ -6,15 +6,13 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public class MessageReceivedContext : BaseControlContext<OpenIdConnectOptions>
public class MessageReceivedContext : BaseOpenIdConnectContext
{
public MessageReceivedContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
public OpenIdConnectMessage ProtocolMessage { get; set; }
/// <summary>
/// Bearer Token. This will give application an opportunity to retrieve token from an alternation location.
/// </summary>

View File

@ -9,13 +9,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Specifies events which the <see cref="OpenIdConnectMiddleware" />invokes to enable developer control over the authentication process.
/// </summary>
public class OpenIdConnectEvents : IOpenIdConnectEvents
public class OpenIdConnectEvents : RemoteAuthenticationEvents, IOpenIdConnectEvents
{
/// <summary>
/// Invoked when the authentication process completes.
/// </summary>
public Func<AuthenticationCompletedContext, Task> OnAuthenticationCompleted { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>
@ -61,8 +56,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
public Func<UserInformationReceivedContext, Task> 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);

View File

@ -10,16 +10,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// When a user configures the <see cref="OpenIdConnectMiddleware"/> to be notified prior to redirecting to an IdentityProvider
/// an instance of <see cref="RedirectContext"/> is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events.
/// </summary>
public class RedirectContext : BaseControlContext<OpenIdConnectOptions>
public class RedirectContext : BaseOpenIdConnectContext
{
public RedirectContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
/// <summary>
/// Gets or sets the <see cref="OpenIdConnectMessage"/>.
/// </summary>
public OpenIdConnectMessage ProtocolMessage { get; set; }
}
}

View File

@ -6,7 +6,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint.
/// </summary>
public class TokenResponseReceivedContext : BaseControlContext<OpenIdConnectOptions>
public class TokenResponseReceivedContext : BaseOpenIdConnectContext
{
/// <summary>
/// Creates a <see cref="TokenResponseReceivedContext"/>
@ -20,11 +20,5 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// Gets or sets the <see cref="OpenIdConnectTokenEndpointResponse"/> that contains the tokens and json response received after redeeming the code at the token endpoint.
/// </summary>
public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; }
/// <summary>
/// Gets or sets the <see cref="OpenIdConnectMessage"/>.
/// </summary>
public OpenIdConnectMessage ProtocolMessage { get; set; }
}
}

View File

@ -7,15 +7,13 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
public class UserInformationReceivedContext : BaseControlContext<OpenIdConnectOptions>
public class UserInformationReceivedContext : BaseOpenIdConnectContext
{
public UserInformationReceivedContext(HttpContext context, OpenIdConnectOptions options)
: base(context, options)
{
}
public OpenIdConnectMessage ProtocolMessage { get; set; }
public JObject User { get; set; }
}
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware.
/// </summary>
public class OpenIdConnectHandler : AuthenticationHandler<OpenIdConnectOptions>
public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>
{
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}");
}
/// <summary>
@ -305,16 +302,10 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks>
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> 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<AuthenticationTicket> HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties)
private async Task<AuthenticateResult> 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<AuthenticationTicket> HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties)
private async Task<AuthenticateResult> 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);
}
/// <summary>
@ -1135,54 +1125,5 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
return ticket;
}
/// <summary>
/// Calls InvokeReplyPathAsync
/// </summary>
/// <returns>True if the request was handled, false if the next middleware should be invoked.</returns>
public override Task<bool> InvokeAsync()
{
return InvokeReturnPathAsync();
}
private async Task<bool> 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;
}
}
}

View File

@ -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)
{

View File

@ -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
/// <summary>
/// Configuration options for <see cref="OpenIdConnectOptions"/>
/// </summary>
public class OpenIdConnectOptions : AuthenticationOptions
public class OpenIdConnectOptions : RemoteAuthenticationOptions
{
/// <summary>
/// Initializes a new <see cref="OpenIdConnectOptions"/>
@ -50,6 +48,8 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
AuthenticationScheme = authenticationScheme;
DisplayName = OpenIdConnectDefaults.Caption;
CallbackPath = new PathString("/signin-oidc");
Events = new OpenIdConnectEvents();
}
/// <summary>
@ -65,36 +65,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
public string Authority { get; set; }
/// <summary>
/// The HttpMessageHandler used to retrieve metadata.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value
/// is a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Gets or sets the timeout when using the backchannel to make an http call.
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "By design we use the property name in the exception")]
public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string DisplayName
{
get { return Description.DisplayName; }
set { Description.DisplayName = value; }
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>If you set this value, then the <see cref="OpenIdConnectHandler"/> 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 -> ...</remarks>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the 'client_id'.
/// </summary>
@ -136,7 +106,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <summary>
/// Gets or sets the <see cref="IOpenIdConnectEvents"/> to notify when processing OpenIdConnect messages.
/// </summary>
public IOpenIdConnectEvents Events { get; set; } = new OpenIdConnectEvents();
public new IOpenIdConnectEvents Events
{
get { return (IOpenIdConnectEvents)base.Events; }
set { base.Events = value; }
}
/// <summary>
/// Gets or sets the <see cref="OpenIdConnectProtocolValidator"/> that is used to ensure that the 'id_token' received
@ -194,11 +168,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
public IList<string> Scope { get; } = new List<string> { "openid", "profile" };
/// <summary>
/// Gets or sets the SignInScheme which will be used to set the <see cref="ClaimsIdentity.AuthenticationType"/>.
/// </summary>
public string SignInScheme { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>

View File

@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
}
/// <summary>
/// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication
/// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate
/// </summary>
internal static string OIDCH_0029_ChallengContextEqualsNull
{
@ -115,7 +115,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
}
/// <summary>
/// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication
/// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate
/// </summary>
internal static string FormatOIDCH_0029_ChallengContextEqualsNull()
{

View File

@ -136,7 +136,7 @@
<value>OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'.</value>
</data>
<data name="OIDCH_0029_ChallengContextEqualsNull" xml:space="preserve">
<value>OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication</value>
<value>OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthenticate</value>
</data>
<data name="OIDCH_0030_Using_Properties_RedirectUri" xml:space="preserve">
<value>OIDCH_0030: Using properties.RedirectUri for 'local redirect' post authentication: '{0}'.</value>

View File

@ -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
{
/// <summary>
/// Base class for other Twitter contexts.
/// </summary>
public class BaseTwitterContext : BaseContext
{
/// <summary>
/// Initializes a <see cref="BaseTwitterContext"/>
/// </summary>
/// <param name="context">The HTTP environment</param>
/// <param name="options">The options for Twitter</param>
public BaseTwitterContext(HttpContext context, TwitterOptions options)
: base(context)
{
Options = options;
}
public TwitterOptions Options { get; }
}
}

View File

@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// Specifies callback methods which the <see cref="TwitterMiddleware"></see> invokes to enable developer control over the authentication process. />
/// </summary>
public interface ITwitterEvents
public interface ITwitterEvents : IRemoteAuthenticationEvents
{
/// <summary>
/// Invoked whenever Twitter succesfully authenticates a user
@ -17,13 +17,6 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task CreatingTicket(TwitterCreatingTicketContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task SigningIn(SigningInContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware
/// </summary>

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class TwitterCreatingTicketContext : BaseContext
public class TwitterCreatingTicketContext : BaseTwitterContext
{
/// <summary>
/// Initializes a <see cref="TwitterCreatingTicketContext"/>
@ -22,11 +22,12 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <param name="accessTokenSecret">Twitter access token secret</param>
public TwitterCreatingTicketContext(
HttpContext context,
TwitterOptions options,
string userId,
string screenName,
string accessToken,
string accessTokenSecret)
: base(context)
: base(context, options)
{
UserId = userId;
ScreenName = screenName;

View File

@ -9,18 +9,13 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// Default <see cref="ITwitterEvents"/> implementation.
/// </summary>
public class TwitterEvents : ITwitterEvents
public class TwitterEvents : RemoteAuthenticationEvents, ITwitterEvents
{
/// <summary>
/// Gets or sets the function that is invoked when the Authenticated method is invoked.
/// </summary>
public Func<TwitterCreatingTicketContext, Task> OnCreatingTicket { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<SigningInContext, Task> OnSigningIn { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
@ -37,13 +32,6 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task CreatingTicket(TwitterCreatingTicketContext context) => OnCreatingTicket(context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task SigningIn(SigningInContext context) => OnSigningIn(context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware
/// </summary>

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// The Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware.
/// </summary>
public class TwitterRedirectToAuthorizationEndpointContext : BaseContext<TwitterOptions>
public class TwitterRedirectToAuthorizationEndpointContext : BaseTwitterContext
{
/// <summary>
/// Creates a new context object.

View File

@ -19,7 +19,7 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNet.Authentication.Twitter
{
internal class TwitterHandler : AuthenticationHandler<TwitterOptions>
internal class TwitterHandler : RemoteAuthenticationHandler<TwitterOptions>
{
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<bool> InvokeAsync()
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
return await InvokeReturnPathAsync();
}
return false;
}
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> 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<AuthenticationTicket> 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<bool> 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<bool> HandleForbiddenAsync(ChallengeContext context)
{
throw new NotSupportedException();
var redirectContext = new TwitterRedirectToAuthorizationEndpointContext(
Context, Options,
properties, twitterAuthenticationEndpoint);
await Options.Events.RedirectToAuthorizationEndpoint(redirectContext);
return true;
}
private async Task<RequestToken> 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<AccessToken> ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier)

View File

@ -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)
{

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// Options for the Twitter authentication middleware.
/// </summary>
public class TwitterOptions : AuthenticationOptions
public class TwitterOptions : RemoteAuthenticationOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="TwitterOptions"/> class.
@ -21,6 +21,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
DisplayName = AuthenticationScheme;
CallbackPath = new PathString("/signin-twitter");
BackchannelTimeout = TimeSpan.FromSeconds(60);
Events = new TwitterEvents();
}
/// <summary>
@ -35,45 +36,6 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <value>The consumer secret used to sign requests to Twitter.</value>
public string ConsumerSecret { get; set; }
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Twitter.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
/// <summary>
/// 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.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string DisplayName
{
get { return Description.DisplayName; }
set { Description.DisplayName = value; }
}
/// <summary>
/// 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".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// 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, <see cref="SharedAuthenticationOptions.SignInScheme"/> is used as a fallback value.
/// </summary>
public string SignInScheme { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
@ -82,7 +44,11 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <summary>
/// Gets or sets the <see cref="ITwitterEvents"/> used to handle authentication events.
/// </summary>
public ITwitterEvents Events { get; set; }
public new ITwitterEvents Events
{
get { return (ITwitterEvents)base.Events; }
set { base.Events = value; }
}
/// <summary>
/// Defines whether access tokens should be stored in the

View File

@ -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
{
/// <summary>
/// Contains the result of an Authenticate call
/// </summary>
public class AuthenticateResult
{
private AuthenticateResult() { }
/// <summary>
/// If a ticket was produced, authenticate was successful.
/// </summary>
public bool Succeeded
{
get
{
return Ticket != null;
}
}
/// <summary>
/// The authentication ticket.
/// </summary>
public AuthenticationTicket Ticket { get; private set; }
/// <summary>
/// Holds error information caused by authentication.
/// </summary>
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) };
}
}
}

View File

@ -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
/// <typeparam name="TOptions">Specifies which type for of AuthenticationOptions property</typeparam>
public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationOptions
{
private Task<AuthenticationTicket> _authenticateTask;
private Task<AuthenticateResult> _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
/// <returns>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.</returns>
public virtual Task<bool> InvokeAsync()
public virtual Task<bool> 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<AuthenticationTicket> HandleAuthenticateOnceAsync()
protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
{
if (_authenticateTask == null)
{
@ -221,18 +233,17 @@ namespace Microsoft.AspNet.Authentication
return _authenticateTask;
}
protected abstract Task<AuthenticationTicket> HandleAuthenticateAsync();
protected abstract Task<AuthenticateResult> 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);
}
/// <summary>
/// </summary>
/// <param name="context"></param>
/// <returns>True if no other handlers should be called</returns>
protected virtual Task<bool> 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;

View File

@ -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<TOptions> CreateHandler();

View File

@ -27,11 +27,16 @@ namespace Microsoft.AspNet.Authentication
}
/// <summary>
/// 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.
/// </summary>
public bool AutomaticAuthentication { get; set; }
public bool AutomaticAuthenticate { get; set; }
/// <summary>
/// If true the authentication middleware should handle automatic challenge.
/// If false the authentication middleware will only alter responses when explicitly indicated by the AuthenticationScheme.
/// </summary>
public bool AutomaticChallenge { get; set; }
/// <summary>
/// Gets or sets the issuer that should be used for any claims that are created

View File

@ -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
{
/// <summary>
/// Base class used for certain event contexts
/// </summary>
public abstract class BaseContext<TOptions>
{
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; }
}
}
}

View File

@ -5,9 +5,9 @@ using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication
{
public class BaseControlContext<TOptions> : BaseContext<TOptions>
public class BaseControlContext : BaseContext
{
protected BaseControlContext(HttpContext context, TOptions options) : base(context, options)
protected BaseControlContext(HttpContext context) : base(context)
{
}

View File

@ -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
{
/// <summary>
/// Provides error context information to middleware providers.
/// </summary>
public class ErrorContext : BaseControlContext
{
public ErrorContext(HttpContext context, Exception error)
: base(context)
{
Error = error;
}
/// <summary>
/// User friendly error message for the error.
/// </summary>
public Exception Error { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Invoked when the remote authentication process has an error.
/// </summary>
Task RemoteError(ErrorContext context);
/// <summary>
/// Invoked before sign in.
/// </summary>
Task TicketReceived(TicketReceivedContext context);
}
}

View File

@ -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<ErrorContext, Task> OnRemoteError { get; set; } = context => Task.FromResult(0);
public Func<TicketReceivedContext, Task> OnTicketReceived { get; set; } = context => Task.FromResult(0);
/// <summary>
/// Invoked when there is a remote error
/// </summary>
public virtual Task RemoteError(ErrorContext context) => OnRemoteError(context);
/// <summary>
/// Invoked after the remote ticket has been recieved.
/// </summary>
public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context);
}
}

View File

@ -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
/// <summary>
/// Provides context information to middleware providers.
/// </summary>
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; }
}
}

View File

@ -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<TOptions> : AuthenticationHandler<TOptions> where TOptions : RemoteAuthenticationOptions
{
public override async Task<bool> HandleRequestAsync()
{
if (Options.CallbackPath == Request.Path)
{
return await HandleRemoteCallbackAsync();
}
return false;
}
protected virtual async Task<bool> 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<AuthenticateResult> HandleRemoteAuthenticateAsync();
protected override Task<AuthenticateResult> 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<bool> HandleForbiddenAsync(ChallengeContext context)
{
throw new NotSupportedException();
}
}
}

View File

@ -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
{
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Twitter.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// 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.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// 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.
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// 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, <see cref="SharedAuthenticationOptions.SignInScheme"/> is used as a fallback value.
/// </summary>
public string SignInScheme { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string DisplayName
{
get { return Description.DisplayName; }
set { Description.DisplayName = value; }
}
public IRemoteAuthenticationEvents Events = new RemoteAuthenticationEvents();
}
}

View File

@ -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-*"
}
}
}
}

View File

@ -9,16 +9,16 @@ namespace Microsoft.AspNet.Authorization
{
public class AuthorizationPolicy
{
public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> activeAuthenticationSchemes)
public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> 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<IAuthorizationRequirement>(requirements).AsReadOnly();
ActiveAuthenticationSchemes = new List<string>(activeAuthenticationSchemes).AsReadOnly();
AuthenticationSchemes = new List<string>(authenticationSchemes).AsReadOnly();
}
public IReadOnlyList<IAuthorizationRequirement> Requirements { get; }
public IReadOnlyList<string> ActiveAuthenticationSchemes { get; }
public IReadOnlyList<string> 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)

View File

@ -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<IAuthorizationRequirement> Requirements { get; set; } = new List<IAuthorizationRequirement>();
public IList<string> ActiveAuthenticationSchemes { get; set; } = new List<string>();
public IList<string> AuthenticationSchemes { get; set; } = new List<string>();
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());
}
}
}

View File

@ -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<AuthenticationTicket> HandleAuthenticateAsync()
protected override Task<AuthenticateResult> 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<AuthenticationTicket> HandleAuthenticateAsync()
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult<AuthenticationTicket>(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<AuthenticationTicket> HandleAuthenticateAsync()
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult<AuthenticationTicket>(null);
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new Http.Authentication.AuthenticationProperties(), "whatever")));
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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<HttpRequestException>(() => 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<object>(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 =>

View File

@ -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()
{

View File

@ -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);

View File

@ -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
/// </summary>
public class OpenIdConnectHandlerForTestingAuthenticate : OpenIdConnectHandler
{
public OpenIdConnectHandlerForTestingAuthenticate()
: base(null)
public OpenIdConnectHandlerForTestingAuthenticate() : base(null)
{
}
protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context)
{
return await base.HandleUnauthorizedAsync(context);
}
protected override Task<OpenIdConnectTokenEndpointResponse> RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri)
{
var jsonResponse = new JObject();

View File

@ -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 =>

View File

@ -154,7 +154,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
var mockOpenIdConnectMessage = new Mock<OpenIdConnectMessage>();
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();

View File

@ -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()
{

View File

@ -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<ClaimsAuthorizationRequirement>().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<ClaimsAuthorizationRequirement>().Count());