// 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.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { /// /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. /// public class OpenIdConnectHandler : RemoteAuthenticationHandler { private const string NonceProperty = "N"; private const string UriSchemeDelimiter = "://"; private const string InputTagFormat = @""; private const string HtmlFormFormat = @" Please wait while you're being redirected to the identity provider
{1}
"; private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); private OpenIdConnectConfiguration _configuration; protected HttpClient Backchannel { get; private set; } protected HtmlEncoder HtmlEncoder { get; private set; } public OpenIdConnectHandler(HttpClient backchannel, HtmlEncoder htmlEncoder) { Backchannel = backchannel; HtmlEncoder = htmlEncoder; } /// /// 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), }; // Set End_Session_Endpoint in order: // 1. properties.Redirect // 2. Options.PostLogoutRedirectUri var properties = new AuthenticationProperties(signout.Properties); var logoutRedirectUri = properties.RedirectUri; if (!string.IsNullOrEmpty(logoutRedirectUri)) { // Relative to PathBase if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal)) { logoutRedirectUri = BuildRedirectUri(logoutRedirectUri); } message.PostLogoutRedirectUri = logoutRedirectUri; } else if (!string.IsNullOrEmpty(Options.PostLogoutRedirectUri)) { logoutRedirectUri = Options.PostLogoutRedirectUri; // Relative to PathBase if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal)) { logoutRedirectUri = BuildRedirectUri(logoutRedirectUri); } message.PostLogoutRedirectUri = logoutRedirectUri; } message.IdTokenHint = await Context.Authentication.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var redirectContext = new RedirectContext(Context, Options, properties) { ProtocolMessage = message }; await Options.Events.RedirectToIdentityProviderForSignOut(redirectContext); if (redirectContext.HandledResponse) { Logger.RedirectToIdentityProviderForSignOutHandledResponse(); return; } else if (redirectContext.Skipped) { Logger.RedirectToIdentityProviderForSignOutSkipped(); return; } message = redirectContext.ProtocolMessage; if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) { var redirectUri = message.CreateLogoutRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri); } Response.Redirect(redirectUri); } else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) { var inputs = new StringBuilder(); foreach (var parameter in message.Parameters) { var name = HtmlEncoder.Encode(parameter.Key); var value = HtmlEncoder.Encode(parameter.Value); var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); inputs.AppendLine(input); } var issuer = HtmlEncoder.Encode(message.IssuerAddress); var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); var buffer = Encoding.UTF8.GetBytes(content); Response.ContentLength = buffer.Length; Response.ContentType = "text/html;charset=UTF-8"; // Emit Cache-Control=no-cache to prevent client caching. Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = "-1"; await Response.Body.WriteAsync(buffer, 0, buffer.Length); } } } /// /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. /// /// protected override async Task HandleUnauthorizedAsync(ChallengeContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName); // order for local RedirectUri // 1. challenge.Properties.RedirectUri // 2. CurrentUri if RedirectUri is not set) var properties = new AuthenticationProperties(context.Properties) { ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout) }; if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = CurrentUri; } Logger.PostAuthenticationLocalRedirect(properties.RedirectUri); 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 = BuildRedirectUri(Options.CallbackPath), Resource = Options.Resource, ResponseType = Options.ResponseType, Scope = string.Join(" ", 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(); WriteNonceCookie(message.Nonce); } GenerateCorrelationId(properties); var redirectContext = new RedirectContext(Context, Options, properties) { ProtocolMessage = message }; await Options.Events.RedirectToIdentityProvider(redirectContext); if (redirectContext.HandledResponse) { Logger.RedirectToIdentityProviderHandledResponse(); return true; } else if (redirectContext.Skipped) { Logger.RedirectToIdentityProviderSkipped(); return false; } message = redirectContext.ProtocolMessage; if (!string.IsNullOrEmpty(message.State)) { properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State; } // When redeeming a 'code' for an AccessToken, this value is needed properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri); message.State = Options.StateDataFormat.Protect(properties); if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) { var redirectUri = message.CreateAuthenticationRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.InvalidAuthenticationRequestUrl(redirectUri); } Response.Redirect(redirectUri); return true; } else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) { var inputs = new StringBuilder(); foreach (var parameter in message.Parameters) { var name = HtmlEncoder.Encode(parameter.Key); var value = HtmlEncoder.Encode(parameter.Value); var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); inputs.AppendLine(input); } var issuer = HtmlEncoder.Encode(message.IssuerAddress); var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); var buffer = Encoding.UTF8.GetBytes(content); Response.ContentLength = buffer.Length; Response.ContentType = "text/html;charset=UTF-8"; // Emit Cache-Control=no-cache to prevent client caching. Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = "-1"; await Response.Body.WriteAsync(buffer, 0, buffer.Length); return true; } throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); } /// /// Invoked to process incoming OpenIdConnect messages. /// /// An if successful. protected override async Task HandleRemoteAuthenticateAsync() { Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName); OpenIdConnectMessage authorizationResponse = null; if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); // 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(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken)) { if (Options.SkipUnrecognizedRequests) { // Not for us? return AuthenticateResult.Skip(); } return AuthenticateResult.Fail("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. 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(); authorizationResponse = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); } if (authorizationResponse == null) { if (Options.SkipUnrecognizedRequests) { // Not for us? return AuthenticateResult.Skip(); } return AuthenticateResult.Fail("No message."); } AuthenticateResult result; try { AuthenticationProperties properties = null; if (!string.IsNullOrEmpty(authorizationResponse.State)) { properties = Options.StateDataFormat.Unprotect(authorizationResponse.State); } var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties); if (messageReceivedContext.CheckEventResult(out result)) { return result; } authorizationResponse = messageReceivedContext.ProtocolMessage; properties = messageReceivedContext.Properties; if (properties == null) { // Fail if state is missing, it's required for the correlation id. if (string.IsNullOrEmpty(authorizationResponse.State)) { // This wasn't a valid OIDC message, it may not have been intended for us. Logger.NullOrEmptyAuthorizationResponseState(); if (Options.SkipUnrecognizedRequests) { return AuthenticateResult.Skip(); } return AuthenticateResult.Fail(Resources.MessageStateIsNullOrEmpty); } // if state exists and we failed to 'unprotect' this is not a message we should process. properties = Options.StateDataFormat.Unprotect(authorizationResponse.State); } if (properties == null) { Logger.UnableToReadAuthorizationResponseState(); if (Options.SkipUnrecognizedRequests) { // Not for us? return AuthenticateResult.Skip(); } return AuthenticateResult.Fail(Resources.MessageStateIsInvalid); } string userstate = null; properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out userstate); authorizationResponse.State = userstate; if (!ValidateCorrelationId(properties)) { return AuthenticateResult.Fail("Correlation failed."); } // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(authorizationResponse.Error)) { Logger.AuthorizationResponseError( authorizationResponse.Error, authorizationResponse.ErrorDescription ?? "ErrorDecription null", authorizationResponse.ErrorUri ?? "ErrorUri null"); return AuthenticateResult.Fail(new OpenIdConnectProtocolException( string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, authorizationResponse.Error, authorizationResponse.ErrorDescription ?? "ErrorDecription null", authorizationResponse.ErrorUri ?? "ErrorUri null"))); } if (_configuration == null && Options.ConfigurationManager != null) { Logger.UpdatingConfiguration(); _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } PopulateSessionProperties(authorizationResponse, properties); AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; string nonce = null; var validationParameters = Options.TokenValidationParameters.Clone(); // Hybrid or Implicit flow if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { Logger.ReceivedIdToken(); ticket = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); nonce = jwt?.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) { nonce = ReadNonceCookie(nonce); } var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, properties, ticket, jwt, nonce); if (tokenValidatedContext.CheckEventResult(out result)) { return result; } authorizationResponse = tokenValidatedContext.ProtocolMessage; properties = tokenValidatedContext.Properties; ticket = tokenValidatedContext.Ticket; jwt = tokenValidatedContext.SecurityToken; nonce = tokenValidatedContext.Nonce; } Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, ProtocolMessage = authorizationResponse, ValidatedIdToken = jwt, Nonce = nonce }); OpenIdConnectMessage tokenEndpointResponse = null; // Authorization Code or Hybrid flow if (!string.IsNullOrEmpty(authorizationResponse.Code)) { var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, properties, ticket, jwt); if (authorizationCodeReceivedContext.CheckEventResult(out result)) { return result; } authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage; properties = authorizationCodeReceivedContext.Properties; var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest; // If the developer redeemed the code themselves... tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse; ticket = authorizationCodeReceivedContext.Ticket; jwt = authorizationCodeReceivedContext.JwtSecurityToken; if (!authorizationCodeReceivedContext.HandledCodeRedemption) { tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest); } var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties); if (tokenResponseReceivedContext.CheckEventResult(out result)) { return result; } authorizationResponse = tokenResponseReceivedContext.ProtocolMessage; tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse; // We only have to process the IdToken if we didn't already get one in the AuthorizationResponse if (ticket == 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]. validationParameters.RequireSignedTokens = false; ticket = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out jwt); nonce = jwt?.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) { nonce = ReadNonceCookie(nonce); } var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, properties, ticket, jwt, nonce); if (tokenValidatedContext.CheckEventResult(out result)) { return result; } authorizationResponse = tokenValidatedContext.ProtocolMessage; tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse; properties = tokenValidatedContext.Properties; ticket = tokenValidatedContext.Ticket; jwt = tokenValidatedContext.SecurityToken; nonce = tokenValidatedContext.Nonce; } // Validate the token response if it wasn't provided manually if (!authorizationCodeReceivedContext.HandledCodeRedemption) { Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, ProtocolMessage = tokenEndpointResponse, ValidatedIdToken = jwt, Nonce = nonce }); } } if (Options.SaveTokens) { SaveTokens(ticket.Properties, tokenEndpointResponse ?? authorizationResponse); } if (Options.GetClaimsFromUserInfoEndpoint) { return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, ticket); } return AuthenticateResult.Success(ticket); } catch (Exception exception) { Logger.ExceptionProcessingMessage(exception); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) { if (Options.ConfigurationManager != null) { Logger.ConfigurationManagerRequestRefreshCalled(); Options.ConfigurationManager.RequestRefresh(); } } var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception); if (authenticationFailedContext.CheckEventResult(out result)) { return result; } return AuthenticateResult.Fail(exception); } } private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties) { if (!string.IsNullOrEmpty(message.SessionState)) { properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; } if (!string.IsNullOrEmpty(_configuration.CheckSessionIframe)) { properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; } } /// /// Redeems the authorization code for tokens at the token endpoint /// /// The request that will be sent to the token endpoint and is available for customization. /// OpenIdConnect message that has tokens inside it. protected virtual async Task RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest) { Logger.RedeemingCodeForTokens(); var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters); var responseMessage = await Backchannel.SendAsync(requestMessage); responseMessage.EnsureSuccessStatusCode(); var tokenResonse = await responseMessage.Content.ReadAsStringAsync(); var jsonTokenResponse = JObject.Parse(tokenResonse); return new OpenIdConnectMessage(jsonTokenResponse); } /// /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity. /// /// message that is being processed /// The . /// authentication ticket with claims principal and identities /// Authentication ticket with identity with additional claims, if any. protected virtual async Task GetUserInformationAsync(OpenIdConnectMessage message, JwtSecurityToken jwt, AuthenticationTicket ticket) { var userInfoEndpoint = _configuration?.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { Logger.UserInfoEndpointNotSet(); return AuthenticateResult.Success(ticket); } if (string.IsNullOrEmpty(message.AccessToken)) { Logger.AccessTokenNotAvailable(); return AuthenticateResult.Success(ticket); } Logger.RetrievingClaims(); 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(); JObject user; var contentType = responseMessage.Content.Headers.ContentType; if (contentType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase)) { user = JObject.Parse(userInfoResponse); } else if (contentType.MediaType.Equals("application/jwt", StringComparison.OrdinalIgnoreCase)) { var userInfoEndpointJwt = new JwtSecurityToken(userInfoResponse); user = JObject.FromObject(userInfoEndpointJwt.Payload); } else { return AuthenticateResult.Fail("Unknown response type: " + contentType.MediaType); } var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(ticket, message, user); AuthenticateResult result; if (userInformationReceivedContext.CheckEventResult(out result)) { return result; } ticket = userInformationReceivedContext.Ticket; user = userInformationReceivedContext.User; Options.ProtocolValidator.ValidateUserInfoResponse(new OpenIdConnectProtocolValidationContext() { UserInfoEndpointResponse = userInfoResponse, ValidatedIdToken = jwt, }); var identity = (ClaimsIdentity)ticket.Principal.Identity; 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, jwt.Issuer)); } return AuthenticateResult.Success(ticket); } /// /// Save the tokens contained in the in the . /// /// The in which tokens are saved. /// The OpenID Connect response. private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) { var tokens = new List(); if (!string.IsNullOrEmpty(message.AccessToken)) { tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken }); } if (!string.IsNullOrEmpty(message.IdToken)) { tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken }); } if (!string.IsNullOrEmpty(message.RefreshToken)) { tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken }); } if (!string.IsNullOrEmpty(message.TokenType)) { tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType }); } if (!string.IsNullOrEmpty(message.ExpiresIn)) { int value; if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) { var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }); } } properties.StoreTokens(tokens); } /// /// Adds the nonce to . /// /// the nonce to remember. /// of is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)' of . /// The value of the cookie is: "N". private void WriteNonceCookie(string nonce) { if (string.IsNullOrEmpty(nonce)) { throw new ArgumentNullException(nameof(nonce)); } Response.Cookies.Append( OpenIdConnectDefaults.CookieNoncePrefix + Options.StringDataFormat.Protect(nonce), NonceProperty, new CookieOptions { HttpOnly = true, Secure = Request.IsHttps, Expires = Options.SystemClock.UtcNow.Add(Options.ProtocolValidator.NonceLifetime) }); } /// /// Searches for a matching nonce. /// /// the nonce that we are looking for. /// echos 'nonce' if a cookie is found that matches, null otherwise. /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. /// of is used to obtain the actual 'nonce'. If the nonce is found, then of is called. private string ReadNonceCookie(string nonce) { if (nonce == null) { return null; } foreach (var nonceKey in Request.Cookies.Keys) { if (nonceKey.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)) { try { var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectDefaults.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.UnableToProtectNonceCookie(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(OpenIdConnectDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1) { return null; } var authenticationIndex = startIndex + OpenIdConnectDefaults.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 RunMessageReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties) { Logger.MessageReceived(message.BuildRedirectUrl()); var messageReceivedContext = new MessageReceivedContext(Context, Options) { ProtocolMessage = message, Properties = properties, }; await Options.Events.MessageReceived(messageReceivedContext); if (messageReceivedContext.HandledResponse) { Logger.MessageReceivedContextHandledResponse(); } else if (messageReceivedContext.Skipped) { Logger.MessageReceivedContextSkipped(); } return messageReceivedContext; } private async Task RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt, string nonce) { var tokenValidatedContext = new TokenValidatedContext(Context, Options) { ProtocolMessage = authorizationResponse, TokenEndpointResponse = tokenEndpointResponse, Properties = properties, Ticket = ticket, SecurityToken = jwt, Nonce = nonce, }; await Options.Events.TokenValidated(tokenValidatedContext); if (tokenValidatedContext.HandledResponse) { Logger.TokenValidatedHandledResponse(); } else if (tokenValidatedContext.Skipped) { Logger.TokenValidatedSkipped(); } return tokenValidatedContext; } private async Task RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt) { Logger.AuthorizationCodeReceived(); var tokenEndpointRequest = new OpenIdConnectMessage() { ClientId = Options.ClientId, ClientSecret = Options.ClientSecret, Code = authorizationResponse.Code, GrantType = OpenIdConnectGrantTypes.AuthorizationCode, RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey] }; var authorizationCodeReceivedContext = new AuthorizationCodeReceivedContext(Context, Options) { ProtocolMessage = authorizationResponse, Properties = properties, TokenEndpointRequest = tokenEndpointRequest, Ticket = ticket, JwtSecurityToken = jwt, Backchannel = Backchannel, }; await Options.Events.AuthorizationCodeReceived(authorizationCodeReceivedContext); if (authorizationCodeReceivedContext.HandledResponse) { Logger.AuthorizationCodeReceivedContextHandledResponse(); } else if (authorizationCodeReceivedContext.Skipped) { Logger.AuthorizationCodeReceivedContextSkipped(); } return authorizationCodeReceivedContext; } private async Task RunTokenResponseReceivedEventAsync(OpenIdConnectMessage message, OpenIdConnectMessage tokenEndpointResponse, AuthenticationProperties properties) { Logger.TokenResponseReceived(); var eventContext = new TokenResponseReceivedContext(Context, Options, properties) { ProtocolMessage = message, TokenEndpointResponse = tokenEndpointResponse }; await Options.Events.TokenResponseReceived(eventContext); if (eventContext.HandledResponse) { Logger.TokenResponseReceivedHandledResponse(); } else if (eventContext.Skipped) { Logger.TokenResponseReceivedSkipped(); } return eventContext; } private async Task RunUserInformationReceivedEventAsync(AuthenticationTicket ticket, OpenIdConnectMessage message, JObject user) { Logger.UserInformationReceived(user.ToString()); var userInformationReceivedContext = new UserInformationReceivedContext(Context, Options) { Ticket = ticket, ProtocolMessage = message, User = user, }; await Options.Events.UserInformationReceived(userInformationReceivedContext); if (userInformationReceivedContext.HandledResponse) { Logger.UserInformationReceivedHandledResponse(); } else if (userInformationReceivedContext.Skipped) { Logger.UserInformationReceivedSkipped(); } return userInformationReceivedContext; } private async Task RunAuthenticationFailedEventAsync(OpenIdConnectMessage message, Exception exception) { var authenticationFailedContext = new AuthenticationFailedContext(Context, Options) { ProtocolMessage = message, Exception = exception }; await Options.Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.HandledResponse) { Logger.AuthenticationFailedContextHandledResponse(); } else if (authenticationFailedContext.Skipped) { Logger.AuthenticationFailedContextSkipped(); } return authenticationFailedContext; } private AuthenticationTicket ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { if (!Options.SecurityTokenValidator.CanReadToken(idToken)) { Logger.UnableToReadIdToken(idToken); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); } 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; var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out validatedToken); jwt = validatedToken as JwtSecurityToken; if (jwt == null) { Logger.InvalidSecurityTokenType(validatedToken?.GetType().ToString()); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); } if (validatedToken == null) { Logger.UnableToValidateIdToken(idToken); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); } var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); 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; } } }