From 72210e4078a7e92197ac83af64c5fe864d833984 Mon Sep 17 00:00:00 2001 From: Patrick Westerhoff Date: Fri, 23 Mar 2018 17:13:02 +0100 Subject: [PATCH] Add AuthenticationProperties.Parameters (#1008) Add a `Parameters` bag to the authentication properties that allow passing arbitrary parameters to an authentication handler. These values are not intended for serialization of persistence, only for flowing data between call sites. Also make existing `Items` collection helpers protected to allow them to be reused in subclasses, make string-based helpers public as a public way to work with the collection, and add helper methods to interact with the `Parameters` dictionary. --- .../AuthenticationProperties.cs | 91 ++++++-- .../AuthenticationPropertiesTests.cs | 207 ++++++++++++++++++ 2 files changed, 284 insertions(+), 14 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs index 9d1e670ea8..271329209a 100644 --- a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs @@ -20,20 +20,29 @@ namespace Microsoft.AspNetCore.Authentication internal const string UtcDateTimeFormat = "r"; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class. /// public AuthenticationProperties() - : this(items: null) - { - } + : this(items: null, parameters: null) + { } /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class. /// - /// + /// State values dictionary to use. public AuthenticationProperties(IDictionary items) + : this(items, parameters: null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + /// Parameters dictionary to use. + public AuthenticationProperties(IDictionary items, IDictionary parameters) { Items = items ?? new Dictionary(StringComparer.Ordinal); + Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); } /// @@ -41,6 +50,12 @@ namespace Microsoft.AspNetCore.Authentication /// public IDictionary Items { get; } + /// + /// Collection of parameters that are passed to the authentication handler. These are not intended for + /// serialization or persistence, only for flowing data between call sites. + /// + public IDictionary Parameters { get; } + /// /// Gets or sets whether the authentication session is persisted across multiple requests. /// @@ -86,12 +101,22 @@ namespace Microsoft.AspNetCore.Authentication set => SetBool(RefreshKey, value); } - private string GetString(string key) + /// + /// Get a string value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + public string GetString(string key) { return Items.TryGetValue(key, out string value) ? value : null; } - private void SetString(string key, string value) + /// + /// Set a string value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + public void SetString(string key, string value) { if (value != null) { @@ -103,16 +128,44 @@ namespace Microsoft.AspNetCore.Authentication } } - private bool? GetBool(string key) + /// + /// Get a parameter from the collection. + /// + /// Parameter type. + /// Parameter key. + /// Retrieved value or the default value if the property is not set. + public T GetParameter(string key) + => Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default; + + /// + /// Set a parameter value in the collection. + /// + /// Parameter type. + /// Parameter key. + /// Value to set. + public void SetParameter(string key, T value) + => Parameters[key] = value; + + /// + /// Get a bool value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + protected bool? GetBool(string key) { - if (Items.TryGetValue(key, out string value) && bool.TryParse(value, out bool refresh)) + if (Items.TryGetValue(key, out string value) && bool.TryParse(value, out bool boolValue)) { - return refresh; + return boolValue; } return null; } - private void SetBool(string key, bool? value) + /// + /// Set a bool value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + protected void SetBool(string key, bool? value) { if (value.HasValue) { @@ -124,7 +177,12 @@ namespace Microsoft.AspNetCore.Authentication } } - private DateTimeOffset? GetDateTimeOffset(string key) + /// + /// Get a DateTimeOffset value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + protected DateTimeOffset? GetDateTimeOffset(string key) { if (Items.TryGetValue(key, out string value) && DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dateTimeOffset)) @@ -134,7 +192,12 @@ namespace Microsoft.AspNetCore.Authentication return null; } - private void SetDateTimeOffset(string key, DateTimeOffset? value) + /// + /// Set a DateTimeOffset value in the collection. + /// + /// Property key. + /// Value to set or null to remove the property. + protected void SetDateTimeOffset(string key, DateTimeOffset? value) { if (value.HasValue) { diff --git a/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs new file mode 100644 index 0000000000..639c9b558e --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Core.Test +{ + public class AuthenticationPropertiesTests + { + [Fact] + public void DefaultConstructor_EmptyCollections() + { + var props = new AuthenticationProperties(); + Assert.Empty(props.Items); + Assert.Empty(props.Parameters); + } + + [Fact] + public void ItemsConstructor_ReusesItemsDictionary() + { + var items = new Dictionary + { + ["foo"] = "bar", + }; + var props = new AuthenticationProperties(items); + Assert.Same(items, props.Items); + Assert.Empty(props.Parameters); + } + + [Fact] + public void FullConstructor_ReusesDictionaries() + { + var items = new Dictionary + { + ["foo"] = "bar", + }; + var parameters = new Dictionary + { + ["number"] = 1234, + ["list"] = new List { "a", "b", "c" }, + }; + var props = new AuthenticationProperties(items, parameters); + Assert.Same(items, props.Items); + Assert.Same(parameters, props.Parameters); + } + + [Fact] + public void GetSetString() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetString("foo")); + Assert.Equal(0, props.Items.Count); + + props.SetString("foo", "foo bar"); + Assert.Equal("foo bar", props.GetString("foo")); + Assert.Equal("foo bar", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("foo", "foo baz"); + Assert.Equal("foo baz", props.GetString("foo")); + Assert.Equal("foo baz", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("bar", "xy"); + Assert.Equal("xy", props.GetString("bar")); + Assert.Equal("xy", props.Items["bar"]); + Assert.Equal(2, props.Items.Count); + + props.SetString("bar", string.Empty); + Assert.Equal(string.Empty, props.GetString("bar")); + Assert.Equal(string.Empty, props.Items["bar"]); + + props.SetString("foo", null); + Assert.Null(props.GetString("foo")); + Assert.Equal(1, props.Items.Count); + } + + [Fact] + public void GetSetParameter_String() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", "foo bar"); + Assert.Equal("foo bar", props.GetParameter("foo")); + Assert.Equal("foo bar", props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void GetSetParameter_Int() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", 123); + Assert.Equal(123, props.GetParameter("foo")); + Assert.Equal(123, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void GetSetParameter_Collection() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + var list = new string[] { "a", "b", "c" }; + props.SetParameter>("foo", list); + Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter>("foo")); + Assert.Same(list, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter>("foo", null); + Assert.Null(props.GetParameter>("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } + + [Fact] + public void IsPersistent_Test() + { + var props = new AuthenticationProperties(); + Assert.False(props.IsPersistent); + + props.IsPersistent = true; + Assert.True(props.IsPersistent); + Assert.Equal(string.Empty, props.Items.First().Value); + + props.Items.Clear(); + Assert.False(props.IsPersistent); + } + + [Fact] + public void RedirectUri_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.RedirectUri); + + props.RedirectUri = "http://example.com"; + Assert.Equal("http://example.com", props.RedirectUri); + Assert.Equal("http://example.com", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.RedirectUri); + } + + [Fact] + public void IssuedUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.IssuedUtc); + + props.IssuedUtc = new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)), props.IssuedUtc); + Assert.Equal("Wed, 21 Mar 2018 00:00:00 GMT", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.IssuedUtc); + } + + [Fact] + public void ExpiresUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.ExpiresUtc); + + props.ExpiresUtc = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)), props.ExpiresUtc); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.ExpiresUtc); + } + + [Fact] + public void AllowRefresh_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.AllowRefresh); + + props.AllowRefresh = true; + Assert.True(props.AllowRefresh); + Assert.Equal("True", props.Items.First().Value); + + props.AllowRefresh = false; + Assert.False(props.AllowRefresh); + Assert.Equal("False", props.Items.First().Value); + + props.Items.Clear(); + Assert.Null(props.AllowRefresh); + } + } +}