From 13925be91e0336a37e782af05a96507bb676122d Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Fri, 31 Mar 2017 11:56:20 -0700 Subject: [PATCH] Initial Auth 2.0 checkin --- HttpAbstractions.sln | 45 ++++ .../AuthenticateContext.cs | 21 ++ .../AuthenticateResult.cs | 105 ++++++++++ .../AuthenticationHttpContextExtensions.cs | 158 ++++++++++++++ .../AuthenticationOptions.cs | 65 ++++++ .../AuthenticationProperties.cs | 197 ++++++++++++++++++ .../AuthenticationScheme.cs | 49 +++++ .../AuthenticationSchemeBuilder.cs | 38 ++++ .../AuthenticationTicket.cs | 56 +++++ .../AuthenticationToken.cs | 22 ++ .../BaseAuthenticationContext.cs | 41 ++++ .../BaseContext.cs | 49 +++++ .../ChallengeBehavior.cs | 15 ++ .../ChallengeContext.cs | 45 ++++ .../IAuthenticationFeature.cs | 23 ++ .../IAuthenticationHandler.cs | 50 +++++ .../IAuthenticationHandlerProvider.cs | 22 ++ .../IAuthenticationRequestHandler.cs | 21 ++ .../IAuthenticationSchemeProvider.cs | 70 +++++++ .../IAuthenticationService.cs | 52 +++++ .../IClaimsTransformation.cs | 21 ++ ...NetCore.Authentication.Abstractions.csproj | 24 +++ .../SignInContext.cs | 37 ++++ .../SignOutContext.cs | 23 ++ .../TokenExtensions.cs | 155 ++++++++++++++ ...ticationCoreServiceCollectionExtensions.cs | 56 +++++ .../AuthenticationFeature.cs | 23 ++ .../AuthenticationHandlerProvider.cs | 63 ++++++ .../AuthenticationSchemeProvider.cs | 170 +++++++++++++++ .../AuthenticationService.cs | 167 +++++++++++++++ ...soft.AspNetCore.Authentication.Core.csproj | 21 ++ .../NoopClaimsTransformation.cs | 24 +++ ...AspNetCore.Authentication.Core.Test.csproj | 19 ++ .../TokenExtensionTests.cs | 123 +++++++++++ 34 files changed, 2070 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateResult.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationOptions.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationScheme.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationSchemeBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationTicket.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/BaseAuthenticationContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/BaseContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeBehavior.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationFeature.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandler.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandlerProvider.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationRequestHandler.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationService.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/IClaimsTransformation.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/Microsoft.AspNetCore.Authentication.Abstractions.csproj create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/SignInContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/SignOutContext.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Abstractions/TokenExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/AuthenticationCoreServiceCollectionExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/AuthenticationFeature.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/AuthenticationHandlerProvider.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/Microsoft.AspNetCore.Authentication.Core.csproj create mode 100644 src/Microsoft.AspNetCore.Authentication.Core/NoopClaimsTransformation.cs create mode 100644 test/Microsoft.AspNetCore.Authentication.Core.Test/Microsoft.AspNetCore.Authentication.Core.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Authentication.Core.Test/TokenExtensionTests.cs diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index 9525343a98..6434e357d0 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -57,6 +57,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{ED7BCAC5 build\Key.snk = build\Key.snk EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Abstractions", "src\Microsoft.AspNetCore.Authentication.Abstractions\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "{3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Core", "src\Microsoft.AspNetCore.Authentication.Core\Microsoft.AspNetCore.Authentication.Core.csproj", "{73CA3145-91BD-4DA5-BC74-40008DE7EA98}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Core.Test", "test\Microsoft.AspNetCore.Authentication.Core.Test\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "{A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -223,6 +229,42 @@ Global {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|x86.ActiveCfg = Release|Any CPU {1D0764B4-1DEB-4232-A714-D4B7E846918A}.Release|x86.Build.0 = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Debug|x86.Build.0 = Debug|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|Any CPU.Build.0 = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|x86.ActiveCfg = Release|Any CPU + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852}.Release|x86.Build.0 = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|x86.ActiveCfg = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Debug|x86.Build.0 = Debug|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|Any CPU.Build.0 = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|x86.ActiveCfg = Release|Any CPU + {73CA3145-91BD-4DA5-BC74-40008DE7EA98}.Release|x86.Build.0 = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Debug|x86.Build.0 = Debug|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|x86.ActiveCfg = Release|Any CPU + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -244,5 +286,8 @@ Global {E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} {1D0764B4-1DEB-4232-A714-D4B7E846918A} = {982F09D8-621E-4872-BA7B-BBDEA47D1EFD} {ED7BCAC5-2796-44BD-9954-7C248263BC8B} = {C6C48D5F-B289-4150-A6FC-77A5C7064BCE} + {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852} = {A5A15F1C-885A-452A-A731-B0173DDBD913} + {73CA3145-91BD-4DA5-BC74-40008DE7EA98} = {A5A15F1C-885A-452A-A731-B0173DDBD913} + {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateContext.cs new file mode 100644 index 0000000000..814d7024e4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateContext.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base class used by methods. + /// + public class AuthenticateContext : BaseAuthenticationContext + { + /// + /// Constructor. + /// + /// The context. + /// The name of the authentication scheme. + public AuthenticateContext(HttpContext context, string authenticationScheme) : base(context, authenticationScheme, properties: null) + { } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateResult.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateResult.cs new file mode 100644 index 0000000000..bb9bbb9716 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticateResult.cs @@ -0,0 +1,105 @@ +// 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.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the result of an Authenticate call + /// + public class AuthenticateResult + { + private AuthenticateResult() { } + + /// + /// If a ticket was produced, authenticate was successful. + /// + public bool Succeeded => Ticket != null; + + /// + /// The authentication ticket. + /// + public AuthenticationTicket Ticket { get; private set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal => Ticket?.Principal; + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties => Ticket?.Properties; + + /// + /// Holds failure information from the authentication. + /// + public Exception Failure { get; private set; } + + /// + /// Indicates that stage of authentication was directly handled by user intervention and no + /// further processing should be attempted. + /// + public bool Handled { get; private set; } + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + public bool Nothing { get; private set; } + + /// + /// Indicates that authentication was successful. + /// + /// The ticket representing the authentication result. + /// The result. + public static AuthenticateResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new AuthenticateResult() { Ticket = ticket }; + } + + /// + /// Indicates that stage of authentication was directly handled by user intervention and no + /// further processing should be attempted. + /// + /// The result. + public static AuthenticateResult Handle() + { + return new AuthenticateResult() { Handled = true }; + } + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + /// The result. + public static AuthenticateResult None() + { + return new AuthenticateResult() { Nothing = true }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// The result. + public static AuthenticateResult Fail(Exception failure) + { + return new AuthenticateResult() { Failure = failure }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// The result. + public static AuthenticateResult Fail(string failureMessage) + { + return new AuthenticateResult() { Failure = new Exception(failureMessage) }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs new file mode 100644 index 0000000000..a1fd3756ef --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs @@ -0,0 +1,158 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Extension methods to expose Authentication on HttpContext. + /// + public static class AuthenticationHttpContextExtensions + { + /// + /// Extension method for authenticate using the scheme. + /// + /// The context. + /// The . + public static Task AuthenticateAsync(this HttpContext context) => + context.AuthenticateAsync(scheme: null); + + /// + /// Extension method for authenticate. + /// + /// The context. + /// The name of the authentication scheme. + /// The . + public static Task AuthenticateAsync(this HttpContext context, string scheme) => + context.RequestServices.GetRequiredService().AuthenticateAsync(context, scheme); + + /// + /// Extension method for Challenge. + /// + /// The context. + /// The name of the authentication scheme. + /// The result. + public static Task ChallengeAsync(this HttpContext context, string scheme) => + context.ChallengeAsync(scheme, properties: null); + + /// + /// Extension method for authenticate using the scheme. + /// + /// The context. + /// The task. + public static Task ChallengeAsync(this HttpContext context) => + context.ChallengeAsync(scheme: null, properties: null); + + /// + /// Extension method for Challenge. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.ChallengeAsync(scheme, properties: properties, behavior: ChallengeBehavior.Automatic); + + /// + /// Extension method for Challenge. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The behavior. + /// The task. + public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties, ChallengeBehavior behavior) => + context.RequestServices.GetRequiredService().ChallengeAsync(context, scheme, properties, behavior); + + /// + /// Extension method for Forbid. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task ForbidAsync(this HttpContext context, string scheme) => + context.ForbidAsync(scheme, properties: null); + + /// + /// Extension method for Forbid. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().ChallengeAsync(context, scheme, properties, ChallengeBehavior.Forbidden); + + /// + /// Extension method for SignIn. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal) => + context.SignInAsync(scheme, principal, properties: null); + + /// + /// Extension method for SignIn using the . + /// + /// The context. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) => + context.SignInAsync(scheme: null, principal: principal, properties: null); + + /// + /// Extension method for SignIn using the . + /// + /// The context. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.SignInAsync(scheme: null, principal: principal, properties: properties); + + /// + /// Extension method for SignIn. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().SignInAsync(context, scheme, principal, properties); + + /// + /// Extension method for SignOut. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task SignOutAsync(this HttpContext context, string scheme) => context.SignOutAsync(scheme, properties: null); + + /// + /// Extension method for SignOut. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// + public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => + context.RequestServices.GetRequiredService().SignOutAsync(context, scheme, properties); + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token. + public static Task GetTokenAsync(this HttpContext context, string scheme, string tokenName) => + context.RequestServices.GetRequiredService().GetTokenAsync(context, scheme, tokenName); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationOptions.cs new file mode 100644 index 0000000000..e7e5936e17 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationOptions.cs @@ -0,0 +1,65 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationOptions + { + private readonly IList _schemes = new List(); + + /// + /// Returns the schemes in the order they were added (important for request handling priority) + /// + public IEnumerable Schemes => _schemes; + + /// + /// Maps schemes by name. + /// + public IDictionary SchemeMap { get; } = new Dictionary(StringComparer.Ordinal); + + /// + /// Adds an . + /// + /// The name of the scheme being added. + /// Configures the scheme. + public void AddScheme(string name, Action configureBuilder) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (configureBuilder == null) + { + throw new ArgumentNullException(nameof(configureBuilder)); + } + if (SchemeMap.ContainsKey(name)) + { + throw new InvalidOperationException("Scheme already exists: " + name); + } + + var builder = new AuthenticationSchemeBuilder(name); + configureBuilder(builder); + _schemes.Add(builder); + SchemeMap[name] = builder; + } + + /// + /// Used by as the default scheme by . + /// + public string DefaultAuthenticationScheme { get; set; } + + /// + /// Used by as the default scheme by . + /// + public string DefaultSignInScheme { get; set; } + + /// + /// Used by as the default scheme by . + /// + public string DefaultChallengeScheme { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs new file mode 100644 index 0000000000..609b6fad58 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs @@ -0,0 +1,197 @@ +// 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; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Dictionary used to store state values about the authentication session. + /// + public class AuthenticationProperties + { + internal const string IssuedUtcKey = ".issued"; + internal const string ExpiresUtcKey = ".expires"; + internal const string IsPersistentKey = ".persistent"; + internal const string RedirectUriKey = ".redirect"; + internal const string RefreshKey = ".refresh"; + internal const string UtcDateTimeFormat = "r"; + + /// + /// Initializes a new instance of the class + /// + public AuthenticationProperties() + : this(items: null) + { + } + + /// + /// Initializes a new instance of the class + /// + /// + public AuthenticationProperties(IDictionary items) + { + Items = items ?? new Dictionary(StringComparer.Ordinal); + } + + /// + /// State values about the authentication session. + /// + public IDictionary Items { get; } + + /// + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// + public bool IsPersistent + { + get { return Items.ContainsKey(IsPersistentKey); } + set + { + if (Items.ContainsKey(IsPersistentKey)) + { + if (!value) + { + Items.Remove(IsPersistentKey); + } + } + else + { + if (value) + { + Items.Add(IsPersistentKey, string.Empty); + } + } + } + } + + /// + /// Gets or sets the full path or absolute URI to be used as an http redirect response value. + /// + public string RedirectUri + { + get + { + string value; + return Items.TryGetValue(RedirectUriKey, out value) ? value : null; + } + set + { + if (value != null) + { + Items[RedirectUriKey] = value; + } + else + { + if (Items.ContainsKey(RedirectUriKey)) + { + Items.Remove(RedirectUriKey); + } + } + } + } + + /// + /// Gets or sets the time at which the authentication ticket was issued. + /// + public DateTimeOffset? IssuedUtc + { + get + { + string value; + if (Items.TryGetValue(IssuedUtcKey, out value)) + { + DateTimeOffset dateTimeOffset; + if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset)) + { + return dateTimeOffset; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[IssuedUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + } + else + { + if (Items.ContainsKey(IssuedUtcKey)) + { + Items.Remove(IssuedUtcKey); + } + } + } + } + + /// + /// Gets or sets the time at which the authentication ticket expires. + /// + public DateTimeOffset? ExpiresUtc + { + get + { + string value; + if (Items.TryGetValue(ExpiresUtcKey, out value)) + { + DateTimeOffset dateTimeOffset; + if (DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset)) + { + return dateTimeOffset; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[ExpiresUtcKey] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + } + else + { + if (Items.ContainsKey(ExpiresUtcKey)) + { + Items.Remove(ExpiresUtcKey); + } + } + } + } + + /// + /// Gets or sets if refreshing the authentication session should be allowed. + /// + public bool? AllowRefresh + { + get + { + string value; + if (Items.TryGetValue(RefreshKey, out value)) + { + bool refresh; + if (bool.TryParse(value, out refresh)) + { + return refresh; + } + } + return null; + } + set + { + if (value.HasValue) + { + Items[RefreshKey] = value.Value.ToString(); + } + else + { + if (Items.ContainsKey(RefreshKey)) + { + Items.Remove(RefreshKey); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationScheme.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationScheme.cs new file mode 100644 index 0000000000..77bd9e6d35 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationScheme.cs @@ -0,0 +1,49 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// AuthenticationSchemes assign a name to a specific + /// handlerType. + /// + public class AuthenticationScheme + { + /// + /// Constructor. + /// + /// The name for the authentication scheme. + /// The type that handles this scheme. + public AuthenticationScheme(string name, Type handlerType) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + if (handlerType == null) + { + throw new ArgumentNullException(nameof(handlerType)); + } + if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType)) + { + throw new ArgumentException("handlerType must implement IAuthenticationSchemeHandler."); + } + + Name = name; + HandlerType = handlerType; + } + + /// + /// The name of the authentication scheme. + /// + public string Name { get; } + + /// + /// The type that handles this scheme. + /// + public Type HandlerType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationSchemeBuilder.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationSchemeBuilder.cs new file mode 100644 index 0000000000..e1ea0cbc4a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationSchemeBuilder.cs @@ -0,0 +1,38 @@ +// 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; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to build s. + /// + public class AuthenticationSchemeBuilder + { + /// + /// Constructor. + /// + /// The name of the scheme being built. + public AuthenticationSchemeBuilder(string name) + { + Name = name; + } + + /// + /// The name of the scheme being built. + /// + public string Name { get; } + + /// + /// The type responsible for this scheme. + /// + public Type HandlerType { get; set; } + + /// + /// Builds the instance. + /// + /// + public AuthenticationScheme Build() => new AuthenticationScheme(Name, HandlerType); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationTicket.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationTicket.cs new file mode 100644 index 0000000000..c31f15ec01 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationTicket.cs @@ -0,0 +1,56 @@ +// 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.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains user identity information as well as additional authentication state. + /// + public class AuthenticationTicket + { + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// additional properties that can be consumed by the user or runtime. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) + : this(principal, properties: null, authenticationScheme: authenticationScheme) + { } + + /// + /// Gets the authentication type. + /// + public string AuthenticationScheme { get; private set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal { get; private set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs new file mode 100644 index 0000000000..555da9e098 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs @@ -0,0 +1,22 @@ +// 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 +{ + /// + /// Name/Value representing an token. + /// + public class AuthenticationToken + { + /// + /// Name. + /// + public string Name { get; set; } + + /// + /// Value. + /// + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseAuthenticationContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseAuthenticationContext.cs new file mode 100644 index 0000000000..cfe5809c5a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseAuthenticationContext.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for authentication. + /// + public abstract class BaseAuthenticationContext : BaseContext + { + /// + /// Constructor. + /// + /// The context. + /// The name of the scheme. + /// The properties. + protected BaseAuthenticationContext(HttpContext context, string authenticationScheme, AuthenticationProperties properties) : base(context) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + + AuthenticationScheme = authenticationScheme; + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// The name of the scheme. + /// + public string AuthenticationScheme { get; } + + /// + /// Contains the extra meta-data arriving with the authentication. May be altered. + /// + public AuthenticationProperties Properties { get; protected set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseContext.cs new file mode 100644 index 0000000000..3d65f0dd75 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/BaseContext.cs @@ -0,0 +1,49 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base class used by other context classes. + /// + public abstract class BaseContext + { + /// + /// Constructor. + /// + /// The request context. + protected BaseContext(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + HttpContext = context; + } + + /// + /// The context. + /// + public HttpContext HttpContext { get; } + + /// + /// The request. + /// + public HttpRequest Request + { + get { return HttpContext.Request; } + } + + /// + /// The response. + /// + public HttpResponse Response + { + get { return HttpContext.Response; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeBehavior.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeBehavior.cs new file mode 100644 index 0000000000..1506021dd4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeBehavior.cs @@ -0,0 +1,15 @@ +// 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 +{ + /// + /// Controls how challenge will behave (i.e. 401 vs 403). + /// + public enum ChallengeBehavior + { + Automatic, + Unauthorized, + Forbidden + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeContext.cs new file mode 100644 index 0000000000..ee2392eb04 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/ChallengeContext.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Context used for challenges. + /// + public class ChallengeContext : BaseAuthenticationContext + { + /// + /// Constructor. + /// + /// The context. + /// The name of the scheme. + public ChallengeContext(HttpContext httpContext, string authenticationScheme) + : this(httpContext, authenticationScheme, properties: null, behavior: ChallengeBehavior.Automatic) + { } + + /// + /// Constructor + /// + /// The context. + /// The name of the scheme. + /// The properties. + /// The challenge behavior. + public ChallengeContext(HttpContext httpContext, string authenticationScheme, AuthenticationProperties properties, ChallengeBehavior behavior) + : base(httpContext, authenticationScheme, properties) + { + if (string.IsNullOrEmpty(authenticationScheme)) + { + throw new ArgumentException(nameof(authenticationScheme)); + } + Behavior = behavior; + } + + /// + /// The challenge behavior. + /// + public ChallengeBehavior Behavior { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationFeature.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationFeature.cs new file mode 100644 index 0000000000..43e5a13b49 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationFeature.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// + public interface IAuthenticationFeature + { + /// + /// The original path base. + /// + PathString OriginalPathBase { get; set; } + + /// + /// The original path. + /// + PathString OriginalPath { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandler.cs new file mode 100644 index 0000000000..7a805f7af4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandler.cs @@ -0,0 +1,50 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Created per request to handle authentication for to a particular scheme. + /// + public interface IAuthenticationHandler + { + /// + /// The handler should initialize anything it needs from the request and scheme here. + /// + /// The scheme. + /// The context. + /// + Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); + + /// + /// Authentication behavior. + /// + /// The context. + /// The result. + Task AuthenticateAsync(AuthenticateContext context); + + /// + /// Challenge behavior. + /// + /// The context. + /// A task. + Task ChallengeAsync(ChallengeContext context); + + /// + /// Handle sign in. + /// + /// The context. + /// A task. + Task SignInAsync(SignInContext context); + + /// + /// Signout behavior. + /// + /// The context. + /// A task. + Task SignOutAsync(SignOutContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandlerProvider.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandlerProvider.cs new file mode 100644 index 0000000000..0507f51d61 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationHandlerProvider.cs @@ -0,0 +1,22 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request. + /// + public interface IAuthenticationHandlerProvider + { + /// + /// Returns the handler instance that will be used. + /// + /// The context. + /// The name of the authentication scheme being handled. + /// The handler instance. + Task GetHandlerAsync(HttpContext context, string authenticationScheme); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationRequestHandler.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationRequestHandler.cs new file mode 100644 index 0000000000..fffe08f427 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationRequestHandler.cs @@ -0,0 +1,21 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to determine if a handler wants to participate in request processing. + /// + public interface IAuthenticationRequestHandler : IAuthenticationHandler + { + + /// + /// Returns true if request processing should stop. + /// + /// + Task HandleRequestAsync(); + } + +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs new file mode 100644 index 0000000000..4b36abdac7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs @@ -0,0 +1,70 @@ +// 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.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Responsible for managing what authenticationSchemes are supported. + /// + public interface IAuthenticationSchemeProvider + { + /// + /// Returns all currently registered s. + /// + /// All currently registered s. + Task> GetAllSchemesAsync(); + + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + Task GetSchemeAsync(string name); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + Task GetDefaultAuthenticateSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + Task GetDefaultChallengeSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + Task GetDefaultSignInSchemeAsync(); + + /// + /// Registers a scheme for use by . + /// + /// The scheme. + void AddScheme(AuthenticationScheme scheme); + + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + void RemoveScheme(string name); + + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + Task> GetRequestHandlerSchemesAsync(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationService.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationService.cs new file mode 100644 index 0000000000..ec54325ea4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationService.cs @@ -0,0 +1,52 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to provide authentication. + /// + public interface IAuthenticationService + { + /// + /// Authenticate for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The result. + Task AuthenticateAsync(HttpContext context, string scheme); + + /// + /// Challenge the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// The . + /// A task. + Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties, ChallengeBehavior behavior); + + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties); + + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/IClaimsTransformation.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/IClaimsTransformation.cs new file mode 100644 index 0000000000..3aed710aa9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/IClaimsTransformation.cs @@ -0,0 +1,21 @@ +// 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.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used by the for claims transformation. + /// + public interface IClaimsTransformation + { + /// + /// Provides a central transformation point to change the specified principal. + /// + /// The to transform. + /// The transformed principal. + Task TransformAsync(ClaimsPrincipal principal); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/Microsoft.AspNetCore.Authentication.Abstractions.csproj b/src/Microsoft.AspNetCore.Authentication.Abstractions/Microsoft.AspNetCore.Authentication.Abstractions.csproj new file mode 100644 index 0000000000..aa2fae7fc3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/Microsoft.AspNetCore.Authentication.Abstractions.csproj @@ -0,0 +1,24 @@ + + + + ASP.NET Core common types used by the various authentication components. + netstandard1.3;net46 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/SignInContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/SignInContext.cs new file mode 100644 index 0000000000..e89b663a7d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/SignInContext.cs @@ -0,0 +1,37 @@ +// 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.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Context used for sign out. + /// + public class SignInContext : BaseAuthenticationContext + { + /// + /// Constructor. + /// + /// The context. + /// The name of the authentication scheme. + /// The user to sign in. + /// The properties. + public SignInContext(HttpContext context, string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, authenticationScheme, properties) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + Principal = principal; + } + + /// + /// The user to sign in. + /// + public ClaimsPrincipal Principal { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/SignOutContext.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/SignOutContext.cs new file mode 100644 index 0000000000..307a3af875 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/SignOutContext.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Context used to sign out. + /// + public class SignOutContext : BaseAuthenticationContext + { + /// + /// Constructor. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + public SignOutContext(HttpContext context, string authenticationScheme, AuthenticationProperties properties) + : base(context, authenticationScheme, properties) + { } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/TokenExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/TokenExtensions.cs new file mode 100644 index 0000000000..24f66d9043 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/TokenExtensions.cs @@ -0,0 +1,155 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Extension methods for storing authentication tokens in . + /// + public static class AuthenticationTokenExtensions + { + private static string TokenNamesKey = ".TokenNames"; + private static string TokenKeyPrefix = ".Token."; + + /// + /// Stores a set of authentication tokens, after removing any old tokens. + /// + /// The properties. + /// The tokens to store. + public static void StoreTokens(this AuthenticationProperties properties, IEnumerable tokens) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + + // Clear old tokens first + var oldTokens = properties.GetTokens(); + foreach (var t in oldTokens) + { + properties.Items.Remove(TokenKeyPrefix + t.Name); + } + properties.Items.Remove(TokenNamesKey); + + var tokenNames = new List(); + foreach (var token in tokens) + { + // REVIEW: should probably check that there are no ; in the token name and throw or encode + tokenNames.Add(token.Name); + properties.Items[TokenKeyPrefix+token.Name] = token.Value; + } + if (tokenNames.Count > 0) + { + properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray()); + } + } + + /// + /// Returns the value of a token. + /// + /// The properties. + /// The token name. + /// The token value. + public static string GetTokenValue(this AuthenticationProperties properties, string tokenName) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var tokenKey = TokenKeyPrefix + tokenName; + return properties.Items.ContainsKey(tokenKey) + ? properties.Items[tokenKey] + : null; + } + + public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var tokenKey = TokenKeyPrefix + tokenName; + if (!properties.Items.ContainsKey(tokenKey)) + { + return false; + } + properties.Items[tokenKey] = tokenValue; + return true; + } + + /// + /// Returns all of the AuthenticationTokens contained in the properties. + /// + /// The properties. + /// The authentication toekns. + public static IEnumerable GetTokens(this AuthenticationProperties properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + var tokens = new List(); + if (properties.Items.ContainsKey(TokenNamesKey)) + { + var tokenNames = properties.Items[TokenNamesKey].Split(';'); + foreach (var name in tokenNames) + { + var token = properties.GetTokenValue(name); + if (token != null) + { + tokens.Add(new AuthenticationToken { Name = name, Value = token }); + } + } + } + + return tokens; + } + + /// + /// Extension method for getting the value of an authentication token. + /// + /// The . + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token. + public static async Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName) + { + if (auth == null) + { + throw new ArgumentNullException(nameof(auth)); + } + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var result = await auth.AuthenticateAsync(context, scheme); + return result?.Properties?.GetTokenValue(tokenName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationCoreServiceCollectionExtensions.cs new file mode 100644 index 0000000000..fdf85a9b45 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationCoreServiceCollectionExtensions.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up authentication services in an . + /// + public static class AuthenticationCoreServiceCollectionExtensions + { + /// + /// Add core authentication services needed for . + /// + /// The . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddScoped(); + services.TryAddSingleton(); // Can be replaced with scoped ones that use DbContext + services.TryAddScoped(); + services.TryAddSingleton(); + return services; + } + + /// + /// Add core authentication services needed for . + /// + /// The . + /// Used to configure the . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action configureOptions) { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.AddAuthenticationCore(); + services.Configure(configureOptions); + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationFeature.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationFeature.cs new file mode 100644 index 0000000000..3282cbf467 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationFeature.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// + public class AuthenticationFeature : IAuthenticationFeature + { + /// + /// The original path base. + /// + public PathString OriginalPathBase { get; set; } + + /// + /// The original path. + /// + public PathString OriginalPath { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationHandlerProvider.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationHandlerProvider.cs new file mode 100644 index 0000000000..c4921e5334 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationHandlerProvider.cs @@ -0,0 +1,63 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Implementation of . + /// + public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider + { + /// + /// Constructor. + /// + /// The . + public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) + { + Schemes = schemes; + } + + /// + /// The . + /// + public IAuthenticationSchemeProvider Schemes { get; } + + // handler instance cache, need to initialize once per request + private Dictionary _handlerMap = new Dictionary(StringComparer.Ordinal); + + /// + /// Returns the handler instance that will be used. + /// + /// The context. + /// The name of the authentication scheme being handled. + /// The handler instance. + public async Task GetHandlerAsync(HttpContext context, string authenticationScheme) + { + if (_handlerMap.ContainsKey(authenticationScheme)) + { + return _handlerMap[authenticationScheme]; + } + + var scheme = await Schemes.GetSchemeAsync(authenticationScheme); + if (scheme == null) + { + return null; + } + var handler = (context.RequestServices.GetService(scheme.HandlerType) ?? + ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType)) + as IAuthenticationHandler; + if (handler != null) + { + await handler.InitializeAsync(scheme, context); + _handlerMap[authenticationScheme] = handler; + } + return handler; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs new file mode 100644 index 0000000000..fe347d6ee9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs @@ -0,0 +1,170 @@ +// 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.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Implements . + /// + public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider + { + /// + /// Constructor. + /// + /// The options. + public AuthenticationSchemeProvider(IOptions options) + { + _options = options.Value; + + foreach (var builder in _options.Schemes) + { + var scheme = builder.Build(); + AddScheme(scheme); + } + } + + private readonly AuthenticationOptions _options; + private readonly object _lock = new object(); + + private IDictionary _map = new Dictionary(StringComparer.Ordinal); + private List _requestHandlers = new List(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + public Task GetDefaultAuthenticateSchemeAsync() + { + if (_options.DefaultAuthenticationScheme != null) + { + return GetSchemeAsync(_options.DefaultAuthenticationScheme); + } + if (_map.Count == 1) + { + return Task.FromResult(_map.Values.First()); + } + return Task.FromResult(null); + } + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + public Task GetDefaultChallengeSchemeAsync() + { + if (_options.DefaultChallengeScheme != null) + { + return GetSchemeAsync(_options.DefaultChallengeScheme); + } + if (_map.Count == 1) + { + return Task.FromResult(_map.Values.First()); + } + return Task.FromResult(null); + } + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, if only a single scheme exists, that will be used, if more than one exists, null will be returned. + /// + /// The scheme that will be used by default for . + public Task GetDefaultSignInSchemeAsync() + { + if (_options.DefaultSignInScheme != null) + { + return GetSchemeAsync(_options.DefaultSignInScheme); + } + if (_map.Count == 1) + { + return Task.FromResult(_map.Values.First()); + } + return Task.FromResult(null); + } + + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + public Task GetSchemeAsync(string name) + { + if (_map.ContainsKey(name)) + { + return Task.FromResult(_map[name]); + } + return Task.FromResult(null); + } + + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + public Task> GetRequestHandlerSchemesAsync() + { + return Task.FromResult>(_requestHandlers); + } + + /// + /// Registers a scheme for use by . + /// + /// The scheme. + public void AddScheme(AuthenticationScheme scheme) + { + if (_map.ContainsKey(scheme.Name)) + { + throw new InvalidOperationException("Scheme already exists: " + scheme.Name); + } + lock (_lock) + { + if (_map.ContainsKey(scheme.Name)) + { + throw new InvalidOperationException("Scheme already exists: " + scheme.Name); + } + if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) + { + _requestHandlers.Add(scheme); + } + _map[scheme.Name] = scheme; + } + } + + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + public void RemoveScheme(string name) + { + if (!_map.ContainsKey(name)) + { + return; + } + lock (_lock) + { + if (_map.ContainsKey(name)) + { + var scheme = _map[name]; + _requestHandlers.Remove(_requestHandlers.Where(s => s.Name == name).FirstOrDefault()); + _map.Remove(name); + } + } + } + + public Task> GetAllSchemesAsync() + { + return Task.FromResult>(_map.Values); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs new file mode 100644 index 0000000000..9b8837f22e --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs @@ -0,0 +1,167 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Implements . + /// + public class AuthenticationService : IAuthenticationService + { + /// + /// Constructor. + /// + /// The . + /// The . + /// The The . + public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) + { + Schemes = schemes; + Handlers = handlers; + Transform = transform; + } + + /// + /// Used to lookup AuthenticationSchemes. + /// + public IAuthenticationSchemeProvider Schemes { get; } + + /// + /// Used to resolve IAuthenticationHandler instances. + /// + public IAuthenticationHandlerProvider Handlers { get; } + + /// + /// Used for claims transformation. + /// + public IClaimsTransformation Transform { get; } + + /// + /// Authenticate for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The result. + public virtual async Task AuthenticateAsync(HttpContext context, string scheme) + { + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}"); + } + + var authContext = new AuthenticateContext(context, scheme); + var result = await handler.AuthenticateAsync(authContext); + if (result.Succeeded) + { + var transformed = await Transform.TransformAsync(result.Principal); + return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme)); + } + return result; + } + + /// + /// Challenge the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// The . + /// A task. + public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties, ChallengeBehavior behavior) + { + if (scheme == null) + { + var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); + scheme = defaultChallengeScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}"); + } + + var challengeContext = new ChallengeContext(context, scheme, properties, behavior); + await handler.ChallengeAsync(challengeContext); + } + + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) + { + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found."); + } + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}"); + } + + var signInContext = new SignInContext(context, scheme, principal, properties); + await handler.SignInAsync(signInContext); + } + + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + if (string.IsNullOrEmpty(scheme)) + { + throw new ArgumentException(nameof(scheme)); + } + + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}"); + } + + var signOutContext = new SignOutContext(context, scheme, properties); + await handler.SignOutAsync(signOutContext); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Core/Microsoft.AspNetCore.Authentication.Core.csproj b/src/Microsoft.AspNetCore.Authentication.Core/Microsoft.AspNetCore.Authentication.Core.csproj new file mode 100644 index 0000000000..61628336f1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/Microsoft.AspNetCore.Authentication.Core.csproj @@ -0,0 +1,21 @@ + + + + + + ASP.NET Core common types used by the various authentication middleware components. + netstandard1.3;net46 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + false + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Core/NoopClaimsTransformation.cs b/src/Microsoft.AspNetCore.Authentication.Core/NoopClaimsTransformation.cs new file mode 100644 index 0000000000..83c488fe42 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Core/NoopClaimsTransformation.cs @@ -0,0 +1,24 @@ +// 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.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Default claims transformation is a no-op. + /// + public class NoopClaimsTransformation : IClaimsTransformation + { + /// + /// Returns the principal unchanged. + /// + /// The user. + /// The principal unchanged. + public virtual Task TransformAsync(ClaimsPrincipal principal) + { + return Task.FromResult(principal); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Core.Test/Microsoft.AspNetCore.Authentication.Core.Test.csproj b/test/Microsoft.AspNetCore.Authentication.Core.Test/Microsoft.AspNetCore.Authentication.Core.Test.csproj new file mode 100644 index 0000000000..925f5aa079 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Core.Test/Microsoft.AspNetCore.Authentication.Core.Test.csproj @@ -0,0 +1,19 @@ + + + + + netcoreapp2.0;net46 + netcoreapp2.0 + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Authentication.Core.Test/TokenExtensionTests.cs b/test/Microsoft.AspNetCore.Authentication.Core.Test/TokenExtensionTests.cs new file mode 100644 index 0000000000..3e3eb9c52b --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Core.Test/TokenExtensionTests.cs @@ -0,0 +1,123 @@ +// 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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class TokenExtensionTests + { + [Fact] + public void CanStoreMultipleTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.Equal("1", props.GetTokenValue("One")); + Assert.Equal("2", props.GetTokenValue("Two")); + Assert.Equal("3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void SubsequentStoreTokenDeletesPreviousTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + + props.StoreTokens(tokens); + + props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); + + Assert.Equal("0", props.GetTokenValue("Zero")); + Assert.Equal(null, props.GetTokenValue("One")); + Assert.Equal(null, props.GetTokenValue("Two")); + Assert.Equal(null, props.GetTokenValue("Three")); + Assert.Equal(1, props.GetTokens().Count()); + } + + [Fact] + public void CanUpdateTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + tok1.Value = ".1"; + tok2.Value = ".2"; + tok3.Value = ".3"; + props.StoreTokens(tokens); + + Assert.Equal(".1", props.GetTokenValue("One")); + Assert.Equal(".2", props.GetTokenValue("Two")); + Assert.Equal(".3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void CanUpdateTokenValues() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.True(props.UpdateTokenValue("One", ".11")); + Assert.True(props.UpdateTokenValue("Two", ".22")); + Assert.True(props.UpdateTokenValue("Three", ".33")); + + Assert.Equal(".11", props.GetTokenValue("One")); + Assert.Equal(".22", props.GetTokenValue("Two")); + Assert.Equal(".33", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } + + [Fact] + public void UpdateTokenValueReturnsFalseForUnknownToken() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.False(props.UpdateTokenValue("ONE", ".11")); + Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); + + Assert.Null(props.GetTokenValue("ONE")); + Assert.Null(props.GetTokenValue("Jigglypuff")); + Assert.Equal(3, props.GetTokens().Count()); + + } + } +}