// Copyright (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 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() { var authResult = await HandleRemoteAuthenticateAsync(); if (authResult != null && authResult.Skipped) { return false; } if (authResult == null || !authResult.Succeeded) { var errorContext = new FailureContext(Context, authResult?.Failure ?? new Exception("Invalid return state, unable to redirect.")); Logger.RemoteAuthenticationError(errorContext.Failure.Message); await Options.Events.RemoteFailure(errorContext); if (errorContext.HandledResponse) { return true; } if (errorContext.Skipped) { return false; } throw new AggregateException("Unhandled remote failure.", errorContext.Failure); } // 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.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; } 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); } if (authenticateContext.Principal != null) { 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 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; } } }