From 8d7f052cf4ca19b1a1a545469846f41ba009494d Mon Sep 17 00:00:00 2001 From: tushar gupta Date: Wed, 8 Jul 2015 17:05:57 -0700 Subject: [PATCH] Adding support for signing in using "code flow" --- .../AuthorizationCodeRedeemedNotification.cs | 36 + .../OpenIdConnectAuthenticationHandler.cs | 697 ++++++++++++------ .../OpenIdConnectAuthenticationMiddleware.cs | 14 +- ...penIdConnectAuthenticationNotifications.cs | 6 + .../OpenIdConnectAuthenticationOptions.cs | 6 + .../OpenIdConnectTokenEndpointResponse.cs | 37 + .../Resources.Designer.cs | 72 ++ .../Resources.resx | 29 +- .../OpenIdConnect/LoggingUtilities.cs | 6 + ...enticationHandlerForTestingAuthenticate.cs | 23 +- .../OpenIdConnectHandlerTests.cs | 116 ++- .../OpenIdConnectMiddlewareTests.cs | 14 +- 12 files changed, 830 insertions(+), 226 deletions(-) create mode 100644 src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeRedeemedNotification.cs create mode 100644 src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectTokenEndpointResponse.cs diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeRedeemedNotification.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeRedeemedNotification.cs new file mode 100644 index 0000000000..5d8a106867 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeRedeemedNotification.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Http; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + /// + /// This Notification can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. + /// + public class AuthorizationCodeRedeemedNotification : BaseNotification + { + /// + /// Creates a + /// + public AuthorizationCodeRedeemedNotification(HttpContext context, OpenIdConnectAuthenticationOptions options) : base(context, options) + { + } + + /// + /// Gets or sets the 'code'. + /// + public string Code { get; set; } + + /// + /// Gets or sets the that contains the tokens and json response received after redeeming the code at the token endpoint. + /// + public OpenIdConnectTokenEndpointResponse TokenEndpointResponse { get; set; } + + /// + /// Gets or sets the . + /// + public OpenIdConnectMessage ProtocolMessage { get; set; } + + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index 6981f48e56..b3203bb8ab 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.IdentityModel.Tokens; using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.Notifications; @@ -16,6 +18,7 @@ using Microsoft.Framework.Caching.Distributed; using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Microsoft.IdentityModel.Protocols; +using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OpenIdConnect { @@ -28,6 +31,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect private const string UriSchemeDelimiter = "://"; private OpenIdConnectConfiguration _configuration; + protected HttpClient Backchannel { get; private set; } + + public OpenIdConnectAuthenticationHandler(HttpClient backchannel) + { + Backchannel = backchannel; + } + /// /// Handles Signout /// @@ -116,17 +126,6 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect properties.RedirectUri = CurrentUri; } - if (!string.IsNullOrEmpty(Options.RedirectUri)) - { - Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); - } - - // When redeeming a 'code' for an AccessToken, this value is needed - if (!string.IsNullOrEmpty(Options.RedirectUri)) - { - properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, Options.RedirectUri); - } - if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); @@ -195,6 +194,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect message = redirectToIdentityProviderNotification.ProtocolMessage; } + var redirectUriForCode = message.RedirectUri; + if (string.IsNullOrEmpty(redirectUriForCode)) + { + Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); + redirectUriForCode = Options.RedirectUri; + } + + if (!string.IsNullOrEmpty(redirectUriForCode)) + { + // When redeeming a 'code' for an AccessToken, this value is needed + properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, redirectUriForCode); + } + message.State = Options.StateDataFormat.Protect(properties); var redirectUri = message.CreateAuthenticationRequestUrl(); @@ -243,27 +255,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect try { - if (Logger.IsEnabled(LogLevel.Debug)) - { - Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl()); - } - - var messageReceivedNotification = - new MessageReceivedNotification(Context, Options) - { - ProtocolMessage = message - }; - - await Options.Notifications.MessageReceived(messageReceivedNotification); + var messageReceivedNotification = await RunMessageReceivedNotificationAsync(message); if (messageReceivedNotification.HandledResponse) { - Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); return messageReceivedNotification.AuthenticationTicket; } - - if (messageReceivedNotification.Skipped) + else if (messageReceivedNotification.Skipped) { - Logger.LogVerbose(Resources.OIDCH_0003_MessageReceivedNotificationSkipped); return null; } @@ -302,178 +300,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } - AuthenticationTicket ticket = null; - JwtSecurityToken jwt = null; - - // OpenIdConnect protocol allows a Code to be received without the id_token - if (!string.IsNullOrEmpty(message.IdToken)) + if (string.IsNullOrEmpty(message.IdToken) && !string.IsNullOrEmpty(message.Code)) { - Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); - var securityTokenReceivedNotification = - new SecurityTokenReceivedNotification(Context, Options) - { - ProtocolMessage = message, - }; - - await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); - if (securityTokenReceivedNotification.HandledResponse) - { - Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); - return securityTokenReceivedNotification.AuthenticationTicket; - } - - if (securityTokenReceivedNotification.Skipped) - { - Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); - return null; - } - - // Copy and augment to avoid cross request race conditions for updated configurations. - var validationParameters = Options.TokenValidationParameters.Clone(); - if (_configuration != null) - { - if (string.IsNullOrEmpty(validationParameters.ValidIssuer)) - { - validationParameters.ValidIssuer = _configuration.Issuer; - } - else if (!string.IsNullOrEmpty(_configuration.Issuer)) - { - validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; - } - - validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; - } - - SecurityToken validatedToken = null; - ClaimsPrincipal principal = null; - foreach (var validator in Options.SecurityTokenValidators) - { - if (validator.CanReadToken(message.IdToken)) - { - principal = validator.ValidateToken(message.IdToken, validationParameters, out validatedToken); - jwt = validatedToken as JwtSecurityToken; - if (jwt == null) - { - Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()); - throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); - } - } - } - - if (validatedToken == null) - { - Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, message.IdToken); - throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken)); - } - - ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); - if (!string.IsNullOrEmpty(message.SessionState)) - { - ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; - } - - if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe)) - { - ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; - } - - // Rename? - if (Options.UseTokenLifetime) - { - var issued = validatedToken.ValidFrom; - if (issued != DateTime.MinValue) - { - ticket.Properties.IssuedUtc = issued; - } - - var expires = validatedToken.ValidTo; - if (expires != DateTime.MinValue) - { - ticket.Properties.ExpiresUtc = expires; - } - } - - var securityTokenValidatedNotification = - new SecurityTokenValidatedNotification(Context, Options) - { - AuthenticationTicket = ticket, - ProtocolMessage = message - }; - - await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); - if (securityTokenValidatedNotification.HandledResponse) - { - Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); - return securityTokenValidatedNotification.AuthenticationTicket; - } - - if (securityTokenValidatedNotification.Skipped) - { - Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); - return null; - } - - string nonce = jwt.Payload.Nonce; - if (Options.CacheNonces) - { - if (await Options.NonceCache.GetAsync(nonce) != null) - { - await Options.NonceCache.RemoveAsync(nonce); - } - else - { - // If the nonce cannot be removed, it was - // already used and MUST be rejected. - nonce = null; - } - } - else - { - nonce = ReadNonceCookie(nonce); - } - - var protocolValidationContext = new OpenIdConnectProtocolValidationContext - { - AuthorizationCode = message.Code, - Nonce = nonce, - }; - - Options.ProtocolValidator.Validate(jwt, protocolValidationContext); + return await HandleCodeOnlyFlow(message, properties); } - - if (message.Code != null) + else if (!string.IsNullOrEmpty(message.IdToken)) { - Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code); - if (ticket == null) - { - ticket = new AuthenticationTicket(properties, Options.AuthenticationScheme); - } - - var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) - { - AuthenticationTicket = ticket, - Code = message.Code, - JwtSecurityToken = jwt, - ProtocolMessage = message, - RedirectUri = ticket.Properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ? - ticket.Properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : string.Empty, - }; - - await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); - if (authorizationCodeReceivedNotification.HandledResponse) - { - Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse); - return authorizationCodeReceivedNotification.AuthenticationTicket; - } - - if (authorizationCodeReceivedNotification.Skipped) - { - Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped); - return null; - } + return await HandleIdTokenFlows(message, properties); + } + else + { + Logger.LogDebug(Resources.OIDCH_0045_Id_Token_Code_Missing); + return null; } - - return ticket; } catch (Exception exception) { @@ -489,23 +328,13 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } - var authenticationFailedNotification = - new AuthenticationFailedNotification(Context, Options) - { - ProtocolMessage = message, - Exception = exception - }; - - await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + var authenticationFailedNotification = await RunAuthenticationFailedNotificationAsync(message, exception); if (authenticationFailedNotification.HandledResponse) { - Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); return authenticationFailedNotification.AuthenticationTicket; } - - if (authenticationFailedNotification.Skipped) + else if (authenticationFailedNotification.Skipped) { - Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); return null; } @@ -513,6 +342,215 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } + private async Task HandleCodeOnlyFlow(OpenIdConnectMessage message, AuthenticationProperties properties) + { + AuthenticationTicket ticket = null; + JwtSecurityToken jwt = null; + + OpenIdConnectTokenEndpointResponse tokenEndpointResponse = null; + string idToken = null; + var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt); + if (authorizationCodeReceivedNotification.HandledResponse) + { + return authorizationCodeReceivedNotification.AuthenticationTicket; + } + else if (authorizationCodeReceivedNotification.Skipped) + { + return null; + } + + // Redeeming authorization code for tokens + Logger.LogDebug(Resources.OIDCH_0038_Redeeming_Auth_Code, message.Code); + + tokenEndpointResponse = await RedeemAuthorizationCodeAsync(message.Code, authorizationCodeReceivedNotification.RedirectUri); + idToken = tokenEndpointResponse.Message.IdToken; + + var authorizationCodeRedeemedNotification = await RunAuthorizationCodeRedeemedNotificationAsync(message, tokenEndpointResponse); + if (authorizationCodeRedeemedNotification.HandledResponse) + { + return authorizationCodeRedeemedNotification.AuthenticationTicket; + } + else if (authorizationCodeRedeemedNotification.Skipped) + { + return null; + } + + // no need to validate signature when token is received using "code flow" as per spec [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. + var validationParameters = Options.TokenValidationParameters.Clone(); + validationParameters.ValidateSignature = false; + + ticket = ValidateToken(idToken, message, properties, validationParameters, out jwt); + + if (Options.GetClaimsFromUserInfoEndpoint) + { + Logger.LogDebug(Resources.OIDCH_0040_Sending_Request_UIEndpoint); + ticket = await GetUserInformationAsync(properties, tokenEndpointResponse.Message, ticket); + } + + var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket); + if (securityTokenValidatedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + else if (securityTokenValidatedNotification.Skipped) + { + return null; + } + + // If id_token is received using code only flow, no need to validate chash. + await ValidateOpenIdConnectProtocolAsync(jwt, message, false); + + return ticket; + } + + private async Task HandleIdTokenFlows(OpenIdConnectMessage message, AuthenticationProperties properties) + { + AuthenticationTicket ticket = null; + JwtSecurityToken jwt = null; + + var securityTokenReceivedNotification = await RunSecurityTokenReceivedNotificationAsync(message); + if (securityTokenReceivedNotification.HandledResponse) + { + return securityTokenReceivedNotification.AuthenticationTicket; + } + else if (securityTokenReceivedNotification.Skipped) + { + return null; + } + + var validationParameters = Options.TokenValidationParameters.Clone(); + ticket = ValidateToken(message.IdToken, message, properties, validationParameters, out jwt); + + var securityTokenValidatedNotification = await RunSecurityTokenValidatedNotificationAsync(message, ticket); + if (securityTokenValidatedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + else if (securityTokenValidatedNotification.Skipped) + { + return null; + } + + await ValidateOpenIdConnectProtocolAsync(jwt, message); + + if (message.Code != null) + { + var authorizationCodeReceivedNotification = await RunAuthorizationCodeReceivedNotificationAsync(message, properties, ticket, jwt); + if (authorizationCodeReceivedNotification.HandledResponse) + { + return authorizationCodeReceivedNotification.AuthenticationTicket; + } + else if (authorizationCodeReceivedNotification.Skipped) + { + return null; + } + } + + return ticket; + } + + /// + /// Redeems the authorization code for tokens at the token endpoint + /// + /// The authorization code to redeem. + /// Uri that was passed in the request sent for the authorization code. + /// OpenIdConnect message that has tokens inside it. + protected virtual async Task RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri) + { + var openIdMessage = new OpenIdConnectMessage() + { + ClientId = Options.ClientId, + ClientSecret = Options.ClientSecret, + Code = authorizationCode, + GrantType = "authorization_code", + RedirectUri = redirectUri + }; + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); + requestMessage.Content = new FormUrlEncodedContent(openIdMessage.Parameters); + var responseMessage = await Backchannel.SendAsync(requestMessage); + responseMessage.EnsureSuccessStatusCode(); + var tokenResonse = await responseMessage.Content.ReadAsStringAsync(); + var jsonTokenResponse = JObject.Parse(tokenResonse); + return new OpenIdConnectTokenEndpointResponse(jsonTokenResponse); + } + + /// + /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity. + /// + /// Authentication Properties + /// message that is being processed + /// authentication ticket with claims principal and identities + /// Authentication ticket with identity with additional claims, if any. + protected virtual async Task GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket) + { + string userInfoEndpoint = null; + if (_configuration != null) + { + userInfoEndpoint = _configuration.UserInfoEndpoint; + } + + if (string.IsNullOrEmpty(userInfoEndpoint)) + { + Logger.LogWarning(Resources.OIDCH_0046_UserInfo_Endpoint_Not_Set); + return ticket; + } + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken); + var responseMessage = await Backchannel.SendAsync(requestMessage); + responseMessage.EnsureSuccessStatusCode(); + var userInfoResponse = await responseMessage.Content.ReadAsStringAsync(); + var user = JObject.Parse(userInfoResponse); + + var identity = (ClaimsIdentity)ticket.Principal.Identity; + var subjectClaimType = identity.FindFirst(ClaimTypes.NameIdentifier); + if (subjectClaimType == null) + { + Logger.LogError(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0041_Subject_Claim_Not_Found, identity.ToString())); + return ticket; + } + + var userInfoSubjectClaimValue = user.Value(JwtRegisteredClaimNames.Sub); + + // check if the sub claim matches + if (userInfoSubjectClaimValue == null || !string.Equals(userInfoSubjectClaimValue, subjectClaimType.Value, StringComparison.Ordinal)) + { + Logger.LogError(Resources.OIDCH_0039_Subject_Claim_Mismatch); + return ticket; + } + + foreach (var claim in identity.Claims) + { + // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set + var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ? + claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty; + + // checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint + JToken value; + var isClaimIncluded = user.TryGetValue(claim.Type, out value) || user.TryGetValue(shortClaimTypeName, out value); + + // if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response + if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal)) + { + if (!user.Remove(claim.Type)) + { + user.Remove(shortClaimTypeName); + } + } + } + + // adding remaining unique claims from userinfo endpoint to the identity + foreach (var pair in user) + { + JToken value; + var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null; + identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + return new AuthenticationTicket(new ClaimsPrincipal(identity), ticket.Properties, ticket.AuthenticationScheme); + } + /// /// Adds the nonce to . /// @@ -609,6 +647,251 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } + private async Task> RunMessageReceivedNotificationAsync(OpenIdConnectMessage message) + { + Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl()); + var messageReceivedNotification = + new MessageReceivedNotification(Context, Options) + { + ProtocolMessage = message + }; + + await Options.Notifications.MessageReceived(messageReceivedNotification); + if (messageReceivedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); + } + else if (messageReceivedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0003_MessageReceivedNotificationSkipped); + } + + return messageReceivedNotification; + } + + private async Task RunAuthorizationCodeReceivedNotificationAsync(OpenIdConnectMessage message, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt) + { + var redirectUri = properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey) ? + properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey] : Options.RedirectUri; + + Logger.LogDebug(Resources.OIDCH_0014_AuthorizationCodeReceived, message.Code); + + var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) + { + Code = message.Code, + ProtocolMessage = message, + RedirectUri = redirectUri, + AuthenticationTicket = ticket, + JwtSecurityToken = jwt + }; + + await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); + if (authorizationCodeReceivedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0015_AuthorizationCodeReceivedNotificationHandledResponse); + } + else if (authorizationCodeReceivedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0016_AuthorizationCodeReceivedNotificationSkipped); + } + + return authorizationCodeReceivedNotification; + } + + private async Task RunAuthorizationCodeRedeemedNotificationAsync(OpenIdConnectMessage message, OpenIdConnectTokenEndpointResponse tokenEndpointResponse) + { + Logger.LogDebug(Resources.OIDCH_0042_AuthorizationCodeRedeemed, message.Code); + var authorizationCodeRedeemedNotification = new AuthorizationCodeRedeemedNotification(Context, Options) + { + Code = message.Code, + ProtocolMessage = message, + TokenEndpointResponse = tokenEndpointResponse + }; + + await Options.Notifications.AuthorizationCodeRedeemed(authorizationCodeRedeemedNotification); + if (authorizationCodeRedeemedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse); + } + else if (authorizationCodeRedeemedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped); + } + return authorizationCodeRedeemedNotification; + } + + private async Task> RunSecurityTokenReceivedNotificationAsync(OpenIdConnectMessage message) + { + Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); + var securityTokenReceivedNotification = + new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = message, + }; + + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); + } + else if (securityTokenReceivedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); + } + + return securityTokenReceivedNotification; + } + + private async Task> RunSecurityTokenValidatedNotificationAsync(OpenIdConnectMessage message, AuthenticationTicket ticket) + { + var securityTokenValidatedNotification = + new SecurityTokenValidatedNotification(Context, Options) + { + AuthenticationTicket = ticket, + ProtocolMessage = message + }; + + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); + } + else if (securityTokenValidatedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); + } + + return securityTokenValidatedNotification; + } + + private async Task> RunAuthenticationFailedNotificationAsync(OpenIdConnectMessage message, Exception exception) + { + var authenticationFailedNotification = + new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = message, + Exception = exception + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + Logger.LogVerbose(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); + } + else if (authenticationFailedNotification.Skipped) + { + Logger.LogVerbose(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); + } + + return authenticationFailedNotification; + } + + private AuthenticationTicket ValidateToken(string idToken, OpenIdConnectMessage message, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) + { + AuthenticationTicket ticket = null; + jwt = null; + + if (_configuration != null) + { + if (string.IsNullOrEmpty(validationParameters.ValidIssuer)) + { + validationParameters.ValidIssuer = _configuration.Issuer; + } + else if (!string.IsNullOrEmpty(_configuration.Issuer)) + { + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; + } + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; + } + + SecurityToken validatedToken = null; + ClaimsPrincipal principal = null; + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(idToken)) + { + principal = validator.ValidateToken(idToken, validationParameters, out validatedToken); + jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); + } + } + } + + if (validatedToken == null) + { + Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, idToken); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, idToken)); + } + + ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); + if (!string.IsNullOrEmpty(message.SessionState)) + { + ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; + } + + if (_configuration != null && !string.IsNullOrEmpty(_configuration.CheckSessionIframe)) + { + ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; + } + + // Rename? + if (Options.UseTokenLifetime) + { + var issued = validatedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + ticket.Properties.IssuedUtc = issued; + } + + var expires = validatedToken.ValidTo; + if (expires != DateTime.MinValue) + { + ticket.Properties.ExpiresUtc = expires; + } + } + + return ticket; + } + + private async Task ValidateOpenIdConnectProtocolAsync(JwtSecurityToken jwt, OpenIdConnectMessage message, bool ValidateCHash = true) + { + string nonce = jwt.Payload.Nonce; + if (Options.CacheNonces) + { + if (await Options.NonceCache.GetAsync(nonce) != null) + { + await Options.NonceCache.RemoveAsync(nonce); + } + else + { + // If the nonce cannot be removed, it was + // already used and MUST be rejected. + nonce = null; + } + } + else + { + nonce = ReadNonceCookie(nonce); + } + + var protocolValidationContext = new OpenIdConnectProtocolValidationContext + { + Nonce = nonce + }; + + // If authorization code is null, protocol validator does not validate the chash + if (ValidateCHash) + { + protocolValidationContext.AuthorizationCode = message.Code; + } + + Options.ProtocolValidator.Validate(jwt, protocolValidationContext); + } + /// /// Calls InvokeReplyPathAsync /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index 532ca9518e..29f1144aaf 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -103,6 +103,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect Options.TokenValidationParameters.ValidAudience = Options.ClientId; } + Backchannel = new HttpClient(ResolveHttpMessageHandler(Options)); + Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET OpenIdConnect middleware"); + Backchannel.Timeout = Options.BackchannelTimeout; + Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + if (Options.ConfigurationManager == null) { if (Options.Configuration != null) @@ -122,10 +127,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect Options.MetadataAddress += ".well-known/openid-configuration"; } - var httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); - httpClient.Timeout = Options.BackchannelTimeout; - httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, httpClient); + Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, Backchannel); } } @@ -137,13 +139,15 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } } + protected HttpClient Backchannel { get; private set; } + /// /// Provides the object for processing authentication-related requests. /// /// An configured with the supplied to the constructor. protected override AuthenticationHandler CreateHandler() { - return new OpenIdConnectAuthenticationHandler(); + return new OpenIdConnectAuthenticationHandler(Backchannel); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs index 11c8c9ccc2..81eeb08d32 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { AuthenticationFailed = notification => Task.FromResult(0); AuthorizationCodeReceived = notification => Task.FromResult(0); + AuthorizationCodeRedeemed = notificaion => Task.FromResult(0); MessageReceived = notification => Task.FromResult(0); SecurityTokenReceived = notification => Task.FromResult(0); SecurityTokenValidated = notification => Task.FromResult(0); @@ -36,6 +37,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public Func AuthorizationCodeReceived { get; set; } + /// + /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. + /// + public Func AuthorizationCodeRedeemed { get; set; } + /// /// Invoked when a protocol message is first received. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs index 05b33548c6..df8f0e9d58 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -56,6 +56,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect AuthenticationScheme = authenticationScheme; BackchannelTimeout = TimeSpan.FromMinutes(1); Caption = OpenIdConnectAuthenticationDefaults.Caption; + GetClaimsFromUserInfoEndpoint = false; ProtocolValidator = new OpenIdConnectProtocolValidator(); RefreshOnIssuerKeyNotFound = true; ResponseMode = OpenIdConnectResponseModes.FormPost; @@ -164,6 +165,11 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// public bool DefaultToCurrentUriOnRedirect { get; set; } + /// + /// Boolean to set whether the middleware should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint. + /// + public bool GetClaimsFromUserInfoEndpoint { get; set; } + /// /// Gets or sets the discovery endpoint for obtaining metadata /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectTokenEndpointResponse.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectTokenEndpointResponse.cs new file mode 100644 index 0000000000..a0cc6ccb85 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectTokenEndpointResponse.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// Class to store the response returned from token endpoint + /// + public class OpenIdConnectTokenEndpointResponse + { + public OpenIdConnectTokenEndpointResponse(JObject jsonResponse) + { + JsonResponse = jsonResponse; + Message = new OpenIdConnectMessage() + { + AccessToken = JsonResponse.Value(OpenIdConnectParameterNames.AccessToken), + IdToken = JsonResponse.Value(OpenIdConnectParameterNames.IdToken), + TokenType = JsonResponse.Value(OpenIdConnectParameterNames.TokenType), + ExpiresIn = JsonResponse.Value(OpenIdConnectParameterNames.ExpiresIn) + }; + } + + /// + /// OpenIdConnect message that contains the id token and access tokens + /// + public OpenIdConnectMessage Message { get; set; } + + /// + /// Json response returned from the token endpoint + /// + public JObject JsonResponse { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs index 67c06b96d8..cdef48e368 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs @@ -180,6 +180,78 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect get { return ResourceManager.GetString("OIDCH_0037_RedirectUri"); } } + /// + /// OIDCH_0038: Id Token is null. Redeeming code : {0} for tokens. + /// + internal static string OIDCH_0038_Redeeming_Auth_Code + { + get { return ResourceManager.GetString("OIDCH_0038_Redeeming_Auth_Code"); } + } + + /// + /// OIDCH_0039: Subject claim received from userinfo endpoint does not match the one in the id token. + /// + internal static string OIDCH_0039_Subject_Claim_Mismatch + { + get { return ResourceManager.GetString("OIDCH_0039_Subject_Claim_Mismatch"); } + } + + /// + /// OIDCH_0040: Sending request to user info endpoint for retrieving claims. + /// + internal static string OIDCH_0040_Sending_Request_UIEndpoint + { + get { return ResourceManager.GetString("OIDCH_0040_Sending_Request_UIEndpoint"); } + } + + /// + /// OIDCH_0041: Subject claim not found in {0}. + /// + internal static string OIDCH_0041_Subject_Claim_Not_Found + { + get { return ResourceManager.GetString("OIDCH_0041_Subject_Claim_Not_Found"); } + } + + /// + /// OIDCH_0042: AuthorizationCode redeemed: '{0}' + /// + internal static string OIDCH_0042_AuthorizationCodeRedeemed + { + get { return ResourceManager.GetString("OIDCH_0042_AuthorizationCodeRedeemed"); } + } + + /// + /// OIDCH_0043: AuthorizationCodeRedeemedNotification.HandledResponse + /// + internal static string OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0043_AuthorizationCodeRedeemedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0044: AuthorizationCodeRedeemedNotification.Skipped + /// + internal static string OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0044_AuthorizationCodeRedeemedNotificationSkipped"); } + } + + /// + /// OIDCH_0045: Cannot process the message.Both id_token and code are missing. + /// + internal static string OIDCH_0045_Id_Token_Code_Missing + { + get { return ResourceManager.GetString("OIDCH_0045_Id_Token_Code_Missing"); } + } + + /// + /// OIDCH_0046: UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed. + /// + internal static string OIDCH_0046_UserInfo_Endpoint_Not_Set + { + get { return ResourceManager.GetString("OIDCH_0046_UserInfo_Endpoint_Not_Set"); } + } + /// /// OIDCH_0000: Entering: '{0}'. /// diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx index f998d95ad7..fe1d1837d5 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -162,6 +162,15 @@ OIDCH_0037: RedirectUri is: '{0}'. + + OIDCH_0038: Id Token is null. Redeeming code : {0} for tokens. + + + OIDCH_0039: Subject claim received from userinfo endpoint does not match the one in the id token. + + + OIDCH_0040: Sending request to user info endpoint for retrieving claims. + OIDCH_0000: Entering: '{0}'. @@ -228,4 +237,22 @@ OIDCH_0021: exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called. - + + OIDCH_0041: Subject claim not found in {0}. + + + OIDCH_0042: Authorization Code redeemed: '{0}' + + + OIDCH_0043: AuthorizationCodeRedeemedNotification.HandledResponse + + + OIDCH_0044: AuthorizationCodeRedeemedNotification.Skipped + + + OIDCH_0045: Cannot process the message. Both id_token and code are missing. + + + OIDCH_0046: UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed. + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs index 64f17d967c..f8886e4842 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/LoggingUtilities.cs @@ -43,6 +43,12 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { "OIDCH_0020:", LogLevel.Debug }, { "OIDCH_0021:", LogLevel.Verbose }, { "OIDCH_0026:", LogLevel.Error }, + { "OIDCH_0038:", LogLevel.Debug }, + { "OIDCH_0040:", LogLevel.Debug }, + { "OIDCH_0042:", LogLevel.Debug }, + { "OIDCH_0043:", LogLevel.Verbose }, + { "OIDCH_0044:", LogLevel.Verbose }, + { "OIDCH_0045:", LogLevel.Debug } }; BuildLogEntryList(); diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs index bf24d77914..02caabacdb 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectAuthenticationHandlerForTestingAuthenticate.cs @@ -2,9 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.IdentityModel.Protocols; +using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { @@ -14,7 +18,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect public class OpenIdConnectAuthenticationHandlerForTestingAuthenticate : OpenIdConnectAuthenticationHandler { public OpenIdConnectAuthenticationHandlerForTestingAuthenticate() - : base() + : base(null) { } @@ -32,6 +36,23 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { return Task.FromResult(0); } + protected override async Task RedeemAuthorizationCodeAsync(string authorizationCode, string redirectUri) + { + var jsonResponse = new JObject(); + jsonResponse.Add(OpenIdConnectParameterNames.IdToken, "test token"); + return new OpenIdConnectTokenEndpointResponse(jsonResponse); + } + + protected override async Task GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket) + { + var claimsIdentity = (ClaimsIdentity)ticket.Principal.Identity; + if (claimsIdentity == null) + { + claimsIdentity = new ClaimsIdentity(); + } + claimsIdentity.AddClaim(new Claim("test claim", "test value")); + return new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), ticket.Properties, ticket.AuthenticationScheme); + } //public override bool ShouldHandleScheme(string authenticationScheme) //{ diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs index a5cf7074c5..2492a8fcdb 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -104,8 +104,9 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); options.Notifications = new OpenIdConnectAuthenticationNotifications { - AuthorizationCodeReceived = notification => + AuthorizationCodeRedeemed = notification => { + notification.HandleResponse(); if (notification.ProtocolMessage.State == null && !notification.ProtocolMessage.Parameters.ContainsKey(ExpectedStateParameter)) return Task.FromResult(null); @@ -138,7 +139,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect get { var formater = new AuthenticationPropertiesFormaterKeyValue(); - var dataset = new TheoryData, OpenIdConnectMessage>(); + var dataset = new TheoryData, OpenIdConnectMessage>(); var properties = new AuthenticationProperties(); var message = new OpenIdConnectMessage(); var validState = UrlEncoder.Default.UrlEncode(formater.Protect(properties)); @@ -155,13 +156,13 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect // State - null, empty string, invalid message = new OpenIdConnectMessage(); - dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateNullOptions, message); + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 28 }, StateNullOptions, message); dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateNullOptions, message); dataset.Add(LogLevel.Error, new int[] { }, StateNullOptions, message); message = new OpenIdConnectMessage(); message.State = string.Empty; - dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7 }, StateEmptyOptions, message); + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 28 }, StateEmptyOptions, message); dataset.Add(LogLevel.Verbose, new int[] { 4, 7 }, StateEmptyOptions, message); dataset.Add(LogLevel.Error, new int[] { }, StateEmptyOptions, message); @@ -225,6 +226,34 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect dataset.Add(LogLevel.Verbose, new int[] { 7, 16 }, AuthorizationCodeReceivedSkippedOptions, message); dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedSkippedOptions, message); + message = new OpenIdConnectMessage(); + message.Code = Guid.NewGuid().ToString(); + message.State = validState; + message.IdToken = "test token"; + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 14, 15 }, AuthorizationCodeReceivedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 15 }, AuthorizationCodeReceivedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 7, 20, 14, 16 }, AuthorizationCodeReceivedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 7, 16 }, AuthorizationCodeReceivedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, AuthorizationCodeReceivedSkippedOptions, message); + + // CodeReceivedAndRedeemed and GetUserInformationFromUIEndpoint + message = new OpenIdConnectMessage(); + message.IdToken = null; + message.Code = Guid.NewGuid().ToString(); + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 26 }, CodeReceivedAndRedeemedHandledOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 26 }, CodeReceivedAndRedeemedHandledOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, CodeReceivedAndRedeemedHandledOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 27 }, CodeReceivedAndRedeemedSkippedOptions, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 27 }, CodeReceivedAndRedeemedSkippedOptions, message); + dataset.Add(LogLevel.Error, new int[] { }, CodeReceivedAndRedeemedSkippedOptions, message); + + dataset.Add(LogLevel.Debug, new int[] { 0, 1, 4, 7, 14, 23, 25, 24, 12 }, GetUserInfoFromUIEndpoint, message); + dataset.Add(LogLevel.Verbose, new int[] { 4, 7, 12 }, GetUserInfoFromUIEndpoint, message); + dataset.Add(LogLevel.Error, new int[] { }, GetUserInfoFromUIEndpoint, message); + return dataset; } } @@ -243,6 +272,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void AuthorizationCodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; + options.ProtocolValidator = MockProtocolValidator(); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -257,6 +288,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void AuthorizationCodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; + options.ProtocolValidator = MockProtocolValidator(); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -271,6 +304,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void AuthenticationErrorHandledOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; + options.ProtocolValidator = MockProtocolValidator(); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -285,6 +320,8 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void AuthenticationErrorSkippedOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; + options.ProtocolValidator = MockProtocolValidator(); options.Notifications = new OpenIdConnectAuthenticationNotifications { @@ -310,6 +347,57 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect }; } + private static void CodeReceivedAndRedeemedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.ResponseType = OpenIdConnectResponseTypes.Code; + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + AuthorizationCodeRedeemed = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void CodeReceivedAndRedeemedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.ResponseType = OpenIdConnectResponseTypes.Code; + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + AuthorizationCodeRedeemed = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void GetUserInfoFromUIEndpoint(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.ResponseType = OpenIdConnectResponseTypes.Code; + options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + options.GetClaimsFromUserInfoEndpoint = true; + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = (notification) => + { + var claimValue = notification.AuthenticationTicket.Principal.FindFirst("test claim"); + Assert.Equal(claimValue.Value, "test value"); + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); @@ -357,6 +445,21 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect }; } + private static ISecurityTokenValidator MockSecurityTokenValidator() + { + var mockValidator = new Mock(); + mockValidator.Setup(v => v.ValidateToken(It.IsAny(), It.IsAny(), out specCompliantJwt)).Returns(new ClaimsPrincipal()); + mockValidator.Setup(v => v.CanReadToken(It.IsAny())).Returns(true); + return mockValidator.Object; + } + + private static OpenIdConnectProtocolValidator MockProtocolValidator() + { + var mockProtocolValidator = new Mock(); + mockProtocolValidator.Setup(v => v.Validate(It.IsAny(), It.IsAny())); + return mockProtocolValidator.Object; + } + private static void SecurityTokenValidatorCannotReadToken(OpenIdConnectAuthenticationOptions options) { AuthenticationErrorHandledOptions(options); @@ -380,10 +483,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect private static void SecurityTokenValidatorValidatesAllTokens(OpenIdConnectAuthenticationOptions options) { DefaultOptions(options); - var mockValidator = new Mock(); - mockValidator.Setup(v => v.ValidateToken(It.IsAny(), It.IsAny(), out specCompliantJwt)).Returns(new ClaimsPrincipal()); - mockValidator.Setup(v => v.CanReadToken(It.IsAny())).Returns(true); - options.SecurityTokenValidators = new Collection { mockValidator.Object }; + options.SecurityTokenValidators = new Collection { MockSecurityTokenValidator() }; options.ProtocolValidator.RequireTimeStampInNonce = false; options.ProtocolValidator.RequireNonce = false; } diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index 07182c64ab..fc0533f883 100644 --- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -142,10 +142,6 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { properties.Items.Add("item1", Guid.NewGuid().ToString()); } - else - { - properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri); - } var server = CreateServer(options => { @@ -164,6 +160,16 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect var transaction = await SendAsync(server, DefaultHost + challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + if (challenge != ChallengeWithProperties) + { + if (userState != null) + { + properties.Items.Add(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, userState); + } + properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri); + } + queryValues.State = stateDataFormat.Protect(properties); queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State })); }