Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Adding code only flow for sign in. #258

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
42f9a55
Adding logging draft.
tushargupta51 Mar 25, 2015
6e32a19
Hooking up wilson event source to oidc handler.
tushargupta51 Mar 25, 2015
5e076e9
Reverting changes to OIDCsample\startup.cs
tushargupta51 Mar 31, 2015
2b0d0ad
Removing the unused file reference
tushargupta51 Mar 31, 2015
9fc2eb7
changing wilsonEventSource to IdentityModelEventSource
tushargupta51 Apr 10, 2015
3d0eb38
Adding code only sign in flow
tushargupta51 May 12, 2015
719e66e
Fixing the test case for code redemption
tushargupta51 May 13, 2015
f3d44a9
Adding code only response type constant
tushargupta51 May 13, 2015
8e5c49a
Correcting the constant name
tushargupta51 May 13, 2015
9c94b49
replacing string with constant variable for code only flow reponse ty…
tushargupta51 May 13, 2015
d430b5d
Adding .designer.cs file
tushargupta51 May 14, 2015
c3b28a9
Removing adal dependency and addressing some github comments
tushargupta51 May 19, 2015
c6f45ba
retrieving claims from userinfo endpoint
tushargupta51 May 22, 2015
081ee8f
removing changes to tokenresponse and cleanup
tushargupta51 May 22, 2015
a34370a
Addressing github comments - removing system dependencies and code cl…
tushargupta51 May 26, 2015
334e66f
Taking off the wilson logging adapter, adding checks for userinfo end…
tushargupta51 Jun 14, 2015
d350329
Removing the .vs folder that was pushed by mistake
tushargupta51 Jun 14, 2015
abd67c1
Cleanup
tushargupta51 Jun 14, 2015
37208ce
updating the redirect uri, adding test for getuserInfo and some other…
tushargupta51 Jun 17, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BOM?

// 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;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.Framework.Logging;
using Microsoft.IdentityModel.Protocols;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNet.Authentication.OpenIdConnect
{
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check the value of OpenIdConnectMessage.ResponseType, since it can be set in RedirectToIdentityProvider later. So any micro changes should be based off of the final request sent to the IDP.

{
Options.ProtocolValidator.RequireNonce = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will influence id_token + code that comes on a different message.
We need to make setting this independent of the value on ProtocolValidator.

}

if (Options.ProtocolValidator.RequireNonce)
{
message.Nonce = Options.ProtocolValidator.GenerateNonce();
Expand Down Expand Up @@ -238,6 +254,7 @@ protected override AuthenticationTicket AuthenticateCore()
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks>
protected override async Task<AuthenticationTicket> 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.
Expand Down Expand Up @@ -299,12 +316,16 @@ protected override async Task<AuthenticationTicket> 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))
{
Expand All @@ -321,6 +342,21 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

// Redeeming authorization code for tokens
if (string.IsNullOrWhiteSpace(message.IdToken) && isCodeOnlyFlow)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just because Options.ResponseType == CodeOnly, that does not mean the message is 'code' only. We need to determine the type of response from the actual message received. This is particularly important when a site is being attacked. Also, consider that RedirectToIdentityProvider can change or set the ResponseType.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the response_type property to the state.

{
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))
{
Expand Down Expand Up @@ -409,6 +445,12 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
}
}

if (Options.GetClaimsFromUserInfoEndpoint)
{
Logger.LogDebug(Resources.OIDCH_0039_Sending_Request_UIEndpoint);
ticket = await GetUserInformationAsync(properties, tokens, ticket);
}

var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
Expand All @@ -429,27 +471,31 @@ protected override async Task<AuthenticationTicket> 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)
Expand Down Expand Up @@ -520,6 +566,90 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
}
}

protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCode(string authorizationCode, string redirectUri)
{
var openIdMessage = new OpenIdConnectMessage()
{
ClientId = Options.ClientId,
ClientSecret = Options.ClientSecret,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can start with ClientSecret, but we will need an issue for: Certs and SignedJwt (AssertionCredentials).

Code = authorizationCode,
GrantType = "authorization_code",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look for a constant in Wilson, if not open an issue in Wilson.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<string>(OpenIdConnectParameterNames.AccessToken),
IdToken = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.IdToken),
TokenType = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.TokenType),
ExpiresIn = jsonTokenResponse.Value<string>(OpenIdConnectParameterNames.ExpiresIn)
};
}

protected virtual async Task<AuthenticationTicket> 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specifically look for the 'sub' claim. Best to check with Mike Jones. Need to be mind-full of how claims mapping will influence this. When you use the ClaimsIdentity, a mapping has been performed by the JwtSecurityTokenHandler.


// check if the sub claim matches
var userInfoSubject = user.Value<string>("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<string, string>(JwtSecurityTokenHandler.InboundClaimTypeMap);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each time a claim is mapped by the JwtSecurityTokenHandler the 'short' name is captured in the properties of the claim. See JwtSecurityTokenHandler.ShortClaimTypeProperty

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;
}

/// <summary>
/// Adds the nonce to <see cref="HttpResponse.Cookies"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configurable via options?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if (Options.ConfigurationManager == null)
{
if (Options.Configuration != null)
Expand All @@ -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<OpenIdConnectConfiguration>(Options.MetadataAddress, httpClient);
Options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(Options.MetadataAddress, Backchannel);
}
}
}

protected HttpClient Backchannel { get; private set; }

/// <summary>
/// Provides the <see cref="AuthenticationHandler"/> object for processing authentication-related requests.
/// </summary>
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="OpenIdConnectAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<OpenIdConnectAuthenticationOptions> CreateHandler()
{
return new OpenIdConnectAuthenticationHandler();
return new OpenIdConnectAuthenticationHandler(Backchannel);
}

[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -163,6 +164,11 @@ public string Caption
/// </summary>
public bool DefaultToCurrentUriOnRedirect { get; set; }

/// <summary>
/// Boolean to set whether the middleware should go to user info endpoint to retrieve claims or not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to indicate that this is an additional step after setting the Identity to the Id_token that arrived from the TokenEndpoint.

/// </summary>
public bool GetClaimsFromUserInfoEndpoint { get; set; }

/// <summary>
/// Gets or sets the discovery endpoint for obtaining metadata
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,13 @@
<data name="OIDCH_0020_IdTokenReceived" xml:space="preserve">
<value>OIDCH_0020: 'id_token' received: '{0}'</value>
</data>
</root>
<data name="OIDCH_0037_Redeeming_Auth_Code" xml:space="preserve">
<value>OIDCH_0037: Id Token is null. Redeeming code : {0} for tokens.</value>
</data>
<data name="OIDCH_0038_Subject_Claim_Mismatch" xml:space="preserve">
<value>OIDCH_0038: Subject claim received from userinfo endpoint does not match the one in the id token.</value>
</data>
<data name="OIDCH_0039_Sending_Request_UIEndpoint" xml:space="preserve">
<value>OIDCH_0039: Sending request to user info endpoint for retrieving claims.</value>
</data>
</root>
Loading