// 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.Globalization; using System.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.Framework.Caching.Distributed; using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OpenIdConnect { /// /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. /// public class OpenIdConnectAuthenticationHandler : AuthenticationHandler { private const string NonceProperty = "N"; private const string UriSchemeDelimiter = "://"; private OpenIdConnectConfiguration _configuration; protected HttpClient Backchannel { get; private set; } public OpenIdConnectAuthenticationHandler(HttpClient backchannel) { Backchannel = backchannel; } /// /// Handles Signout /// /// protected override async Task HandleSignOutAsync(SignOutContext signout) { if (signout != null) { if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var message = new OpenIdConnectMessage() { IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty), RequestType = OpenIdConnectRequestType.LogoutRequest, }; // Set End_Session_Endpoint in order: // 1. properties.Redirect // 2. Options.PostLogoutRedirectUri var properties = new AuthenticationProperties(signout.Properties); if (!string.IsNullOrEmpty(properties.RedirectUri)) { message.PostLogoutRedirectUri = properties.RedirectUri; } else if (!string.IsNullOrEmpty(Options.PostLogoutRedirectUri)) { message.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; } if (Options.Notifications.RedirectToIdentityProvider != null) { var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) { ProtocolMessage = message }; await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); if (redirectToIdentityProviderNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); return; } else if (redirectToIdentityProviderNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); return; } message = redirectToIdentityProviderNotification.ProtocolMessage; } var redirectUri = message.CreateLogoutRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); } Response.Redirect(redirectUri); } } /// /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. /// /// /// Uses log id's OIDCH-0026 - OIDCH-0050, next num: 37 protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) { Logger.LogDebug(Resources.OIDCH_0026_ApplyResponseChallengeAsync, this.GetType()); // order for local RedirectUri // 1. challenge.Properties.RedirectUri // 2. CurrentUri if Options.DefaultToCurrentUriOnRedirect is true) AuthenticationProperties properties = new AuthenticationProperties(context.Properties); if (!string.IsNullOrEmpty(properties.RedirectUri)) { Logger.LogDebug(Resources.OIDCH_0030_Using_Properties_RedirectUri, properties.RedirectUri); } else if (Options.DefaultToCurrentUriOnRedirect) { Logger.LogDebug(Resources.OIDCH_0032_UsingCurrentUriRedirectUri, CurrentUri); properties.RedirectUri = CurrentUri; } if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var message = new OpenIdConnectMessage { ClientId = Options.ClientId, IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty, RedirectUri = Options.RedirectUri, // [brentschmaltz] - #215 this should be a property on RedirectToIdentityProviderNotification not on the OIDCMessage. RequestType = OpenIdConnectRequestType.AuthenticationRequest, Resource = Options.Resource, ResponseType = Options.ResponseType, Scope = Options.Scope }; // Omitting the response_mode parameter when it already corresponds to the default // response_mode used for the specified response_type is recommended by the specifications. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes if (!string.Equals(Options.ResponseType, OpenIdConnectResponseTypes.Code, StringComparison.Ordinal) || !string.Equals(Options.ResponseMode, OpenIdConnectResponseModes.Query, StringComparison.Ordinal)) { message.ResponseMode = Options.ResponseMode; } if (Options.ProtocolValidator.RequireNonce) { message.Nonce = Options.ProtocolValidator.GenerateNonce(); if (Options.CacheNonces) { if (await Options.NonceCache.GetAsync(message.Nonce) != null) { Logger.LogError(Resources.OIDCH_0033_NonceAlreadyExists, message.Nonce); throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0033_NonceAlreadyExists, message.Nonce)); } await Options.NonceCache.SetAsync(message.Nonce, new byte[0], new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = Options.ProtocolValidator.NonceLifetime }); } else { WriteNonceCookie(message.Nonce); } } if (Options.Notifications.RedirectToIdentityProvider != null) { var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) { ProtocolMessage = message }; await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); if (redirectToIdentityProviderNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); return true; } else if (redirectToIdentityProviderNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); return false; } if (!string.IsNullOrEmpty(redirectToIdentityProviderNotification.ProtocolMessage.State)) { properties.Items[OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey] = redirectToIdentityProviderNotification.ProtocolMessage.State; } message = redirectToIdentityProviderNotification.ProtocolMessage; } var redirectUriForCode = message.RedirectUri; if (string.IsNullOrEmpty(redirectUriForCode)) { Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); redirectUriForCode = Options.RedirectUri; } if (!string.IsNullOrEmpty(redirectUriForCode)) { // When redeeming a 'code' for an AccessToken, this value is needed properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, redirectUriForCode); } message.State = Options.StateDataFormat.Protect(properties); var redirectUri = message.CreateAuthenticationRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); } Response.Redirect(redirectUri); return true; } /// /// Invoked to process incoming OpenIdConnect messages. /// /// An if successful. /// Uses log id's OIDCH-0000 - OIDCH-0025 protected override async Task HandleAuthenticateAsync() { 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)) { message = new OpenIdConnectMessage(Request.Query); // response_mode=query (explicit or not) and a response_type containing id_token // or token are not considered as a safe combination and MUST be rejected. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security if (!string.IsNullOrEmpty(message.IdToken) || !string.IsNullOrEmpty(message.Token)) { Logger.LogError("An OpenID Connect response cannot contain an identity token " + "or an access token when using response_mode=query"); return null; } } // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(Request.ContentType) // May have media/type; charset=utf-8, allow partial match. && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) && Request.Body.CanRead) { var form = await Request.ReadFormAsync(); Request.Body.Seek(0, SeekOrigin.Begin); message = new OpenIdConnectMessage(form); } if (message == null) { return null; } try { var messageReceivedNotification = await RunMessageReceivedNotificationAsync(message); if (messageReceivedNotification.HandledResponse) { return messageReceivedNotification.AuthenticationTicket; } else if (messageReceivedNotification.Skipped) { return null; } var properties = new AuthenticationProperties(); // if state is missing, just log it if (string.IsNullOrEmpty(message.State)) { Logger.LogWarning(Resources.OIDCH_0004_MessageStateIsNullOrEmpty); } else { // if state exists and we failed to 'unprotect' this is not a message we should process. properties = Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(message.State)); if (properties == null) { Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); return null; } string userstate = null; properties.Items.TryGetValue(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, out userstate); message.State = userstate; } // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(message.Error)) { Logger.LogError(Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null")); } if (_configuration == null && Options.ConfigurationManager != null) { Logger.LogVerbose(Resources.OIDCH_0007_UpdatingConfiguration); _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } if (string.IsNullOrEmpty(message.IdToken) && !string.IsNullOrEmpty(message.Code)) { return await HandleCodeOnlyFlow(message, properties); } else if (!string.IsNullOrEmpty(message.IdToken)) { return await HandleIdTokenFlows(message, properties); } else { Logger.LogDebug(Resources.OIDCH_0045_Id_Token_Code_Missing); return null; } } catch (Exception exception) { Logger.LogError(Resources.OIDCH_0017_ExceptionOccurredWhileProcessingMessage, exception); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) { if (Options.ConfigurationManager != null) { Logger.LogVerbose(Resources.OIDCH_0021_AutomaticConfigurationRefresh); Options.ConfigurationManager.RequestRefresh(); } } var authenticationFailedNotification = await RunAuthenticationFailedNotificationAsync(message, exception); if (authenticationFailedNotification.HandledResponse) { return authenticationFailedNotification.AuthenticationTicket; } else if (authenticationFailedNotification.Skipped) { return null; } throw; } } private async Task HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties) { AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; OpenIdConnectTokenEndpointResponse tokenEndpointResponse = null; string idToken = null; var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt); if (authorizationCodeReceivedNotification.HandledResponse) { return authorizationCodeReceivedNotification.AuthenticationTicket; } else if (authorizationCodeReceivedNotification.Skipped) { return null; } // Redeeming authorization code for tokens Logger.LogDebug(Resources.OIDCH_0038_Redeeming_Auth_Code, message.Code); tokenEndpointResponse = await RedeemAuthorizationCodeAsync(message.Code, authorizationCodeReceivedNotification.RedirectUri); idToken = tokenEndpointResponse.Message.IdToken; var authorizationCodeRedeemedNotification = await RunAuthorizationCodeRedeemedNotificationAsync(message, tokenEndpointResponse); if (authorizationCodeRedeemedNotification.HandledResponse) { return authorizationCodeRedeemedNotification.AuthenticationTicket; } else if (authorizationCodeRedeemedNotification.Skipped) { return null; } // no need to validate signature when token is received using "code flow" as per spec [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. var validationParameters = Options.TokenValidationParameters.Clone(); validationParameters.ValidateSignature = false; ticket = ValidateToken(idToken, message, properties, validationParameters, out jwt); if (Options.GetClaimsFromUserInfoEndpoint) { Logger.LogDebug(Resources.OIDCH_0040_Sending_Request_UIEndpoint); ticket = await GetUserInformationAsync(properties, tokenEndpointResponse.Message, ticket); } await ValidateOpenIdConnectProtocolAsync(jwt, null); var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket); if (securityTokenValidatedNotification.HandledResponse) { return securityTokenValidatedNotification.AuthenticationTicket; } else if (securityTokenValidatedNotification.Skipped) { return null; } return ticket; } private async Task HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties) { AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; var securityTokenReceivedNotification = await RunSecurityTokenReceivedNotificationAsync(message); if (securityTokenReceivedNotification.HandledResponse) { return securityTokenReceivedNotification.AuthenticationTicket; } else if (securityTokenReceivedNotification.Skipped) { return null; } var validationParameters = Options.TokenValidationParameters.Clone(); ticket = ValidateToken(message.IdToken, message, properties, validationParameters, out jwt); await ValidateOpenIdConnectProtocolAsync(jwt, message); var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket); if (securityTokenValidatedNotification.HandledResponse) { return securityTokenValidatedNotification.AuthenticationTicket; } else if (securityTokenValidatedNotification.Skipped) { return null; } if (message.Code != null) { var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt); if (authorizationCodeReceivedNotification.HandledResponse) { return authorizationCodeReceivedNotification.AuthenticationTicket; } else if (authorizationCodeReceivedNotification.Skipped) { return null; } } return ticket; } /// /// Redeems the authorization code for tokens at the token endpoint /// /// The authorization code to redeem. /// Uri that was passed in the request sent for the authorization code. /// OpenIdConnect message that has tokens inside it. protected virtual async Task RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri) { var openIdMessage = new OpenIdConnectMessage() { ClientId = Options.ClientId, ClientSecret = Options.ClientSecret, Code = authorizationCode, GrantType = "authorization_code", RedirectUri = redirectUri }; var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); requestMessage.Content = new FormUrlEncodedContent(openIdMessage.Parameters); var responseMessage = await Backchannel.SendAsync(requestMessage); responseMessage.EnsureSuccessStatusCode(); var tokenResonse = await responseMessage.Content.ReadAsStringAsync(); var jsonTokenResponse = JObject.Parse(tokenResonse); return new OpenIdConnectTokenEndpointResponse(jsonTokenResponse); } /// /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity. /// /// Authentication Properties /// message that is being processed /// authentication ticket with claims principal and identities /// Authentication ticket with identity with additional claims, if any. protected virtual async Task GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket) { string userInfoEndpoint = null; if (_configuration != null) { userInfoEndpoint = _configuration.UserInfoEndpoint; } if (string.IsNullOrEmpty(userInfoEndpoint)) { Logger.LogWarning(Resources.OIDCH_0046_UserInfo_Endpoint_Not_Set); return ticket; } var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken); var responseMessage = await Backchannel.SendAsync(requestMessage); responseMessage.EnsureSuccessStatusCode(); var userInfoResponse = await responseMessage.Content.ReadAsStringAsync(); var user = JObject.Parse(userInfoResponse); var identity = (ClaimsIdentity)ticket.Principal.Identity; var subjectClaimType = identity.FindFirst(ClaimTypes.NameIdentifier); if (subjectClaimType == null) { Logger.LogError(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0041_Subject_Claim_Not_Found, identity.ToString())); return ticket; } var userInfoSubjectClaimValue = user.Value(JwtRegisteredClaimNames.Sub); // check if the sub claim matches if (userInfoSubjectClaimValue == null || !string.Equals(userInfoSubjectClaimValue, subjectClaimType.Value, StringComparison.Ordinal)) { Logger.LogError(Resources.OIDCH_0039_Subject_Claim_Mismatch); return ticket; } foreach (var claim in identity.Claims) { // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ? claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty; // checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint JToken value; var isClaimIncluded = user.TryGetValue(claim.Type, out value) || user.TryGetValue(shortClaimTypeName, out value); // if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal)) { if (!user.Remove(claim.Type)) { user.Remove(shortClaimTypeName); } } } // adding remaining unique claims from userinfo endpoint to the identity foreach (var pair in user) { JToken value; var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null; identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, Options.ClaimsIssuer)); } return new AuthenticationTicket(new ClaimsPrincipal(identity), ticket.Properties, ticket.AuthenticationScheme); } /// /// Adds the nonce to . /// /// the nonce to remember. /// is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)'. /// The value of the cookie is: "N". private void WriteNonceCookie(string nonce) { if (string.IsNullOrEmpty(nonce)) { throw new ArgumentNullException(nameof(nonce)); } Response.Cookies.Append( OpenIdConnectAuthenticationDefaults.CookieNoncePrefix + Options.StringDataFormat.Protect(nonce), NonceProperty, new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }); } /// /// Searches for a matching nonce. /// /// the nonce that we are looking for. /// echos 'nonce' if a cookie is found that matches, null otherwise. /// Examine that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. /// is used to obtain the actual 'nonce'. If the nonce is found, then is called. private string ReadNonceCookie(string nonce) { if (nonce == null) { return null; } foreach (var nonceKey in Request.Cookies.Keys) { if (nonceKey.StartsWith(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix)) { try { var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length)); if (nonceDecodedValue == nonce) { var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; Response.Cookies.Delete(nonceKey, cookieOptions); return nonce; } } catch (Exception ex) { Logger.LogWarning("Failed to un-protect the nonce cookie.", ex); } } } return null; } private AuthenticationProperties GetPropertiesFromState(string state) { // assume a well formed query string: OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d> var startIndex = 0; if (string.IsNullOrEmpty(state) || (startIndex = state.IndexOf(OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1) { return null; } var authenticationIndex = startIndex + OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey.Length; if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=') { return null; } // scan rest of string looking for '&' authenticationIndex++; var endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal); // -1 => no other parameters are after the AuthenticationPropertiesKey if (endIndex == -1) { return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' '))); } else { return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' '))); } } private async Task> RunMessageReceivedNotificationAsync(OpenIdConnectMessage message) { Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl()); var messageReceivedNotification = new MessageReceivedNotification(Context, Options) { ProtocolMessage = message }; await Options.Notifications.MessageReceived(messageReceivedNotification); if (messageReceivedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); } else if (messageReceivedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0003_MessageReceivedNotificationSkipped); } return messageReceivedNotification; } private async Task RunAuthorizationCodeReceivedNotificationAsync(OpenIdConnectMessage message, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt) { var redirectUri = properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ? properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : Options.RedirectUri; Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code); var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) { Code = message.Code, ProtocolMessage = message, RedirectUri = redirectUri, AuthenticationTicket = ticket, JwtSecurityToken = jwt }; await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); if (authorizationCodeReceivedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse); } else if (authorizationCodeReceivedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped); } return authorizationCodeReceivedNotification; } private async Task RunAuthorizationCodeRedeemedNotificationAsync(OpenIdConnectMessage message, OpenIdConnectTokenEndpointResponse tokenEndpointResponse) { Logger.LogDebug(Resources.OIDCH_0042_AuthorizationCodeRedeemed, message.Code); var authorizationCodeRedeemedNotification = new AuthorizationCodeRedeemedNotification(Context, Options) { Code = message.Code, ProtocolMessage = message, TokenEndpointResponse = tokenEndpointResponse }; await Options.Notifications.AuthorizationCodeRedeemed(authorizationCodeRedeemedNotification); if (authorizationCodeRedeemedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse); } else if (authorizationCodeRedeemedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped); } return authorizationCodeRedeemedNotification; } private async Task> RunSecurityTokenReceivedNotificationAsync(OpenIdConnectMessage message) { Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) { ProtocolMessage = message, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); if (securityTokenReceivedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); } else if (securityTokenReceivedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); } return securityTokenReceivedNotification; } private async Task> RunSecurityTokenValidatedNotificationAsync(OpenIdConnectMessage message, AuthenticationTicket ticket) { var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) { AuthenticationTicket = ticket, ProtocolMessage = message }; await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); if (securityTokenValidatedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); } else if (securityTokenValidatedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); } return securityTokenValidatedNotification; } private async Task> RunAuthenticationFailedNotificationAsync(OpenIdConnectMessage message, Exception exception) { var authenticationFailedNotification = new AuthenticationFailedNotification(Context, Options) { ProtocolMessage = message, Exception = exception }; await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); if (authenticationFailedNotification.HandledResponse) { Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); } else if (authenticationFailedNotification.Skipped) { Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); } return authenticationFailedNotification; } private AuthenticationTicket ValidateToken(string idToken, OpenIdConnectMessage message, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { AuthenticationTicket ticket = null; jwt = null; if (_configuration != null) { if (string.IsNullOrEmpty(validationParameters.ValidIssuer)) { validationParameters.ValidIssuer = _configuration.Issuer; } else if (!string.IsNullOrEmpty(_configuration.Issuer)) { validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; } validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; } SecurityToken validatedToken = null; ClaimsPrincipal principal = null; foreach (var validator in Options.SecurityTokenValidators) { if (validator.CanReadToken(idToken)) { principal = validator.ValidateToken(idToken, validationParameters, out validatedToken); jwt = validatedToken as JwtSecurityToken; if (jwt == null) { Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); } } } if (validatedToken == null) { Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, idToken); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, idToken)); } ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); if (!string.IsNullOrEmpty(message.SessionState)) { ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; } if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe)) { ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; } // Rename? if (Options.UseTokenLifetime) { var issued = validatedToken.ValidFrom; if (issued != DateTime.MinValue) { ticket.Properties.IssuedUtc = issued; } var expires = validatedToken.ValidTo; if (expires != DateTime.MinValue) { ticket.Properties.ExpiresUtc = expires; } } return ticket; } private async Task ValidateOpenIdConnectProtocolAsync(JwtSecurityToken jwt, OpenIdConnectMessage message) { string nonce = jwt.Payload.Nonce; if (Options.CacheNonces) { if (await Options.NonceCache.GetAsync(nonce) != null) { await Options.NonceCache.RemoveAsync(nonce); } else { // If the nonce cannot be removed, it was // already used and MUST be rejected. nonce = null; } } else { nonce = ReadNonceCookie(nonce); } var protocolValidationContext = new OpenIdConnectProtocolValidationContext { ProtocolMessage = message, IdToken = jwt, ClientId = Options.ClientId, Nonce = nonce }; Options.ProtocolValidator.Validate(protocolValidationContext); } /// /// Calls InvokeReplyPathAsync /// /// True if the request was handled, false if the next middleware should be invoked. public override Task InvokeAsync() { return InvokeReplyPathAsync(); } private async Task InvokeReplyPathAsync() { var ticket = await HandleAuthenticateOnceAsync(); if (ticket != null) { 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; } } }