From c1e6c3322c43e354fe6c14aa665976d29d6a5a9c Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 2 Jul 2019 19:17:05 -0600 Subject: [PATCH 1/6] Add response_mode=query support for OpenID Connect --- .../OpenIdConnectAuthenticationOptions.cs | 7 +++ .../OpenidConnectAuthenticationHandler.cs | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index 820f51c3..8c01c6cc 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -41,6 +41,7 @@ public OpenIdConnectAuthenticationOptions() /// Caption: . /// ProtocolValidator: new . /// RefreshOnIssuerKeyNotFound: true + /// ResponseMode: /// ResponseType: /// Scope: . /// TokenValidationParameters: new with AuthenticationType = authenticationType. @@ -60,6 +61,7 @@ public OpenIdConnectAuthenticationOptions(string authenticationType) NonceLifetime = TimeSpan.FromMinutes(15) }; RefreshOnIssuerKeyNotFound = true; + ResponseMode = OpenIdConnectResponseMode.FormPost; ResponseType = OpenIdConnectResponseType.CodeIdToken; Scope = OpenIdConnectScope.OpenIdProfile; SecurityTokenValidator = new JwtSecurityTokenHandler(); @@ -216,6 +218,11 @@ public OpenIdConnectProtocolValidator ProtocolValidator /// public string Resource { get; set; } + /// + /// Gets or sets the 'response_mode'. + /// + public string ResponseMode { get; set; } + /// /// Gets or sets the 'response_type'. /// diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 8ee87874..3062fc2b 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -145,12 +145,20 @@ protected override async Task ApplyResponseChallengeAsync() RedirectUri = Options.RedirectUri, RequestType = OpenIdConnectRequestType.Authentication, Resource = Options.Resource, - ResponseMode = OpenIdConnectResponseMode.FormPost, ResponseType = Options.ResponseType, Scope = Options.Scope, State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Uri.EscapeDataString(Options.StateDataFormat.Protect(properties)), }; + // 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, OpenIdConnectResponseType.Code, StringComparison.Ordinal) || + !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal)) + { + openIdConnectMessage.ResponseMode = Options.ResponseMode; + } + if (Options.ProtocolValidator.RequireNonce) { AddNonceToMessage(openIdConnectMessage); @@ -191,8 +199,39 @@ protected override async Task AuthenticateCoreAsync() OpenIdConnectMessage openIdConnectMessage = null; + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + openIdConnectMessage = 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(openIdConnectMessage.IdToken) || !string.IsNullOrEmpty(openIdConnectMessage.AccessToken)) + { + var invalidResponseEx = new OpenIdConnectProtocolException("An OpenID Connect response cannot contain an identity token or an access token when using response_mode=query"); + + _logger.WriteError("Exception occurred while processing message: ", invalidResponseEx); + + var authenticationFailedNotification = new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage, + Exception = invalidResponseEx + }; + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (authenticationFailedNotification.Skipped) + { + return null; + } + + throw invalidResponseEx; + } + } // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. - if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) + else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(Request.ContentType) // May have media/type; charset=utf-8, allow partial match. && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) From cc39b8720c1bcdf63cf0310d7f342c6991ccaf33 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 9 Jul 2019 11:24:50 -0600 Subject: [PATCH 2/6] Add authorization code redemption for OpenID Connect --- .../AuthorizationCodeReceivedNotification.cs | 5 + .../OpenIdConnectAuthenticationMiddleware.cs | 15 +- .../OpenIdConnectAuthenticationOptions.cs | 13 ++ .../OpenidConnectAuthenticationHandler.cs | 199 +++++++++++++++--- tests/Katana.Sandbox.WebServer/Startup.cs | 5 +- 5 files changed, 204 insertions(+), 33 deletions(-) diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs index d1e3a627..0ad6131e 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs @@ -36,6 +36,11 @@ public AuthorizationCodeReceivedNotification(IOwinContext context, OpenIdConnect /// public JwtSecurityToken JwtSecurityToken { get; set; } + /// + /// The request that will be sent to the token endpoint and is available for customization. + /// + public OpenIdConnectMessage TokenEndpointRequest { get; set; } + /// /// Gets or sets the . /// diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index dbd0cb08..1a5bd163 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -67,6 +67,14 @@ public OpenIdConnectAuthenticationMiddleware(OwinMiddleware next, IAppBuilder ap Options.TokenValidationParameters.ValidAudience = Options.ClientId; } + if (Options.Backchannel == null) + { + Options.Backchannel = new HttpClient(ResolveHttpMessageHandler(Options)); + Options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect middleware"); + Options.Backchannel.Timeout = Options.BackchannelTimeout; + Options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + if (Options.ConfigurationManager == null) { if (Options.Configuration != null) @@ -91,13 +99,8 @@ public OpenIdConnectAuthenticationMiddleware(OwinMiddleware next, IAppBuilder ap throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); } - var backchannel = new HttpClient(ResolveHttpMessageHandler(Options)); - backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect middleware"); - backchannel.Timeout = Options.BackchannelTimeout; - backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever(backchannel) { RequireHttps = Options.RequireHttpsMetadata }); + new HttpDocumentRetriever(Options.Backchannel) { RequireHttps = Options.RequireHttpsMetadata }); } } diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index 8c01c6cc..95c21e09 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -124,6 +124,11 @@ public TimeSpan BackchannelTimeout } } + /// + /// Used to communicate with the remote identity provider. + /// + public HttpClient Backchannel { get; set; } + /// /// Get or sets the text that the user can display on a sign in user interface. /// @@ -297,6 +302,14 @@ public bool UseTokenLifetime set; } + /// + /// Defines whether access and refresh tokens should be stored in the + /// after a successful authorization. + /// This property is set to false by default to reduce + /// the size of the final authentication cookie. + /// + public bool SaveTokens { get; set; } + /// /// An abstraction for reading and setting cookies during the authentication process. /// diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 3062fc2b..ec150720 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -7,6 +7,7 @@ using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; +using System.Net.Http; using System.Runtime.ExceptionServices; using System.Security.Claims; using System.Security.Cryptography; @@ -31,6 +32,14 @@ public class OpenIdConnectAuthenticationHandler : AuthenticationHandler /// Creates a new OpenIdConnectAuthenticationHandler /// @@ -199,7 +208,7 @@ protected override async Task AuthenticateCoreAsync() OpenIdConnectMessage openIdConnectMessage = null; - if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase) && Request.Query.Any()) { openIdConnectMessage = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); @@ -288,18 +297,52 @@ protected override async Task AuthenticateCoreAsync() // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error)) { - throw new OpenIdConnectProtocolException( - string.Format(CultureInfo.InvariantCulture, - Resources.Exception_OpenIdConnectMessageError, - openIdConnectMessage.Error, openIdConnectMessage.ErrorDescription ?? string.Empty, openIdConnectMessage.ErrorUri ?? string.Empty)); + throw CreateOpenIdConnectProtocolException(openIdConnectMessage); } - // code is only accepted with id_token, in this version, hence check for code is inside this if - // OpenIdConnect protocol allows a Code to be received without the id_token - if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken)) + if (_configuration == null) { - _logger.WriteWarning("The id_token is missing."); - return null; + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled); + } + + PopulateSessionProperties(openIdConnectMessage, properties); + + // Authorization Code Flow + if (openIdConnectMessage.Code != null && string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken)) + { + var tokenEndpointRequest = new OpenIdConnectMessage() + { + ClientId = Options.ClientId, + ClientSecret = Options.ClientSecret, + Code = openIdConnectMessage.Code, + GrantType = OpenIdConnectGrantTypes.AuthorizationCode, + RedirectUri = properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] + }; + + var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) + { + Code = openIdConnectMessage.Code, + ProtocolMessage = openIdConnectMessage, + RedirectUri = properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? + properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, + TokenEndpointRequest = tokenEndpointRequest + }; + await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); + if (authorizationCodeReceivedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (authorizationCodeReceivedNotification.Skipped) + { + return null; + } + + openIdConnectMessage = await RedeemAuthorizationCodeAsync(authorizationCodeReceivedNotification.TokenEndpointRequest); + } + + if (Options.SaveTokens) + { + SaveTokens(properties, openIdConnectMessage); } var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) @@ -316,11 +359,6 @@ protected override async Task AuthenticateCoreAsync() return null; } - if (_configuration == null) - { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled); - } - // Copy and augment to avoid cross request race conditions for updated configurations. TokenValidationParameters tvp = Options.TokenValidationParameters.Clone(); IEnumerable issuers = new[] { _configuration.Issuer }; @@ -347,17 +385,6 @@ protected override async Task AuthenticateCoreAsync() nonce = RetrieveNonce(openIdConnectMessage); } - // remember 'session_state' and 'check_session_iframe' - if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState)) - { - ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState; - } - - if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) - { - ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; - } - if (Options.UseTokenLifetime) { // Override any session persistence to match the token lifetime. @@ -462,6 +489,91 @@ protected override async Task AuthenticateCoreAsync() return null; } + /// + /// 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) + { + var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); + requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters); + + var responseMessage = await Backchannel.SendAsync(requestMessage); + + var contentMediaType = responseMessage.Content.Headers.ContentType != null ? responseMessage.Content.Headers.ContentType.MediaType : null; + if (string.IsNullOrEmpty(contentMediaType)) + { + _logger.WriteVerbose(string.Format("Unexpected token response format. Status Code: {0}. Content-Type header is missing.", (int)responseMessage.StatusCode)); + } + else if (!string.Equals(contentMediaType, "application/json", StringComparison.OrdinalIgnoreCase)) + { + _logger.WriteVerbose(string.Format("Unexpected token response format. Status Code: {0}. Content-Type {1}.", (int)responseMessage.StatusCode, responseMessage.Content.Headers.ContentType)); + } + + // Error handling: + // 1. If the response body can't be parsed as json, throws. + // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed, + // pass the error information from body to the exception. + OpenIdConnectMessage message; + try + { + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + message = new OpenIdConnectMessage(responseContent); + } + catch (Exception ex) + { + throw new OpenIdConnectProtocolException(string.Format("Failed to parse token response body as JSON. Status Code: {0}. Content-Type: {1}", (int)responseMessage.StatusCode, responseMessage.Content.Headers.ContentType), ex); + } + + if (!responseMessage.IsSuccessStatusCode) + { + throw CreateOpenIdConnectProtocolException(message); + } + + return message; + } + + /// + /// Save the tokens contained in the in the . + /// + /// The in which tokens are saved. + /// The OpenID Connect response. + private static void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) + { + if (!string.IsNullOrEmpty(message.AccessToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.AccessToken] = message.AccessToken; + } + + if (!string.IsNullOrEmpty(message.IdToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.IdToken] = message.IdToken; + } + + if (!string.IsNullOrEmpty(message.RefreshToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.RefreshToken] = message.RefreshToken; + } + + if (!string.IsNullOrEmpty(message.TokenType)) + { + properties.Dictionary[OpenIdConnectParameterNames.TokenType] = message.TokenType; + } + + if (!string.IsNullOrEmpty(message.ExpiresIn)) + { + int value; + if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + { + var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(value); + // https://www.w3.org/TR/xmlschema-2/#dateTime + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx + properties.Dictionary["expires_at"] = expiresAt.ToString("o", CultureInfo.InvariantCulture); + } + } + } + /// /// Sets to . /// @@ -613,6 +725,41 @@ private AuthenticationProperties GetPropertiesFromState(string state) } } + private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties) + { + // remember 'session_state' and 'check_session_iframe' + if (!string.IsNullOrWhiteSpace(message.SessionState)) + { + properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = message.SessionState; + } + + if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) + { + properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; + } + } + + private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message) + { + var description = message.ErrorDescription ?? "error_description is null"; + var errorUri = message.ErrorUri ?? "error_uri is null"; + + var errorMessage = string.Format( + CultureInfo.InvariantCulture, + Resources.Exception_OpenIdConnectMessageError, + message.Error, + description, + errorUri); + + _logger.WriteError(errorMessage); + + var ex = new OpenIdConnectProtocolException(errorMessage); + ex.Data["error"] = message.Error; + ex.Data["error_description"] = description; + ex.Data["error_uri"] = errorUri; + return ex; + } + /// /// Calls InvokeReplyPathAsync /// diff --git a/tests/Katana.Sandbox.WebServer/Startup.cs b/tests/Katana.Sandbox.WebServer/Startup.cs index 2a89815d..cdeb417b 100644 --- a/tests/Katana.Sandbox.WebServer/Startup.cs +++ b/tests/Katana.Sandbox.WebServer/Startup.cs @@ -132,7 +132,10 @@ public void Configuration(IAppBuilder app) Authority = Environment.GetEnvironmentVariable("oidc:authority"), ClientId = Environment.GetEnvironmentVariable("oidc:clientid"), RedirectUri = "https://localhost:44318/", - CookieManager = new SystemWebCookieManager() + CookieManager = new SystemWebCookieManager(), + //ResponseType = "code", + //ResponseMode = "query", + //SaveTokens = true }); /* From 37dad56a1a68196e79863a85e6ea8b32f0a63031 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 9 Jul 2019 16:23:05 -0600 Subject: [PATCH 3/6] Fixed response validation issue --- .../OpenidConnectAuthenticationHandler.cs | 255 ++++++++++++------ .../Resources.Designer.cs | 22 +- .../Resources.resx | 6 + 3 files changed, 194 insertions(+), 89 deletions(-) diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index ec150720..07a6b0b1 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -307,6 +307,51 @@ protected override async Task AuthenticateCoreAsync() PopulateSessionProperties(openIdConnectMessage, properties); + string nonce = null; + ClaimsPrincipal user = null; + JwtSecurityToken jwt = null; + // Copy and augment to avoid cross request race conditions for updated configurations. + var validationParameters = Options.TokenValidationParameters.Clone(); + + // Hybrid or Implicit flow + if (!string.IsNullOrEmpty(openIdConnectMessage.IdToken)) + { + var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage, + }; + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (securityTokenReceivedNotification.Skipped) + { + return null; + } + + user = ValidateToken(openIdConnectMessage.IdToken, validationParameters, out jwt); + } + + if (Options.ProtocolValidator.RequireNonce) + { + if (string.IsNullOrWhiteSpace(openIdConnectMessage.Nonce) && jwt != null) + { + openIdConnectMessage.Nonce = jwt.Payload.Nonce; + } + + // deletes the nonce cookie + nonce = RetrieveNonce(openIdConnectMessage); + } + + Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() + { + ClientId = Options.ClientId, + ProtocolMessage = openIdConnectMessage, + ValidatedIdToken = jwt, + Nonce = nonce + }); + // Authorization Code Flow if (openIdConnectMessage.Code != null && string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken)) { @@ -338,53 +383,60 @@ protected override async Task AuthenticateCoreAsync() } openIdConnectMessage = await RedeemAuthorizationCodeAsync(authorizationCodeReceivedNotification.TokenEndpointRequest); - } - - if (Options.SaveTokens) - { - SaveTokens(properties, openIdConnectMessage); - } - - var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) - { - ProtocolMessage = openIdConnectMessage, - }; - await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); - if (securityTokenReceivedNotification.HandledResponse) - { - return GetHandledResponseTicket(); - } - if (securityTokenReceivedNotification.Skipped) - { - return null; - } - // Copy and augment to avoid cross request race conditions for updated configurations. - TokenValidationParameters tvp = Options.TokenValidationParameters.Clone(); - IEnumerable issuers = new[] { _configuration.Issuer }; - tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); - tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage, + }; + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (securityTokenReceivedNotification.Skipped) + { + return null; + } - SecurityToken validatedToken; - ClaimsPrincipal principal = Options.SecurityTokenValidator.ValidateToken(openIdConnectMessage.IdToken, tvp, out validatedToken); - ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity; + // 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; - // claims principal could have changed claim values, use bits received on wire for validation. - JwtSecurityToken jwt = validatedToken as JwtSecurityToken; - AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties); + // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. + // And we'll want to validate the new JWT in ValidateTokenResponse. + user = ValidateToken(openIdConnectMessage.IdToken, validationParameters, out jwt); - string nonce = null; - if (Options.ProtocolValidator.RequireNonce) - { - if (String.IsNullOrWhiteSpace(openIdConnectMessage.Nonce)) + if (Options.ProtocolValidator.RequireNonce) { - openIdConnectMessage.Nonce = jwt.Payload.Nonce; + if (string.IsNullOrWhiteSpace(openIdConnectMessage.Nonce) && jwt != null) + { + openIdConnectMessage.Nonce = jwt.Payload.Nonce; + } + + // deletes the nonce cookie + if (nonce == null) + { + nonce = RetrieveNonce(openIdConnectMessage); + } } - // deletes the nonce cookie - nonce = RetrieveNonce(openIdConnectMessage); + Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() + { + ClientId = Options.ClientId, + ProtocolMessage = openIdConnectMessage, + ValidatedIdToken = jwt, + Nonce = nonce + }); + } + + if (Options.SaveTokens) + { + SaveTokens(properties, openIdConnectMessage); } + ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; + AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties); + if (Options.UseTokenLifetime) { // Override any session persistence to match the token lifetime. @@ -418,14 +470,6 @@ protected override async Task AuthenticateCoreAsync() // Flow possible changes ticket = securityTokenValidatedNotification.AuthenticationTicket; - Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() - { - ClientId = Options.ClientId, - ProtocolMessage = openIdConnectMessage, - ValidatedIdToken = jwt, - Nonce = nonce - }); - if (openIdConnectMessage.Code != null) { var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) @@ -534,46 +578,6 @@ protected virtual async Task RedeemAuthorizationCodeAsync( return message; } - /// - /// Save the tokens contained in the in the . - /// - /// The in which tokens are saved. - /// The OpenID Connect response. - private static void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) - { - if (!string.IsNullOrEmpty(message.AccessToken)) - { - properties.Dictionary[OpenIdConnectParameterNames.AccessToken] = message.AccessToken; - } - - if (!string.IsNullOrEmpty(message.IdToken)) - { - properties.Dictionary[OpenIdConnectParameterNames.IdToken] = message.IdToken; - } - - if (!string.IsNullOrEmpty(message.RefreshToken)) - { - properties.Dictionary[OpenIdConnectParameterNames.RefreshToken] = message.RefreshToken; - } - - if (!string.IsNullOrEmpty(message.TokenType)) - { - properties.Dictionary[OpenIdConnectParameterNames.TokenType] = message.TokenType; - } - - if (!string.IsNullOrEmpty(message.ExpiresIn)) - { - int value; - if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) - { - var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(value); - // https://www.w3.org/TR/xmlschema-2/#dateTime - // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx - properties.Dictionary["expires_at"] = expiresAt.ToString("o", CultureInfo.InvariantCulture); - } - } - } - /// /// Sets to . /// @@ -694,7 +698,84 @@ protected virtual string GetNonceKey(string nonce) return OpenIdConnectAuthenticationDefaults.CookiePrefix + OpenIdConnectAuthenticationDefaults.Nonce + Convert.ToBase64String(hash.ComputeHash(Encoding.UTF8.GetBytes(nonce))); } } - + + /// + /// Save the tokens contained in the in the . + /// + /// The in which tokens are saved. + /// The OpenID Connect response. + private static void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) + { + if (!string.IsNullOrEmpty(message.AccessToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.AccessToken] = message.AccessToken; + } + + if (!string.IsNullOrEmpty(message.IdToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.IdToken] = message.IdToken; + } + + if (!string.IsNullOrEmpty(message.RefreshToken)) + { + properties.Dictionary[OpenIdConnectParameterNames.RefreshToken] = message.RefreshToken; + } + + if (!string.IsNullOrEmpty(message.TokenType)) + { + properties.Dictionary[OpenIdConnectParameterNames.TokenType] = message.TokenType; + } + + if (!string.IsNullOrEmpty(message.ExpiresIn)) + { + int value; + if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + { + var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(value); + // https://www.w3.org/TR/xmlschema-2/#dateTime + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx + properties.Dictionary["expires_at"] = expiresAt.ToString("o", CultureInfo.InvariantCulture); + } + } + } + + private ClaimsPrincipal ValidateToken(string idToken, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) + { + + if (!Options.SecurityTokenValidator.CanReadToken(idToken)) + { + _logger.WriteError(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + } + + if (_configuration != null) + { + var issuer = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers == null ? issuer : validationParameters.ValidIssuers.Concat(issuer); + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys + : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys); + } + + SecurityToken validatedToken; + var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out validatedToken); + jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + var tokenType = validatedToken != null ? validatedToken.GetType().ToString() : null; + _logger.WriteError(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, tokenType)); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, tokenType)); + } + + if (validatedToken == null) + { + _logger.WriteError(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); + } + + return principal; + } + private AuthenticationProperties GetPropertiesFromState(string state) { // assume a well formed query string: OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d> diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.Owin.Security.OpenIdConnect/Resources.Designer.cs index efc77447..87e5d342 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/Resources.Designer.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.35317 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace Microsoft.Owin.Security.OpenIdConnect { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -95,5 +95,23 @@ internal static string Exception_ValidatorHandlerMismatch { return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); } } + + /// + /// Looks up a localized string similar to Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'.". + /// + internal static string UnableToValidateToken { + get { + return ResourceManager.GetString("UnableToValidateToken", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'.. + /// + internal static string ValidatedSecurityTokenNotJwt { + get { + return ResourceManager.GetString("ValidatedSecurityTokenNotJwt", resourceCulture); + } + } } } diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Resources.resx b/src/Microsoft.Owin.Security.OpenIdConnect/Resources.resx index 7abad90e..a10fdaaf 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/Resources.resx +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Resources.resx @@ -129,4 +129,10 @@ An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." + + + The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. + \ No newline at end of file From c02461b1cd023cf94df31c984421084c916fc7ed Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 1 Aug 2019 14:23:11 -0600 Subject: [PATCH 4/6] Update OpenID Connect handler to be more in line with .Net Core implementation --- .../OpenidConnectAuthenticationHandler.cs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 07a6b0b1..33feb966 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -330,7 +330,7 @@ protected override async Task AuthenticateCoreAsync() return null; } - user = ValidateToken(openIdConnectMessage.IdToken, validationParameters, out jwt); + user = ValidateToken(openIdConnectMessage.IdToken, properties, validationParameters, out jwt); } if (Options.ProtocolValidator.RequireNonce) @@ -353,7 +353,7 @@ protected override async Task AuthenticateCoreAsync() }); // Authorization Code Flow - if (openIdConnectMessage.Code != null && string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken)) + if (!string.IsNullOrEmpty(openIdConnectMessage.Code) && string.IsNullOrEmpty(openIdConnectMessage.IdToken)) { var tokenEndpointRequest = new OpenIdConnectMessage() { @@ -404,7 +404,7 @@ protected override async Task AuthenticateCoreAsync() // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. // And we'll want to validate the new JWT in ValidateTokenResponse. - user = ValidateToken(openIdConnectMessage.IdToken, validationParameters, out jwt); + user = ValidateToken(openIdConnectMessage.IdToken, properties, validationParameters, out jwt); if (Options.ProtocolValidator.RequireNonce) { @@ -437,22 +437,6 @@ protected override async Task AuthenticateCoreAsync() ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties); - if (Options.UseTokenLifetime) - { - // Override any session persistence to match the token lifetime. - DateTime issued = jwt.ValidFrom; - if (issued != DateTime.MinValue) - { - ticket.Properties.IssuedUtc = issued.ToUniversalTime(); - } - DateTime expires = jwt.ValidTo; - if (expires != DateTime.MinValue) - { - ticket.Properties.ExpiresUtc = expires.ToUniversalTime(); - } - ticket.Properties.AllowRefresh = false; - } - var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) { AuthenticationTicket = ticket, @@ -739,7 +723,8 @@ private static void SaveTokens(AuthenticationProperties properties, OpenIdConnec } } - private ClaimsPrincipal ValidateToken(string idToken, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) + // Note this modifies properties if Options.UseTokenLifetime + private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { if (!Options.SecurityTokenValidator.CanReadToken(idToken)) @@ -773,6 +758,22 @@ private ClaimsPrincipal ValidateToken(string idToken, TokenValidationParameters throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); } + if (Options.UseTokenLifetime) + { + // Override any session persistence to match the token lifetime. + DateTime issued = jwt.ValidFrom; + if (issued != DateTime.MinValue) + { + properties.IssuedUtc = issued.ToUniversalTime(); + } + DateTime expires = jwt.ValidTo; + if (expires != DateTime.MinValue) + { + properties.ExpiresUtc = expires.ToUniversalTime(); + } + properties.AllowRefresh = false; + } + return principal; } From a8c59ec76103207610c586d66fa3d75c1467f3b1 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 6 Aug 2019 18:32:06 -0600 Subject: [PATCH 5/6] Update OpenID Connect handler flow to match AspNetCore flow --- .../AuthorizationCodeReceivedNotification.cs | 52 ++++ .../OpenIdConnectAuthenticationOptions.cs | 8 + .../OpenidConnectAuthenticationHandler.cs | 239 ++++++++++-------- .../Katana.Sandbox.WebServer.csproj | 3 + tests/Katana.Sandbox.WebServer/Startup.cs | 18 +- 5 files changed, 216 insertions(+), 104 deletions(-) diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs index 0ad6131e..3a1bf65f 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs @@ -52,5 +52,57 @@ public AuthorizationCodeReceivedNotification(IOwinContext context, OpenIdConnect /// This is the redirect_uri that was sent in the id_token + code OpenIdConnectRequest. [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "user controlled, not necessarily a URI")] public string RedirectUri { get; set; } + + /// + /// If the developer chooses to redeem the code themselves then they can provide the resulting tokens here. This is the + /// same as calling HandleCodeRedemption. If set then the handler will not attempt to redeem the code. An IdToken + /// is required if one had not been previously received in the authorization response. + /// + public OpenIdConnectMessage TokenEndpointResponse { get; set; } + + /// + /// Indicates if the developer choose to handle (or skip) the code redemption. If true then the handler will not attempt + /// to redeem the code. See HandleCodeRedemption and TokenEndpointResponse. + /// + public bool HandledCodeRedemption + { + get + { + return TokenEndpointResponse != null; + } + } + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption() + { + TokenEndpointResponse = new OpenIdConnectMessage(); + } + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption(string accessToken, string idToken) + { + TokenEndpointResponse = new OpenIdConnectMessage() { AccessToken = accessToken, IdToken = idToken }; + } + + /// + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or + /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then + /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received + /// in the authorization response. Calling this is the same as setting TokenEndpointResponse. + /// + public void HandleCodeRedemption(OpenIdConnectMessage tokenEndpointResponse) + { + TokenEndpointResponse = tokenEndpointResponse; + } } } \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index 95c21e09..1fe8fc43 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -46,6 +46,7 @@ public OpenIdConnectAuthenticationOptions() /// Scope: . /// TokenValidationParameters: new with AuthenticationType = authenticationType. /// UseTokenLifetime: true. + /// RedeemCode: false. /// /// will be used to when creating the for the AuthenticationType property. [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions.set_Caption(System.String)", Justification = "Not a LOC field")] @@ -69,6 +70,7 @@ public OpenIdConnectAuthenticationOptions(string authenticationType) TokenValidationParameters = new TokenValidationParameters(); UseTokenLifetime = true; CookieManager = new CookieManager(); + RedeemCode = false; } /// @@ -314,5 +316,11 @@ public bool UseTokenLifetime /// An abstraction for reading and setting cookies during the authentication process. /// public ICookieManager CookieManager { get; set; } + + /// + /// When set to true the authorization code will be redeemed for tokens at the token endpoint. + /// This property is set to false by default. + /// + public bool RedeemCode { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 33feb966..71e1205d 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -206,16 +206,16 @@ protected override async Task AuthenticateCoreAsync() return null; } - OpenIdConnectMessage openIdConnectMessage = null; + OpenIdConnectMessage authorizationResponse = null; if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase) && Request.Query.Any()) { - openIdConnectMessage = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + 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(openIdConnectMessage.IdToken) || !string.IsNullOrEmpty(openIdConnectMessage.AccessToken)) + if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken)) { var invalidResponseEx = new OpenIdConnectProtocolException("An OpenID Connect response cannot contain an identity token or an access token when using response_mode=query"); @@ -223,7 +223,7 @@ protected override async Task AuthenticateCoreAsync() var authenticationFailedNotification = new AuthenticationFailedNotification(Context, Options) { - ProtocolMessage = openIdConnectMessage, + ProtocolMessage = authorizationResponse, Exception = invalidResponseEx }; await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); @@ -260,10 +260,10 @@ protected override async Task AuthenticateCoreAsync() Request.Body.Seek(0, SeekOrigin.Begin); // TODO: a delegate on OpenIdConnectAuthenticationOptions would allow for users to hook their own custom message. - openIdConnectMessage = new OpenIdConnectMessage(form); + authorizationResponse = new OpenIdConnectMessage(form); } - if (openIdConnectMessage == null) + if (authorizationResponse == null) { return null; } @@ -273,7 +273,7 @@ protected override async Task AuthenticateCoreAsync() { var messageReceivedNotification = new MessageReceivedNotification(Context, Options) { - ProtocolMessage = openIdConnectMessage + ProtocolMessage = authorizationResponse }; await Options.Notifications.MessageReceived(messageReceivedNotification); if (messageReceivedNotification.HandledResponse) @@ -287,7 +287,7 @@ protected override async Task AuthenticateCoreAsync() // runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we // should process. - AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State); + AuthenticationProperties properties = GetPropertiesFromState(authorizationResponse.State); if (properties == null) { _logger.WriteWarning("The state field is missing or invalid."); @@ -295,9 +295,9 @@ protected override async Task AuthenticateCoreAsync() } // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. - if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error)) + if (!string.IsNullOrWhiteSpace(authorizationResponse.Error)) { - throw CreateOpenIdConnectProtocolException(openIdConnectMessage); + throw CreateOpenIdConnectProtocolException(authorizationResponse); } if (_configuration == null) @@ -305,20 +305,22 @@ protected override async Task AuthenticateCoreAsync() _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled); } - PopulateSessionProperties(openIdConnectMessage, properties); - - string nonce = null; + PopulateSessionProperties(authorizationResponse, properties); + ClaimsPrincipal user = null; + AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; + string nonce = null; // Copy and augment to avoid cross request race conditions for updated configurations. var validationParameters = Options.TokenValidationParameters.Clone(); // Hybrid or Implicit flow - if (!string.IsNullOrEmpty(openIdConnectMessage.IdToken)) + if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { + // NOTE: This diverges from AspNetCore to maintain the existing order of events in AspNetKatana. var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) { - ProtocolMessage = openIdConnectMessage, + ProtocolMessage = authorizationResponse, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); if (securityTokenReceivedNotification.HandledResponse) @@ -330,44 +332,68 @@ protected override async Task AuthenticateCoreAsync() return null; } - user = ValidateToken(openIdConnectMessage.IdToken, properties, validationParameters, out jwt); + user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + + ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; + ticket = new AuthenticationTicket(claimsIdentity, properties); + + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + AuthenticationTicket = ticket, + ProtocolMessage = authorizationResponse, + }; + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (securityTokenValidatedNotification.Skipped) + { + return null; + } + // Flow possible changes + ticket = securityTokenValidatedNotification.AuthenticationTicket; } if (Options.ProtocolValidator.RequireNonce) { - if (string.IsNullOrWhiteSpace(openIdConnectMessage.Nonce) && jwt != null) + if (string.IsNullOrWhiteSpace(authorizationResponse.Nonce) && jwt != null) { - openIdConnectMessage.Nonce = jwt.Payload.Nonce; + authorizationResponse.Nonce = jwt.Payload.Nonce; } // deletes the nonce cookie - nonce = RetrieveNonce(openIdConnectMessage); + nonce = RetrieveNonce(authorizationResponse); } Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, - ProtocolMessage = openIdConnectMessage, + ProtocolMessage = authorizationResponse, ValidatedIdToken = jwt, Nonce = nonce }); - // Authorization Code Flow - if (!string.IsNullOrEmpty(openIdConnectMessage.Code) && string.IsNullOrEmpty(openIdConnectMessage.IdToken)) + OpenIdConnectMessage tokenEndpointResponse = null; + + // Authorization Code or Hybrid flow + if (!string.IsNullOrEmpty(authorizationResponse.Code)) { var tokenEndpointRequest = new OpenIdConnectMessage() { ClientId = Options.ClientId, ClientSecret = Options.ClientSecret, - Code = openIdConnectMessage.Code, + Code = authorizationResponse.Code, GrantType = OpenIdConnectGrantTypes.AuthorizationCode, RedirectUri = properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] }; var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) { - Code = openIdConnectMessage.Code, - ProtocolMessage = openIdConnectMessage, + AuthenticationTicket = ticket, + Code = authorizationResponse.Code, + JwtSecurityToken = jwt, + ProtocolMessage = authorizationResponse, RedirectUri = properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, TokenEndpointRequest = tokenEndpointRequest @@ -381,101 +407,108 @@ protected override async Task AuthenticateCoreAsync() { return null; } + // Flow possible changes + authorizationResponse = authorizationCodeReceivedNotification.ProtocolMessage; + ticket = authorizationCodeReceivedNotification.AuthenticationTicket; + tokenEndpointRequest = authorizationCodeReceivedNotification.TokenEndpointRequest; + tokenEndpointResponse = authorizationCodeReceivedNotification.TokenEndpointResponse; + jwt = authorizationCodeReceivedNotification.JwtSecurityToken; - openIdConnectMessage = await RedeemAuthorizationCodeAsync(authorizationCodeReceivedNotification.TokenEndpointRequest); - - var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) - { - ProtocolMessage = openIdConnectMessage, - }; - await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); - if (securityTokenReceivedNotification.HandledResponse) + if (!authorizationCodeReceivedNotification.HandledCodeRedemption && Options.RedeemCode) { - return GetHandledResponseTicket(); + tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest); } - if (securityTokenReceivedNotification.Skipped) + + if (tokenEndpointResponse != null) { - return null; - } + // NOTE: This event has already fired for Hybrid or Implicit flow. Should it fire again here in those flows? + var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = tokenEndpointResponse + }; + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (securityTokenReceivedNotification.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]. - validationParameters.RequireSignedTokens = false; + // 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; - // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. - // And we'll want to validate the new JWT in ValidateTokenResponse. - user = ValidateToken(openIdConnectMessage.IdToken, properties, validationParameters, out jwt); + // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. + // And we'll want to validate the new JWT in ValidateTokenResponse. + JwtSecurityToken tokenEndpointJwt = null; + var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt); - if (Options.ProtocolValidator.RequireNonce) - { - if (string.IsNullOrWhiteSpace(openIdConnectMessage.Nonce) && jwt != null) + // Avoid running the event, etc, if it was already done as part of the authorization response validation. + if (user == null) { - openIdConnectMessage.Nonce = jwt.Payload.Nonce; + ClaimsIdentity claimsIdentity = tokenEndpointUser.Identity as ClaimsIdentity; + ticket = new AuthenticationTicket(claimsIdentity, properties); + + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + AuthenticationTicket = ticket, + ProtocolMessage = tokenEndpointResponse, + }; + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + return GetHandledResponseTicket(); + } + if (securityTokenValidatedNotification.Skipped) + { + return null; + } + // Flow possible changes + ticket = securityTokenValidatedNotification.AuthenticationTicket; } - - // deletes the nonce cookie - if (nonce == null) + else { - nonce = RetrieveNonce(openIdConnectMessage); + if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal)) + { + throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints."); + } } + + jwt = tokenEndpointJwt; } - Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() + // Validate the token response if it wasn't provided manually + if (!authorizationCodeReceivedNotification.HandledCodeRedemption && Options.RedeemCode) { - ClientId = Options.ClientId, - ProtocolMessage = openIdConnectMessage, - ValidatedIdToken = jwt, - Nonce = nonce - }); - } - - if (Options.SaveTokens) - { - SaveTokens(properties, openIdConnectMessage); - } - - ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; - AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties); + if (Options.ProtocolValidator.RequireNonce) + { + if (string.IsNullOrWhiteSpace(tokenEndpointResponse.Nonce) && jwt != null) + { + tokenEndpointResponse.Nonce = jwt.Payload.Nonce; + } + + // deletes the nonce cookie + if (nonce == null) + { + nonce = RetrieveNonce(tokenEndpointResponse); + } + } - var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) - { - AuthenticationTicket = ticket, - ProtocolMessage = openIdConnectMessage, - }; - await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); - if (securityTokenValidatedNotification.HandledResponse) - { - return GetHandledResponseTicket(); - } - if (securityTokenValidatedNotification.Skipped) - { - return null; + Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() + { + ClientId = Options.ClientId, + ProtocolMessage = tokenEndpointResponse, + ValidatedIdToken = jwt, + Nonce = nonce + }); + } } - // Flow possible changes - ticket = securityTokenValidatedNotification.AuthenticationTicket; - if (openIdConnectMessage.Code != null) + if (Options.SaveTokens && ticket != null) { - var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) - { - AuthenticationTicket = ticket, - Code = openIdConnectMessage.Code, - JwtSecurityToken = jwt, - ProtocolMessage = openIdConnectMessage, - RedirectUri = ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? - ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, - }; - await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); - if (authorizationCodeReceivedNotification.HandledResponse) - { - return GetHandledResponseTicket(); - } - if (authorizationCodeReceivedNotification.Skipped) - { - return null; - } - // Flow possible changes - ticket = authorizationCodeReceivedNotification.AuthenticationTicket; + SaveTokens(ticket.Properties, tokenEndpointResponse ?? authorizationResponse); } return ticket; @@ -498,7 +531,7 @@ protected override async Task AuthenticateCoreAsync() var authenticationFailedNotification = new AuthenticationFailedNotification(Context, Options) { - ProtocolMessage = openIdConnectMessage, + ProtocolMessage = authorizationResponse, Exception = authFailedEx.SourceException }; await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); diff --git a/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj b/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj index 01ef6161..62edb256 100644 --- a/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj +++ b/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj @@ -23,6 +23,7 @@ 12.0 + true @@ -51,6 +52,8 @@ ..\..\packages\Microsoft.IdentityModel.Logging.5.3.0\lib\net45\Microsoft.IdentityModel.Logging.dll True + + ..\..\packages\Microsoft.IdentityModel.Tokens.5.3.0\lib\net45\Microsoft.IdentityModel.Tokens.dll True diff --git a/tests/Katana.Sandbox.WebServer/Startup.cs b/tests/Katana.Sandbox.WebServer/Startup.cs index cdeb417b..bc1e3f9b 100644 --- a/tests/Katana.Sandbox.WebServer/Startup.cs +++ b/tests/Katana.Sandbox.WebServer/Startup.cs @@ -135,7 +135,23 @@ public void Configuration(IAppBuilder app) CookieManager = new SystemWebCookieManager(), //ResponseType = "code", //ResponseMode = "query", - //SaveTokens = true + //SaveTokens = true, + //Scope = "openid profile offline_access", + //RedeemCode = true, + //Notifications = new Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationNotifications + //{ + // AuthorizationCodeReceived = async n => + // { + // var _configuration = await n.Options.ConfigurationManager.GetConfigurationAsync(n.OwinContext.Request.CallCancelled); + // var requestMessage = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, _configuration.TokenEndpoint); + // requestMessage.Content = new System.Net.Http.FormUrlEncodedContent(n.TokenEndpointRequest.Parameters); + // var responseMessage = await n.Options.Backchannel.SendAsync(requestMessage); + // var responseContent = await responseMessage.Content.ReadAsStringAsync(); + // Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage message = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage(responseContent); + + // n.HandleCodeRedemption(message); + // } + //} }); /* From b6002bebf954eb7df2a9cfc4b00019a8d0441083 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 12 Aug 2019 15:13:07 -0600 Subject: [PATCH 6/6] Add TokenResponseReceivedNotification to OpenID Connect, Fix nonce retrieval placement --- ...crosoft.Owin.Security.OpenIdConnect.csproj | 1 + .../TokenResponseReceivedNotification.cs | 32 ++++++++++ ...penIdConnectAuthenticationNotifications.cs | 6 ++ .../OpenidConnectAuthenticationHandler.cs | 63 +++++++++---------- tests/Katana.Sandbox.WebServer/Startup.cs | 2 + 5 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.Owin.Security.OpenIdConnect/Notifications/TokenResponseReceivedNotification.cs diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Microsoft.Owin.Security.OpenIdConnect.csproj b/src/Microsoft.Owin.Security.OpenIdConnect/Microsoft.Owin.Security.OpenIdConnect.csproj index d19b2403..24e5f2d2 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/Microsoft.Owin.Security.OpenIdConnect.csproj +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Microsoft.Owin.Security.OpenIdConnect.csproj @@ -85,6 +85,7 @@ + diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/TokenResponseReceivedNotification.cs b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/TokenResponseReceivedNotification.cs new file mode 100644 index 00000000..1dd3b716 --- /dev/null +++ b/src/Microsoft.Owin.Security.OpenIdConnect/Notifications/TokenResponseReceivedNotification.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Owin.Security.OpenIdConnect; + +namespace Microsoft.Owin.Security.Notifications +{ + /// + /// This Notification can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. + /// + public class TokenResponseReceivedNotification : BaseNotification + { + /// + /// Creates a + /// + public TokenResponseReceivedNotification(IOwinContext context, OpenIdConnectAuthenticationOptions options) + : base(context, options) + { + } + + /// + /// Gets or sets the that contains the code redeemed for tokens at the token endpoint. + /// + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// Gets or sets the that contains the tokens received after redeeming the code at the token endpoint. + /// + public OpenIdConnectMessage TokenEndpointResponse { get; set; } + } +} diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs index f51533c6..84986069 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs @@ -24,6 +24,7 @@ public OpenIdConnectAuthenticationNotifications() SecurityTokenReceived = notification => Task.FromResult(0); SecurityTokenValidated = notification => Task.FromResult(0); RedirectToIdentityProvider = notification => Task.FromResult(0); + TokenResponseReceived = notification => Task.FromResult(0); } /// @@ -55,5 +56,10 @@ public OpenIdConnectAuthenticationNotifications() /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// public Func, Task> SecurityTokenValidated { get; set; } + + /// + /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. + /// + public Func TokenResponseReceived { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 71e1205d..bee807bd 100644 --- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -317,7 +317,6 @@ protected override async Task AuthenticateCoreAsync() // Hybrid or Implicit flow if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { - // NOTE: This diverges from AspNetCore to maintain the existing order of events in AspNetKatana. var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) { ProtocolMessage = authorizationResponse, @@ -334,6 +333,17 @@ protected override async Task AuthenticateCoreAsync() user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + if (Options.ProtocolValidator.RequireNonce) + { + if (string.IsNullOrWhiteSpace(authorizationResponse.Nonce)) + { + authorizationResponse.Nonce = jwt.Payload.Nonce; + } + + // deletes the nonce cookie + nonce = RetrieveNonce(authorizationResponse); + } + ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; ticket = new AuthenticationTicket(claimsIdentity, properties); @@ -355,17 +365,6 @@ protected override async Task AuthenticateCoreAsync() ticket = securityTokenValidatedNotification.AuthenticationTicket; } - if (Options.ProtocolValidator.RequireNonce) - { - if (string.IsNullOrWhiteSpace(authorizationResponse.Nonce) && jwt != null) - { - authorizationResponse.Nonce = jwt.Payload.Nonce; - } - - // deletes the nonce cookie - nonce = RetrieveNonce(authorizationResponse); - } - Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, @@ -421,17 +420,17 @@ protected override async Task AuthenticateCoreAsync() if (tokenEndpointResponse != null) { - // NOTE: This event has already fired for Hybrid or Implicit flow. Should it fire again here in those flows? - var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) + var tokenResponseReceivedNotification = new TokenResponseReceivedNotification(Context, Options) { - ProtocolMessage = tokenEndpointResponse + ProtocolMessage = authorizationResponse, + TokenEndpointResponse = tokenEndpointResponse }; - await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); - if (securityTokenReceivedNotification.HandledResponse) + await Options.Notifications.TokenResponseReceived(tokenResponseReceivedNotification); + if (tokenResponseReceivedNotification.HandledResponse) { return GetHandledResponseTicket(); } - if (securityTokenReceivedNotification.Skipped) + if (tokenResponseReceivedNotification.Skipped) { return null; } @@ -448,6 +447,20 @@ protected override async Task AuthenticateCoreAsync() // Avoid running the event, etc, if it was already done as part of the authorization response validation. if (user == null) { + if (Options.ProtocolValidator.RequireNonce) + { + if (string.IsNullOrWhiteSpace(tokenEndpointResponse.Nonce)) + { + tokenEndpointResponse.Nonce = tokenEndpointJwt.Payload.Nonce; + } + + // deletes the nonce cookie + if (nonce == null) + { + nonce = RetrieveNonce(tokenEndpointResponse); + } + } + ClaimsIdentity claimsIdentity = tokenEndpointUser.Identity as ClaimsIdentity; ticket = new AuthenticationTicket(claimsIdentity, properties); @@ -482,20 +495,6 @@ protected override async Task AuthenticateCoreAsync() // Validate the token response if it wasn't provided manually if (!authorizationCodeReceivedNotification.HandledCodeRedemption && Options.RedeemCode) { - if (Options.ProtocolValidator.RequireNonce) - { - if (string.IsNullOrWhiteSpace(tokenEndpointResponse.Nonce) && jwt != null) - { - tokenEndpointResponse.Nonce = jwt.Payload.Nonce; - } - - // deletes the nonce cookie - if (nonce == null) - { - nonce = RetrieveNonce(tokenEndpointResponse); - } - } - Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, diff --git a/tests/Katana.Sandbox.WebServer/Startup.cs b/tests/Katana.Sandbox.WebServer/Startup.cs index bc1e3f9b..bd771020 100644 --- a/tests/Katana.Sandbox.WebServer/Startup.cs +++ b/tests/Katana.Sandbox.WebServer/Startup.cs @@ -131,6 +131,7 @@ public void Configuration(IAppBuilder app) { Authority = Environment.GetEnvironmentVariable("oidc:authority"), ClientId = Environment.GetEnvironmentVariable("oidc:clientid"), + ClientSecret = Environment.GetEnvironmentVariable("oidc:clientsecret"), RedirectUri = "https://localhost:44318/", CookieManager = new SystemWebCookieManager(), //ResponseType = "code", @@ -146,6 +147,7 @@ public void Configuration(IAppBuilder app) // var requestMessage = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, _configuration.TokenEndpoint); // requestMessage.Content = new System.Net.Http.FormUrlEncodedContent(n.TokenEndpointRequest.Parameters); // var responseMessage = await n.Options.Backchannel.SendAsync(requestMessage); + // responseMessage.EnsureSuccessStatusCode(); // var responseContent = await responseMessage.Content.ReadAsStringAsync(); // Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage message = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage(responseContent);