// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Authentication { public abstract class RemoteAuthenticationHandler : AuthenticationHandler where TOptions : RemoteAuthenticationOptions { private const string CorrelationPrefix = ".AspNetCore.Correlation."; private const string CorrelationProperty = ".xsrf"; private const string CorrelationMarker = "N"; private const string AuthSchemeKey = ".AuthScheme"; private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); public override async Task HandleRequestAsync() { if (Options.CallbackPath == Request.Path) { return await HandleRemoteCallbackAsync(); } return false; } protected virtual async Task HandleRemoteCallbackAsync() { AuthenticationTicket ticket = null; Exception exception = null; try { var authResult = await HandleRemoteAuthenticateAsync(); if (authResult == null) { exception = new InvalidOperationException("Invalid return state, unable to redirect."); } else if (authResult.Handled) { return true; } else if (authResult.Skipped) { return false; } else if (!authResult.Succeeded) { exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect."); } ticket = authResult.Ticket; } catch (Exception ex) { exception = ex; } if (exception != null) { Logger.RemoteAuthenticationError(exception.Message); var errorContext = new FailureContext(Context, exception); await Options.Events.RemoteFailure(errorContext); if (errorContext.HandledResponse) { return true; } if (errorContext.Skipped) { return false; } throw new AggregateException("Unhandled remote failure.", exception); } // We have a ticket if we get here var context = new TicketReceivedContext(Context, Options, ticket) { ReturnUri = ticket.Properties.RedirectUri, }; // REVIEW: is this safe or good? ticket.Properties.RedirectUri = null; // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync context.Properties.Items[AuthSchemeKey] = Options.AuthenticationScheme; await Options.Events.TicketReceived(context); if (context.HandledResponse) { Logger.SigninHandled(); return true; } else if (context.Skipped) { Logger.SigninSkipped(); return false; } await Context.Authentication.SignInAsync(Options.SignInScheme, context.Principal, context.Properties); // Default redirect path is the base path if (string.IsNullOrEmpty(context.ReturnUri)) { context.ReturnUri = "/"; } Response.Redirect(context.ReturnUri); return true; } /// /// Authenticate the user identity with the identity provider. /// /// The method process the request on the endpoint defined by CallbackPath. /// protected abstract Task HandleRemoteAuthenticateAsync(); protected override async Task HandleAuthenticateAsync() { // Most RemoteAuthenticationHandlers will have a PriorHandler, but it might not be set up during unit tests. if (PriorHandler != null) { var authenticateContext = new AuthenticateContext(Options.SignInScheme); await PriorHandler.AuthenticateAsync(authenticateContext); if (authenticateContext.Accepted) { if (authenticateContext.Error != null) { return AuthenticateResult.Fail(authenticateContext.Error); } // The SignInScheme may be shared with multiple providers, make sure this middleware issued the identity. string authenticatedScheme; if (authenticateContext.Principal != null && authenticateContext.Properties != null && authenticateContext.Properties.TryGetValue(AuthSchemeKey, out authenticatedScheme) && string.Equals(Options.AuthenticationScheme, authenticatedScheme, StringComparison.Ordinal)) { return AuthenticateResult.Success(new AuthenticationTicket(authenticateContext.Principal, new AuthenticationProperties(authenticateContext.Properties), Options.AuthenticationScheme)); } return AuthenticateResult.Fail("Not authenticated"); } } return AuthenticateResult.Fail("Remote authentication does not directly support authenticate"); } protected override Task HandleSignOutAsync(SignOutContext context) { throw new NotSupportedException(); } protected override Task HandleSignInAsync(SignInContext context) { throw new NotSupportedException(); } protected override async Task HandleForbiddenAsync(ChallengeContext context) { var challengeContext = new ChallengeContext(Options.SignInScheme, context.Properties, ChallengeBehavior.Forbidden); await PriorHandler.ChallengeAsync(challengeContext); return challengeContext.Accepted; } protected virtual void GenerateCorrelationId(AuthenticationProperties properties) { if (properties == null) { throw new ArgumentNullException(nameof(properties)); } var bytes = new byte[32]; CryptoRandom.GetBytes(bytes); var correlationId = Base64UrlTextEncoder.Encode(bytes); var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps, Expires = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout), }; properties.Items[CorrelationProperty] = correlationId; var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId; Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions); } protected virtual bool ValidateCorrelationId(AuthenticationProperties properties) { if (properties == null) { throw new ArgumentNullException(nameof(properties)); } string correlationId; if (!properties.Items.TryGetValue(CorrelationProperty, out correlationId)) { Logger.CorrelationPropertyNotFound(CorrelationPrefix); return false; } properties.Items.Remove(CorrelationProperty); var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId; var correlationCookie = Request.Cookies[cookieName]; if (string.IsNullOrEmpty(correlationCookie)) { Logger.CorrelationCookieNotFound(cookieName); return false; } var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; Response.Cookies.Delete(cookieName, cookieOptions); if (!string.Equals(correlationCookie, CorrelationMarker, StringComparison.Ordinal)) { Logger.UnexpectedCorrelationCookieValue(cookieName, correlationCookie); return false; } return true; } } }