From ace166fa31af61a6c9924873e6bdcb45e7785d5d Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 3 Mar 2016 12:50:46 -0800 Subject: [PATCH] Save tokens in auth properties instead of claims --- samples/SocialSample/Startup.cs | 66 ++++---- .../JwtBearerHandler.cs | 8 + .../JwtBearerOptions.cs | 6 + .../OAuthHandler.cs | 23 +-- .../OpenIdConnectHandler.cs | 27 ++-- .../TwitterHandler.cs | 8 +- .../AuthenticationToken.cs | 12 ++ .../RemoteAuthenticationOptions.cs | 16 +- .../TokenExtensions.cs | 115 ++++++++++++++ .../Google/GoogleMiddlewareTests.cs | 15 ++ .../TestExtensions.cs | 19 +++ .../TokenExtensionTests.cs | 144 ++++++++++++++++++ .../Transaction.cs | 12 ++ 13 files changed, 404 insertions(+), 67 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs create mode 100644 src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index ffbaf8dc59..4ab304c32e 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -5,6 +5,7 @@ using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; @@ -71,13 +72,13 @@ namespace CookieSample // You must first create an app with facebook and add it's ID and Secret to your config.json or user-secrets. // https://developers.facebook.com/apps/ - app.UseFacebookAuthentication(new FacebookOptions - { - AppId = Configuration["facebook:appid"], - AppSecret = Configuration["facebook:appsecret"], - Scope = { "email" }, - Fields = { "name", "email" } - }); + //app.UseFacebookAuthentication(new FacebookOptions + //{ + // AppId = Configuration["facebook:appid"], + // AppSecret = Configuration["facebook:appsecret"], + // Scope = { "email" }, + // Fields = { "name", "email" } + //}); // See config.json app.UseOAuthAuthentication(new OAuthOptions @@ -90,7 +91,7 @@ namespace CookieSample AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint, TokenEndpoint = GoogleDefaults.TokenEndpoint, Scope = { "openid", "profile", "email" }, - SaveTokensAsClaims = true + SaveTokens = true }); // See config.json @@ -146,27 +147,27 @@ namespace CookieSample The sample app can then be run via: dnx web */ - app.UseOAuthAuthentication(new OAuthOptions - { - AuthenticationScheme = "Microsoft-AccessToken", - DisplayName = "MicrosoftAccount-AccessToken - Requires project changes", - ClientId = Configuration["msa:clientid"], - ClientSecret = Configuration["msa:clientsecret"], - CallbackPath = new PathString("/signin-microsoft-token"), - AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint, - TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint, - Scope = { "wl.basic" }, - SaveTokensAsClaims = true - }); + //app.UseOAuthAuthentication(new OAuthOptions + //{ + // AuthenticationScheme = "Microsoft-AccessToken", + // DisplayName = "MicrosoftAccount-AccessToken - Requires project changes", + // ClientId = Configuration["msa:clientid"], + // ClientSecret = Configuration["msa:clientsecret"], + // CallbackPath = new PathString("/signin-microsoft-token"), + // AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint, + // TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint, + // Scope = { "wl.basic" }, + // SaveTokens = true + //}); - //// You must first create an app with live.com and add it's ID and Secret to your config.json or user-secrets. - app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions - { - DisplayName = "MicrosoftAccount - Requires project changes", - ClientId = Configuration["msa:clientid"], - ClientSecret = Configuration["msa:clientsecret"], - Scope = { "wl.emails" } - }); + ////// You must first create an app with live.com and add it's ID and Secret to your config.json or user-secrets. + //app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions + //{ + // DisplayName = "MicrosoftAccount - Requires project changes", + // ClientId = Configuration["msa:clientid"], + // ClientSecret = Configuration["msa:clientsecret"], + // Scope = { "wl.emails" } + //}); // See config.json // https://github.com/settings/applications/ @@ -179,7 +180,7 @@ namespace CookieSample CallbackPath = new PathString("/signin-github-token"), AuthorizationEndpoint = "https://github.com/login/oauth/authorize", TokenEndpoint = "https://github.com/login/oauth/access_token", - SaveTokensAsClaims = true + SaveTokens = true }); // See config.json @@ -318,6 +319,13 @@ namespace CookieSample { await context.Response.WriteAsync(claim.Type + ": " + claim.Value + "
"); } + + await context.Response.WriteAsync("Tokens:
"); + + await context.Response.WriteAsync("Access Token: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "access_token") + "
"); + await context.Response.WriteAsync("Refresh Token: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "refresh_token") + "
"); + await context.Response.WriteAsync("Token Type: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "token_type") + "
"); + await context.Response.WriteAsync("expires_at: " + await AuthenticationToken.GetTokenAsync(context, CookieAuthenticationDefaults.AuthenticationScheme, "expires_at") + "
"); await context.Response.WriteAsync("Logout"); await context.Response.WriteAsync(""); }); diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs index 00ecae0fb2..613dfbc152 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs @@ -153,6 +153,14 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer return AuthenticateResult.Skip(); } + if (Options.SaveToken) + { + ticket.Properties.StoreTokens(new[] + { + new AuthenticationToken { Name = "access_token", Value = token } + }); + } + return AuthenticateResult.Success(ticket); } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs index 0a08ff3e44..a4b5ef2ae2 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs @@ -110,5 +110,11 @@ namespace Microsoft.AspNetCore.Builder /// Contains the types and definitions required for validating a token. /// if 'value' is null. public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); + + /// + /// Defines whether the bearer token should be stored in the + /// after a successful authorization. + /// + public bool SaveToken { get; set; } = true; } } diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs index 7a06bee702..ddd26d9f01 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs @@ -85,21 +85,19 @@ namespace Microsoft.AspNetCore.Authentication.OAuth var identity = new ClaimsIdentity(Options.ClaimsIssuer); - if (Options.SaveTokensAsClaims) + if (Options.SaveTokens) { - identity.AddClaim(new Claim("access_token", tokens.AccessToken, - ClaimValueTypes.String, Options.ClaimsIssuer)); + var authTokens = new List(); + authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken }); if (!string.IsNullOrEmpty(tokens.RefreshToken)) { - identity.AddClaim(new Claim("refresh_token", tokens.RefreshToken, - ClaimValueTypes.String, Options.ClaimsIssuer)); + authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken }); } if (!string.IsNullOrEmpty(tokens.TokenType)) { - identity.AddClaim(new Claim("token_type", tokens.TokenType, - ClaimValueTypes.String, Options.ClaimsIssuer)); + authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType }); } if (!string.IsNullOrEmpty(tokens.ExpiresIn)) @@ -107,13 +105,18 @@ namespace Microsoft.AspNetCore.Authentication.OAuth int value; if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) { - var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx - identity.AddClaim(new Claim("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture), - ClaimValueTypes.DateTime, Options.ClaimsIssuer)); + var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); + authTokens.Add(new AuthenticationToken + { + Name = "expires_at", + Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) + }); } } + + properties.StoreTokens(authTokens); } return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, tokens)); diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index 4dc4a32b85..3c7f044386 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -105,9 +105,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect message.PostLogoutRedirectUri = logoutRedirectUri; } - var principal = await Context.Authentication.AuthenticateAsync(Options.SignInScheme); - message.IdTokenHint = principal?.FindFirst(OpenIdConnectParameterNames.IdToken)?.Value; - + message.IdTokenHint = await Context.Authentication.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var redirectContext = new RedirectContext(Context, Options, properties) { ProtocolMessage = message @@ -513,9 +511,9 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect tokenEndpointResponse = authenticationValidatedContext.TokenEndpointResponse; ticket = authenticationValidatedContext.Ticket; - if (Options.SaveTokensAsClaims) + if (Options.SaveTokens) { - SaveTokens(ticket.Principal, tokenEndpointResponse ?? authorizationResponse, jwt.Issuer); + SaveTokens(ticket.Properties, tokenEndpointResponse ?? authorizationResponse); } if (Options.GetClaimsFromUserInfoEndpoint) @@ -693,32 +691,28 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// /// The principal in which tokens are saved. /// The OpenID Connect response. - private void SaveTokens(ClaimsPrincipal principal, OpenIdConnectMessage message, string issuer) + private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message) { - var identity = (ClaimsIdentity)principal.Identity; + var tokens = new List(); if (!string.IsNullOrEmpty(message.AccessToken)) { - identity.AddClaim(new Claim(OpenIdConnectParameterNames.AccessToken, message.AccessToken, - ClaimValueTypes.String, issuer)); + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken }); } if (!string.IsNullOrEmpty(message.IdToken)) { - identity.AddClaim(new Claim(OpenIdConnectParameterNames.IdToken, message.IdToken, - ClaimValueTypes.String, issuer)); + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken }); } if (!string.IsNullOrEmpty(message.RefreshToken)) { - identity.AddClaim(new Claim(OpenIdConnectParameterNames.RefreshToken, message.RefreshToken, - ClaimValueTypes.String, issuer)); + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken }); } if (!string.IsNullOrEmpty(message.TokenType)) { - identity.AddClaim(new Claim(OpenIdConnectParameterNames.TokenType, message.TokenType, - ClaimValueTypes.String, issuer)); + tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType }); } if (!string.IsNullOrEmpty(message.ExpiresIn)) @@ -729,8 +723,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx - identity.AddClaim(new Claim("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture), - ClaimValueTypes.DateTime, issuer)); + tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs index c39cce1210..d856a9b845 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs @@ -88,10 +88,12 @@ namespace Microsoft.AspNetCore.Authentication.Twitter }, Options.ClaimsIssuer); - if (Options.SaveTokensAsClaims) + if (Options.SaveTokens) { - identity.AddClaim(new Claim("access_token", accessToken.Token, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("access_token_secret", accessToken.TokenSecret, ClaimValueTypes.String, Options.ClaimsIssuer)); + properties.StoreTokens(new [] { + new AuthenticationToken { Name = "access_token", Value = accessToken.Token }, + new AuthenticationToken { Name = "access_token_secret", Value = accessToken.TokenSecret } + }); } return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, accessToken)); diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs new file mode 100644 index 0000000000..6503f0bb85 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs @@ -0,0 +1,12 @@ +// 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 +{ + public class AuthenticationToken + { + public string Name { get; set; } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs index 43e011c40e..d925be8ec4 100644 --- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs @@ -51,19 +51,19 @@ namespace Microsoft.AspNetCore.Builder set { Description.DisplayName = value; } } - /// - /// Defines whether access and refresh tokens should be stored in the - /// after a successful authorization with the remote provider. - /// This property is set to false by default to reduce - /// the size of the final authentication cookie. - /// - public bool SaveTokensAsClaims { get; set; } - /// /// Gets or sets the time limit for completing the authentication flow (15 minutes by default). /// public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15); public IRemoteAuthenticationEvents Events = new RemoteAuthenticationEvents(); + + /// + /// Defines whether access and refresh tokens should be stored in the + /// after a successful authorization. + /// This property is set to false by default to reduce + /// the size of the final authentication cookie. + /// + public bool SaveTokens { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs b/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs new file mode 100644 index 0000000000..8065139baf --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs @@ -0,0 +1,115 @@ +// 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.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AuthenticationTokenExtensions + { + private static string TokenNamesKey = ".TokenNames"; + private static string TokenKeyPrefix = ".Token."; + + 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()); + } + } + + 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 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; + } + + public static Task GetTokenAsync(this AuthenticationManager manager, string tokenName) + { + return manager.GetTokenAsync(AuthenticationManager.AutomaticScheme, tokenName); + } + + public static async Task GetTokenAsync(this AuthenticationManager manager, string signInScheme, string tokenName) + { + if (manager == null) + { + throw new ArgumentNullException(nameof(manager)); + } + if (signInScheme == null) + { + throw new ArgumentNullException(nameof(signInScheme)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var authContext = new AuthenticateContext(signInScheme); + await manager.AuthenticateAsync(authContext); + return new AuthenticationProperties(authContext.Properties).GetTokenValue(tokenName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs index 6a07808127..6a3442e826 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -292,6 +292,7 @@ namespace Microsoft.AspNetCore.Authentication.Google { ClientId = "Test Id", ClientSecret = "Test Secret", + SaveTokens = true, StateDataFormat = stateFormat, ClaimsIssuer = claimsIssuer, BackchannelHttpHandler = new TestHttpMessageHandler @@ -334,6 +335,7 @@ namespace Microsoft.AspNetCore.Authentication.Google } } }); + var properties = new AuthenticationProperties(); var correlationKey = ".xsrf"; var correlationValue = "TestCorrelationId"; @@ -361,6 +363,12 @@ namespace Microsoft.AspNetCore.Authentication.Google // Ensure claims transformation Assert.Equal("yup", transaction.FindClaimValue("xform")); + + transaction = await server.SendAsync("https://example.com/tokens", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token")); + Assert.Equal("Bearer", transaction.FindTokenValue("token_type")); + Assert.NotNull(transaction.FindTokenValue("expires_at")); } // REVIEW: Fix this once we revisit error handling to not blow up @@ -781,6 +789,13 @@ namespace Microsoft.AspNetCore.Authentication.Google { await context.Authentication.ChallengeAsync("Google"); } + else if (req.Path == new PathString("/tokens")) + { + var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme); + await context.Authentication.AuthenticateAsync(authContext); + var tokens = AuthenticationToken.GetTokens(new AuthenticationProperties(authContext.Properties)); + res.Describe(tokens); + } else if (req.Path == new PathString("/me")) { res.Describe(context.User); diff --git a/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs b/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs index 2e9001dd8c..87d6d95a2c 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs @@ -1,6 +1,7 @@ // 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.IO; using System.Linq; using System.Net.Http; @@ -63,5 +64,23 @@ namespace Microsoft.AspNetCore.Authentication var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); res.Body.Write(xmlBytes, 0, xmlBytes.Length); } + + public static void Describe(this HttpResponse res, IEnumerable tokens) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (tokens != null) + { + foreach (var token in tokens) + { + xml.Add(new XElement("token", new XAttribute("name", token.Name), + new XAttribute("value", token.Value))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + res.Body.Write(xmlBytes, 0, xmlBytes.Length); + } + } } diff --git a/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs new file mode 100644 index 0000000000..ef030d1154 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs @@ -0,0 +1,144 @@ +// 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.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Http.Features.Authentication.Internal; +using Microsoft.AspNetCore.Http.Internal; +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()); + } + + public class TestAuthHandler : IAuthenticationHandler + { + private readonly AuthenticationProperties _props; + public TestAuthHandler(AuthenticationProperties props) + { + _props = props; + } + + public Task AuthenticateAsync(AuthenticateContext context) + { + context.Authenticated(new ClaimsPrincipal(), _props.Items, new Dictionary()); + return Task.FromResult(0); + } + + public Task ChallengeAsync(ChallengeContext context) + { + throw new NotImplementedException(); + } + + public void GetDescriptions(DescribeSchemesContext context) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(SignInContext context) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(SignOutContext context) + { + throw new NotImplementedException(); + } + } + + [Fact] + public async Task CanGetTokenFromContext() + { + 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); + + var context = new DefaultHttpContext(); + var handler = new TestAuthHandler(props); + context.Features.Set(new HttpAuthenticationFeature() { Handler = handler }); + + Assert.Equal("1", await context.Authentication.GetTokenAsync("One")); + Assert.Equal("2", await context.Authentication.GetTokenAsync("Two")); + Assert.Equal("3", await context.Authentication.GetTokenAsync("Three")); + } + + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs b/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs index 63f8af1bb2..f7128a6f11 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs @@ -46,5 +46,17 @@ namespace Microsoft.AspNetCore.Authentication } return claim.Attribute("value").Value; } + + public string FindTokenValue(string name) + { + var claim = ResponseElement.Elements("token") + .SingleOrDefault(elt => elt.Attribute("name").Value == name); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } }