// 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.LogInformation("Error from RemoteAuthentication: " + 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.LogDebug("The SigningIn event returned Handled."); return true; } else if (context.Skipped) { Logger.LogDebug("The SigningIn event returned Skipped."); 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 Task HandleAuthenticateAsync() { return Task.FromResult(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 Task HandleForbiddenAsync(ChallengeContext context) { throw new NotSupportedException(); } 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 = properties.ExpiresUtc }; 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.LogWarning(26, "{0} state property not found.", CorrelationPrefix); return false; } properties.Items.Remove(CorrelationProperty); var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId; var correlationCookie = Request.Cookies[cookieName]; if (string.IsNullOrEmpty(correlationCookie)) { Logger.LogWarning(27, "'{0}' cookie not found.", 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.LogWarning(28, "The correlation cookie value '{0}' did not match the expected value '{1}'.", cookieName); return false; } return true; } } }