diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs
index cc132e9bb..61a9f87e5 100644
--- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs
+++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs
@@ -1,11 +1,14 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens;
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;
@@ -13,6 +16,7 @@
using Microsoft.AspNet.Http.Authentication;
using Microsoft.Framework.Logging;
using Microsoft.IdentityModel.Protocols;
+using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
@@ -38,6 +42,13 @@ private string CurrentUri
}
}
+ protected HttpClient Backchannel { get; private set; }
+
+ public OpenIdConnectAuthenticationHandler(HttpClient backchannel)
+ {
+ Backchannel = backchannel;
+ }
+
protected override void ApplyResponseGrant()
{
ApplyResponseGrantAsync().GetAwaiter().GetResult();
@@ -153,17 +164,16 @@ protected override async Task ApplyResponseChallengeAsync()
properties.RedirectUri = CurrentUri;
}
- if (!string.IsNullOrWhiteSpace(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.IsNullOrWhiteSpace(Options.RedirectUri))
{
+ Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri);
properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri);
}
+ // adding response type to properties so that we can determine the response type of an incoming message.
+ properties.Items.Add(OpenIdConnectParameterNames.ResponseType, Options.ResponseType);
+
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
@@ -183,6 +193,12 @@ protected override async Task ApplyResponseChallengeAsync()
State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.UrlEncode(Options.StateDataFormat.Protect(properties))
};
+ // if response_type=code, nonce is not required.
+ if (Options.ResponseType.Equals(OpenIdConnectResponseTypes.Code))
+ {
+ Options.ProtocolValidator.RequireNonce = false;
+ }
+
if (Options.ProtocolValidator.RequireNonce)
{
message.Nonce = Options.ProtocolValidator.GenerateNonce();
@@ -238,6 +254,7 @@ protected override AuthenticationTicket AuthenticateCore()
/// Uses log id's OIDCH-0000 - OIDCH-0025
protected override async Task AuthenticateCoreAsync()
{
+ OpenIdConnectMessage tokens = null;
Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType());
// Allow login to be constrained to a specific path. Need to make this runtime configurable.
@@ -299,12 +316,16 @@ protected override async Task AuthenticateCoreAsync()
}
var properties = GetPropertiesFromState(message.State);
+
if (properties == null)
{
Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid);
return null;
}
+ var isCodeOnlyFlow = properties.Items.ContainsKey(OpenIdConnectParameterNames.ResponseType) ?
+ (properties.Items[OpenIdConnectParameterNames.ResponseType] == OpenIdConnectResponseTypes.Code) : false;
+
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
if (!string.IsNullOrWhiteSpace(message.Error))
{
@@ -321,6 +342,21 @@ protected override async Task AuthenticateCoreAsync()
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
+ // Redeeming authorization code for tokens
+ if (string.IsNullOrWhiteSpace(message.IdToken) && isCodeOnlyFlow)
+ {
+ Logger.LogDebug(Resources.OIDCH_0037_Redeeming_Auth_Code, message.Code);
+
+ var redirectUri = properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ?
+ properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : Options.RedirectUri;
+
+ tokens = await RedeemAuthorizationCode(message.Code, redirectUri);
+ if (tokens != null)
+ {
+ message.IdToken = tokens.IdToken;
+ }
+ }
+
// OpenIdConnect protocol allows a Code to be received without the id_token
if (!string.IsNullOrWhiteSpace(message.IdToken))
{
@@ -409,6 +445,12 @@ protected override async Task AuthenticateCoreAsync()
}
}
+ if (Options.GetClaimsFromUserInfoEndpoint)
+ {
+ Logger.LogDebug(Resources.OIDCH_0039_Sending_Request_UIEndpoint);
+ ticket = await GetUserInformationAsync(properties, tokens, ticket);
+ }
+
var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification(Context, Options)
{
@@ -429,27 +471,31 @@ protected override async Task AuthenticateCoreAsync()
return null;
}
- string nonce = jwt.Payload.Nonce;
- if (Options.NonceCache != null)
+ // If id_token is received using code only flow, no need to validate chash and nonce.
+ if (!isCodeOnlyFlow)
{
- // if the nonce cannot be removed, it was used
- if (!Options.NonceCache.TryRemoveNonce(nonce))
+ string nonce = jwt.Payload.Nonce;
+ if (Options.NonceCache != null)
+ {
+ // if the nonce cannot be removed, it was used
+ if (!Options.NonceCache.TryRemoveNonce(nonce))
+ {
+ nonce = null;
+ }
+ }
+ else
{
- nonce = null;
+ nonce = ReadNonceCookie(nonce);
}
- }
- else
- {
- nonce = ReadNonceCookie(nonce);
- }
- var protocolValidationContext = new OpenIdConnectProtocolValidationContext
- {
- AuthorizationCode = message.Code,
- Nonce = nonce,
- };
+ var protocolValidationContext = new OpenIdConnectProtocolValidationContext
+ {
+ AuthorizationCode = message.Code,
+ Nonce = nonce,
+ };
- Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
+ Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
+ }
}
if (message.Code != null)
@@ -520,6 +566,90 @@ protected override async Task AuthenticateCoreAsync()
}
}
+ protected virtual async Task RedeemAuthorizationCode(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 OpenIdConnectMessage()
+ {
+ AccessToken = jsonTokenResponse.Value(OpenIdConnectParameterNames.AccessToken),
+ IdToken = jsonTokenResponse.Value(OpenIdConnectParameterNames.IdToken),
+ TokenType = jsonTokenResponse.Value(OpenIdConnectParameterNames.TokenType),
+ ExpiresIn = jsonTokenResponse.Value(OpenIdConnectParameterNames.ExpiresIn)
+ };
+ }
+
+ protected virtual async Task GetUserInformationAsync(AuthenticationProperties properties, OpenIdConnectMessage message, AuthenticationTicket ticket)
+ {
+ string userInfoEndpoint = null;
+ if (_configuration != null)
+ {
+ userInfoEndpoint = _configuration.UserInfoEndpoint;
+ }
+
+ if (string.IsNullOrEmpty(userInfoEndpoint))
+ {
+ userInfoEndpoint = Options.Configuration != null ? Options.Configuration.UserInfoEndpoint : null;
+ }
+
+ if (string.IsNullOrEmpty(userInfoEndpoint))
+ {
+ Logger.LogError("UserInfo endpoint is not set. Request to retrieve claims from userinfo endpoint cannot be completed.");
+ 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 subject = identity.FindFirst(ClaimTypes.NameIdentifier).Value;
+
+ // check if the sub claim matches
+ var userInfoSubject = user.Value("sub");
+ if (userInfoSubject == null || !string.Equals(userInfoSubject, subject, StringComparison.OrdinalIgnoreCase))
+ {
+ Logger.LogError(Resources.OIDCH_0038_Subject_Claim_Mismatch);
+ throw new ArgumentException(Resources.OIDCH_0038_Subject_Claim_Mismatch);
+ }
+
+ var userInfoIdentity = new ClaimsIdentity(identity);
+ foreach (var pair in user)
+ {
+ JToken value;
+ var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null;
+
+ var inboundClaimTypeMap = new Dictionary(JwtSecurityTokenHandler.InboundClaimTypeMap);
+ string longClaimTypeName;
+ inboundClaimTypeMap.TryGetValue(pair.Key, out longClaimTypeName);
+
+ // checking if claim exist with either short or long name
+ if (!(identity.HasClaim(pair.Key, claimValue) || identity.HasClaim(longClaimTypeName, claimValue)))
+ {
+ userInfoIdentity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, Options.ClaimsIssuer));
+ }
+ }
+
+ ticket.Principal.AddIdentity(userInfoIdentity);
+ return ticket;
+ }
+
///
/// Adds the nonce to .
///
diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs
index 9353f385c..a507dee49 100644
--- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs
+++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -13,11 +13,11 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.DataProtection;
using Microsoft.AspNet.Http;
+using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
-using Microsoft.IdentityModel.Protocols;
-using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders;
+using Microsoft.IdentityModel.Protocols;
namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
@@ -100,6 +100,11 @@ public OpenIdConnectAuthenticationMiddleware(
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)
@@ -119,21 +124,20 @@ public OpenIdConnectAuthenticationMiddleware(
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);
}
}
}
+ 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/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs
index 9e4bfa55d..d77dd87fb 100644
--- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs
+++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs
@@ -55,6 +55,7 @@ public OpenIdConnectAuthenticationOptions(string authenticationScheme)
AuthenticationScheme = authenticationScheme;
BackchannelTimeout = TimeSpan.FromMinutes(1);
Caption = OpenIdConnectAuthenticationDefaults.Caption;
+ GetClaimsFromUserInfoEndpoint = false;
ProtocolValidator = new OpenIdConnectProtocolValidator();
RefreshOnIssuerKeyNotFound = true;
ResponseMode = OpenIdConnectResponseModes.FormPost;
@@ -163,6 +164,11 @@ public string Caption
///
public bool DefaultToCurrentUriOnRedirect { get; set; }
+ ///
+ /// Boolean to set whether the middleware should go to user info endpoint to retrieve claims or not.
+ ///
+ public bool GetClaimsFromUserInfoEndpoint { get; set; }
+
///
/// Gets or sets the discovery endpoint for obtaining metadata
///
diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs
index b95de8318..ad1702f04 100644
--- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs
@@ -172,6 +172,30 @@ internal static string OIDCH_0036_UriIsNotWellFormed
get { return ResourceManager.GetString("OIDCH_0036_UriIsNotWellFormed"); }
}
+ ///
+ /// OIDCH_0037: Id Token is null. Redeeming code : {0} for tokens.
+ ///
+ internal static string OIDCH_0037_Redeeming_Auth_Code
+ {
+ get { return ResourceManager.GetString("OIDCH_0037_Redeeming_Auth_Code"); }
+ }
+
+ ///
+ /// OIDCH_0038: Subject claim received from userinfo endpoint does not match the one in the id token.
+ ///
+ internal static string OIDCH_0038_Subject_Claim_Mismatch
+ {
+ get { return ResourceManager.GetString("OIDCH_0038_Subject_Claim_Mismatch"); }
+ }
+
+ ///
+ /// OIDCH_0038: Subject claim received from userinfo endpoint does not match the one in the id token.
+ ///
+ internal static string OIDCH_0039_Sending_Request_UIEndpoint
+ {
+ get { return ResourceManager.GetString("OIDCH_0039_Sending_Request_UIEndpoint"); }
+ }
+
///
/// OIDCH_0000: Entering: '{0}'.
///
diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx
index 454dae209..0084347f2 100644
--- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx
+++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx
@@ -222,4 +222,13 @@
OIDCH_0020: 'id_token' received: '{0}'
-
+
+ OIDCH_0037: Id Token is null. Redeeming code : {0} for tokens.
+
+
+ OIDCH_0038: Subject claim received from userinfo endpoint does not match the one in the id token.
+
+
+ OIDCH_0039: Sending request to user info endpoint for retrieving claims.
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs
index 183ae6018..6cc2afd4a 100644
--- a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs
+++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
// this controls if the logs are written to the console.
@@ -7,9 +7,13 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IdentityModel.Tokens;
using System.Net.Http;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authentication.Notifications;
+using Microsoft.AspNet.Authentication.OAuth;
using Microsoft.AspNet.Authentication.OpenIdConnect;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.DataProtection;
@@ -62,6 +66,8 @@ static OpenIdConnectHandlerTests()
{ "OIDCH_0019:", LogLevel.Debug },
{ "OIDCH_0020:", LogLevel.Debug },
{ "OIDCH_0026:", LogLevel.Error },
+ { "OIDCH_0037:", LogLevel.Debug },
+ { "OIDCH_0039:", LogLevel.Debug }
};
BuildLogEntryList();
@@ -157,6 +163,13 @@ public async Task AuthenticateCore()
logsEntriesExpected = new int[] {0, 1, 7, 20, 9 };
await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedSkippedOptions, errors, logsEntriesExpected);
+ message.IdToken = null;
+ logsEntriesExpected = new int[] { 0, 1, 7, 22, 20, 8};
+ await RunVariation(LogLevel.Debug, message, CodeReceivedAndRedeemedHandledOptions, errors, logsEntriesExpected);
+
+ logsEntriesExpected = new int[] { 0, 1, 7, 22, 20, 23, 12 };
+ await RunVariation(LogLevel.Debug, message, GetUserInfoFromUIEndpoint, errors, logsEntriesExpected);
+
#if _Verbose
Console.WriteLine("\n ===== \n");
DisplayErrors(errors);
@@ -317,6 +330,22 @@ private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOption
};
}
+ private static void CodeReceivedAndRedeemedHandledOptions(OpenIdConnectAuthenticationOptions options)
+ {
+ DefaultOptions(options);
+ options.ResponseType = OpenIdConnectResponseTypes.Code;
+ options.StateDataFormat = new CodeOnlyAuthenticationPropertiesFormater();
+ options.Notifications =
+ new OpenIdConnectAuthenticationNotifications
+ {
+ SecurityTokenReceived = (notification) =>
+ {
+ notification.HandleResponse();
+ return Task.FromResult