Move GenerateCorrelationId and ValidateCorrelationId to RemoteAuthenticationHandler

This commit is contained in:
Kévin Chalet 2016-01-18 00:20:44 +01:00 committed by Chris R
parent bafb097e9f
commit bbcabc0212
13 changed files with 126 additions and 187 deletions

View File

@ -1,12 +0,0 @@
// 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.
namespace Microsoft.AspNetCore.Authentication.OAuth
{
internal static class Constants
{
internal const string SecurityAuthenticate = "security.Authenticate";
internal const string CorrelationPrefix = ".AspNetCore.Correlation.";
}
}

View File

@ -7,15 +7,12 @@ using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
@ -23,8 +20,6 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
{
public class OAuthHandler<TOptions> : RemoteAuthenticationHandler<TOptions> where TOptions : OAuthOptions
{
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
public OAuthHandler(HttpClient backchannel)
{
Backchannel = backchannel;
@ -177,7 +172,11 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
throw new ArgumentNullException(nameof(context));
}
var properties = new AuthenticationProperties(context.Properties);
var properties = new AuthenticationProperties(context.Properties)
{
ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout)
};
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = CurrentUri;
@ -216,71 +215,5 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
// OAuth2 3.3 space separated
return string.Join(" ", Options.Scope);
}
protected void GenerateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var correlationKey = Constants.CorrelationPrefix + Options.AuthenticationScheme;
var nonceBytes = new byte[32];
CryptoRandom.GetBytes(nonceBytes);
var correlationId = Base64UrlTextEncoder.Encode(nonceBytes);
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps
};
properties.Items[correlationKey] = correlationId;
Response.Cookies.Append(correlationKey, correlationId, cookieOptions);
}
protected bool ValidateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var correlationKey = Constants.CorrelationPrefix + Options.AuthenticationScheme;
var correlationCookie = Request.Cookies[correlationKey];
if (string.IsNullOrEmpty(correlationCookie))
{
Logger.LogWarning("{0} cookie not found.", correlationKey);
return false;
}
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps
};
Response.Cookies.Delete(correlationKey, cookieOptions);
string correlationExtra;
if (!properties.Items.TryGetValue(
correlationKey,
out correlationExtra))
{
Logger.LogWarning("{0} state property not found.", correlationKey);
return false;
}
properties.Items.Remove(correlationKey);
if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal))
{
Logger.LogWarning("{0} correlation cookie and state property mismatch.", correlationKey);
return false;
}
return true;
}
}
}

View File

@ -28,11 +28,6 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
/// </summary>
public static readonly string CookieNoncePrefix = ".AspNetCore.OpenIdConnect.Nonce.";
/// <summary>
/// The prefix used for the state in the cookie.
/// </summary>
public static readonly string CookieStatePrefix = ".AspNetCore.OpenIdConnect.State.";
/// <summary>
/// The property for the RedirectUri that was used when asking for a 'authorizationCode'.
/// </summary>

View File

@ -184,7 +184,10 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
// order for local RedirectUri
// 1. challenge.Properties.RedirectUri
// 2. CurrentUri if RedirectUri is not set)
var properties = new AuthenticationProperties(context.Properties);
var properties = new AuthenticationProperties(context.Properties)
{
ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout)
};
if (string.IsNullOrEmpty(properties.RedirectUri))
{
@ -810,7 +813,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
{
HttpOnly = true,
Secure = Request.IsHttps,
Expires = DateTime.UtcNow + Options.ProtocolValidator.NonceLifetime
Expires = Options.SystemClock.UtcNow.Add(Options.ProtocolValidator.NonceLifetime)
});
}
@ -857,76 +860,6 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
return null;
}
private void GenerateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var correlationKey = OpenIdConnectDefaults.CookieStatePrefix;
var nonceBytes = new byte[32];
CryptoRandom.GetBytes(nonceBytes);
var correlationId = Base64UrlTextEncoder.Encode(nonceBytes);
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps,
Expires = DateTime.UtcNow + Options.ProtocolValidator.NonceLifetime
};
properties.Items[correlationKey] = correlationId;
Response.Cookies.Append(correlationKey + correlationId, NonceProperty, cookieOptions);
}
private bool ValidateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var correlationKey = OpenIdConnectDefaults.CookieStatePrefix;
string correlationId;
if (!properties.Items.TryGetValue(
correlationKey,
out correlationId))
{
Logger.LogWarning(26, "{0} state property not found.", correlationKey);
return false;
}
properties.Items.Remove(correlationKey);
var cookieName = correlationKey + correlationId;
var correlationCookie = Request.Cookies[cookieName];
if (string.IsNullOrEmpty(correlationCookie))
{
Logger.LogWarning(27, "{0} cookie not found.", cookieName);
return false;
}
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps
};
Response.Cookies.Delete(cookieName, cookieOptions);
if (!string.Equals(correlationCookie, NonceProperty, StringComparison.Ordinal))
{
Logger.LogWarning(28, "{0} correlation cookie and state property mismatch.", correlationKey);
return false;
}
return true;
}
private AuthenticationProperties GetPropertiesFromState(string state)
{
// assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>

View File

@ -122,7 +122,11 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
throw new ArgumentNullException(nameof(context));
}
var properties = new AuthenticationProperties(context.Properties);
var properties = new AuthenticationProperties(context.Properties)
{
ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout)
};
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = CurrentUri;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.ComponentModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Twitter;
using Microsoft.AspNetCore.Http;
@ -50,5 +51,11 @@ namespace Microsoft.AspNetCore.Builder
get { return (ITwitterEvents)base.Events; }
set { base.Events = value; }
}
/// <summary>
/// For testing purposes only.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public ISystemClock SystemClock { get; set; } = new SystemClock();
}
}

View File

@ -2,15 +2,24 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Authentication
{
public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions> where TOptions : RemoteAuthenticationOptions
{
private const string CorrelationPrefix = ".AspNetCore.Correlation.";
private const string CorrelationProperty = ".xsrf";
private const string CorrelationMarker = "N";
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
public override async Task<bool> HandleRequestAsync()
{
if (Options.CallbackPath == Request.Path)
@ -99,5 +108,71 @@ namespace Microsoft.AspNetCore.Authentication
{
throw new NotSupportedException();
}
protected virtual void GenerateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
var bytes = new byte[32];
CryptoRandom.GetBytes(bytes);
var correlationId = Base64UrlTextEncoder.Encode(bytes);
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps,
Expires = properties.ExpiresUtc
};
properties.Items[CorrelationProperty] = correlationId;
var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId;
Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions);
}
protected virtual bool ValidateCorrelationId(AuthenticationProperties properties)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
string correlationId;
if (!properties.Items.TryGetValue(CorrelationProperty, out correlationId))
{
Logger.LogWarning(26, "{0} state property not found.", CorrelationPrefix);
return false;
}
properties.Items.Remove(CorrelationProperty);
var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId;
var correlationCookie = Request.Cookies[cookieName];
if (string.IsNullOrEmpty(correlationCookie))
{
Logger.LogWarning(27, "'{0}' cookie not found.", cookieName);
return false;
}
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = Request.IsHttps
};
Response.Cookies.Delete(cookieName, cookieOptions);
if (!string.Equals(correlationCookie, CorrelationMarker, StringComparison.Ordinal))
{
Logger.LogWarning(28, "The correlation cookie value '{0}' did not match the expected value '{1}'.", cookieName);
return false;
}
return true;
}
}
}

View File

@ -56,6 +56,11 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public bool SaveTokensAsClaims { get; set; }
/// <summary>
/// Gets or sets the time limit for completing the authentication flow (15 minutes by default).
/// </summary>
public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15);
public IRemoteAuthenticationEvents Events = new RemoteAuthenticationEvents();
}
}

View File

@ -212,14 +212,14 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
}, handler: null);
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Facebook";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync(
"https://example.com/signin-facebook?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Facebook.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?'));

View File

@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
ClientSecret = "Test Secret"
});
var transaction = await server.SendAsync("https://example.com/challenge");
Assert.Contains(".AspNetCore.Correlation.Google=", transaction.SetCookie.Single());
Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google."));
}
[Fact]
@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
AutomaticChallenge = true
});
var transaction = await server.SendAsync("https://example.com/401");
Assert.Contains(".AspNetCore.Correlation.Google=", transaction.SetCookie.Single());
Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google."));
}
[Fact]
@ -335,18 +335,18 @@ namespace Microsoft.AspNetCore.Authentication.Google
}
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
Assert.Equal(2, transaction.SetCookie.Count);
Assert.Contains(correlationKey, transaction.SetCookie[0]);
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
var authCookie = transaction.AuthenticationCookieValue;
@ -394,7 +394,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
} : new OAuthEvents()
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
@ -402,7 +402,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
var state = stateFormat.Protect(properties);
var sendTask = server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
if (redirect)
{
var transaction = await sendTask;
@ -446,14 +446,14 @@ namespace Microsoft.AspNetCore.Authentication.Google
} : new OAuthEvents()
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties);
var sendTask = server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
if (redirect)
{
var transaction = await sendTask;
@ -528,18 +528,18 @@ namespace Microsoft.AspNetCore.Authentication.Google
}
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
Assert.Equal(2, transaction.SetCookie.Count);
Assert.Contains(correlationKey, transaction.SetCookie[0]);
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
var authCookie = transaction.AuthenticationCookieValue;
@ -607,17 +607,17 @@ namespace Microsoft.AspNetCore.Authentication.Google
}
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First());
Assert.Equal(2, transaction.SetCookie.Count);
Assert.Contains(correlationKey, transaction.SetCookie[0]);
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
}
@ -690,7 +690,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Google";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/foo";
@ -699,7 +699,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
//Post a message to the Google middleware
var transaction = await server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Google.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First());

View File

@ -152,18 +152,18 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
}
});
var properties = new AuthenticationProperties();
var correlationKey = ".AspNetCore.Correlation.Microsoft";
var correlationKey = ".xsrf";
var correlationValue = "TestCorrelationId";
properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync(
"https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue);
$".AspNetCore.Correlation.Microsoft.{correlationValue}=N");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
Assert.Equal(2, transaction.SetCookie.Count);
Assert.Contains(correlationKey, transaction.SetCookie[0]);
Assert.Contains($".AspNetCore.Correlation.Microsoft.{correlationValue}", transaction.SetCookie[0]);
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
var authCookie = transaction.AuthenticationCookieValue;

View File

@ -22,11 +22,10 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect
return "null";
}
var encoder = UrlEncoder.Default;
var sb = new StringBuilder();
foreach(var item in data.Items)
{
sb.Append(encoder.Encode(item.Key) + " " + encoder.Encode(item.Value) + " ");
sb.Append(Uri.EscapeDataString(item.Key) + " " + Uri.EscapeDataString(item.Value) + " ");
}
return sb.ToString();

View File

@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect
Assert.Contains("expires", firstCookie);
var secondCookie = transaction.SetCookie.Skip(1).First();
Assert.Contains(OpenIdConnectDefaults.CookieStatePrefix, secondCookie);
Assert.StartsWith(".AspNetCore.Correlation.OpenIdConnect.", secondCookie);
Assert.Contains("expires", secondCookie);
}