Merge branch 'release/2.1' into dev

This commit is contained in:
Chris Ross (ASP.NET) 2018-03-23 14:56:48 -07:00
commit 8c797639e5
15 changed files with 982 additions and 94 deletions

View File

@ -225,11 +225,28 @@ namespace OpenIdConnectSample
return;
}
if (context.Request.Path.Equals("/login-challenge"))
{
// Challenge the user authentication, and force a login prompt by overwriting the
// "prompt". This could be used for example to require the user to re-enter their
// credentials at the authentication provider, to add an extra confirmation layer.
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties()
{
Prompt = "login",
// it is also possible to specify different scopes, e.g.
// Scope = new string[] { "openid", "profile", "other" }
});
return;
}
await WriteHtmlAsync(response, async res =>
{
await res.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(user.Identity.Name)}</h1>");
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/refresh\">Refresh tokens</a>");
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/restricted\">Restricted</a>");
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/login-challenge\">Login challenge</a>");
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remote</a>");

View File

@ -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.Globalization;
using System.Net.Http;
using System.Security.Claims;
@ -65,12 +66,15 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
}
}
protected override string FormatScope()
protected override string FormatScope(IEnumerable<string> scopes)
{
// Facebook deviates from the OAuth spec here. They require comma separated instead of space separated.
// https://developers.facebook.com/docs/reference/dialogs/oauth
// http://tools.ietf.org/html/rfc6749#section-3.3
return string.Join(",", Options.Scope);
return string.Join(",", scopes);
}
protected override string FormatScope()
=> base.FormatScope();
}
}

View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.OAuth;
namespace Microsoft.AspNetCore.Authentication.Google
{
public class GoogleChallengeProperties : OAuthChallengeProperties
{
/// <summary>
/// The parameter key for the "access_type" argument being used for a challenge request.
/// </summary>
public static readonly string AccessTypeKey = "access_type";
/// <summary>
/// The parameter key for the "approval_prompt" argument being used for a challenge request.
/// </summary>
public static readonly string ApprovalPromptKey = "approval_prompt";
/// <summary>
/// The parameter key for the "include_granted_scopes" argument being used for a challenge request.
/// </summary>
public static readonly string IncludeGrantedScopesKey = "include_granted_scopes";
/// <summary>
/// The parameter key for the "login_hint" argument being used for a challenge request.
/// </summary>
public static readonly string LoginHintKey = "login_hint";
/// <summary>
/// The parameter key for the "prompt" argument being used for a challenge request.
/// </summary>
public static readonly string PromptParameterKey = "prompt";
public GoogleChallengeProperties()
{ }
public GoogleChallengeProperties(IDictionary<string, string> items)
: base(items)
{ }
public GoogleChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
: base(items, parameters)
{ }
/// <summary>
/// The "access_type" parameter value being used for a challenge request.
/// </summary>
public string AccessType
{
get => GetParameter<string>(AccessTypeKey);
set => SetParameter(AccessTypeKey, value);
}
/// <summary>
/// The "approval_prompt" parameter value being used for a challenge request.
/// </summary>
public string ApprovalPrompt
{
get => GetParameter<string>(ApprovalPromptKey);
set => SetParameter(ApprovalPromptKey, value);
}
/// <summary>
/// The "include_granted_scopes" parameter value being used for a challenge request.
/// </summary>
public bool? IncludeGrantedScopes
{
get => GetParameter<bool?>(IncludeGrantedScopesKey);
set => SetParameter(IncludeGrantedScopesKey, value);
}
/// <summary>
/// The "login_hint" parameter value being used for a challenge request.
/// </summary>
public string LoginHint
{
get => GetParameter<string>(LoginHintKey);
set => SetParameter(LoginHintKey, value);
}
/// <summary>
/// The "prompt" parameter value being used for a challenge request.
/// </summary>
public string Prompt
{
get => GetParameter<string>(PromptParameterKey);
set => SetParameter(PromptParameterKey, value);
}
}
}

View File

@ -57,12 +57,12 @@ namespace Microsoft.AspNetCore.Authentication.Google
queryStrings.Add("client_id", Options.ClientId);
queryStrings.Add("redirect_uri", redirectUri);
AddQueryString(queryStrings, properties, "scope", FormatScope());
AddQueryString(queryStrings, properties, "access_type", Options.AccessType);
AddQueryString(queryStrings, properties, "approval_prompt");
AddQueryString(queryStrings, properties, "prompt");
AddQueryString(queryStrings, properties, "login_hint");
AddQueryString(queryStrings, properties, "include_granted_scopes");
AddQueryString(queryStrings, properties, GoogleChallengeProperties.ScopeKey, FormatScope, Options.Scope);
AddQueryString(queryStrings, properties, GoogleChallengeProperties.AccessTypeKey, Options.AccessType);
AddQueryString(queryStrings, properties, GoogleChallengeProperties.ApprovalPromptKey);
AddQueryString(queryStrings, properties, GoogleChallengeProperties.PromptParameterKey);
AddQueryString(queryStrings, properties, GoogleChallengeProperties.LoginHintKey);
AddQueryString(queryStrings, properties, GoogleChallengeProperties.IncludeGrantedScopesKey, v => v?.ToString().ToLower(), (bool?)null);
var state = Options.StateDataFormat.Protect(properties);
queryStrings.Add("state", state);
@ -71,29 +71,38 @@ namespace Microsoft.AspNetCore.Authentication.Google
return authorizationEndpoint;
}
private static void AddQueryString(
private void AddQueryString<T>(
IDictionary<string, string> queryStrings,
AuthenticationProperties properties,
string name,
Func<T, string> formatter,
T defaultValue)
{
string value = null;
var parameterValue = properties.GetParameter<T>(name);
if (parameterValue != null)
{
value = formatter(parameterValue);
}
else if (!properties.Items.TryGetValue(name, out value))
{
value = formatter(defaultValue);
}
// Remove the parameter from AuthenticationProperties so it won't be serialized into the state
properties.Items.Remove(name);
if (value != null)
{
queryStrings[name] = value;
}
}
private void AddQueryString(
IDictionary<string, string> queryStrings,
AuthenticationProperties properties,
string name,
string defaultValue = null)
{
string value;
if (!properties.Items.TryGetValue(name, out value))
{
value = defaultValue;
}
else
{
// Remove the parameter from AuthenticationProperties so it won't be serialized to state parameter
properties.Items.Remove(name);
}
if (value == null)
{
return;
}
queryStrings[name] = value;
}
=> AddQueryString(queryStrings, properties, name, x => x, defaultValue);
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Authentication.OAuth
{
public class OAuthChallengeProperties : AuthenticationProperties
{
/// <summary>
/// The parameter key for the "scope" argument being used for a challenge request.
/// </summary>
public static readonly string ScopeKey = "scope";
public OAuthChallengeProperties()
{ }
public OAuthChallengeProperties(IDictionary<string, string> items)
: base(items)
{ }
public OAuthChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
: base(items, parameters)
{ }
/// <summary>
/// The "scope" parameter value being used for a challenge request.
/// </summary>
public ICollection<string> Scope
{
get => GetParameter<ICollection<string>>(ScopeKey);
set => SetParameter(ScopeKey, value);
}
/// <summary>
/// Set the "scope" parameter value.
/// </summary>
/// <param name="scopes">List of scopes.</param>
public virtual void SetScope(params string[] scopes)
{
Scope = scopes;
}
}
}

View File

@ -209,7 +209,8 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
var scope = FormatScope();
var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
var state = Options.StateDataFormat.Protect(properties);
var parameters = new Dictionary<string, string>
@ -223,10 +224,20 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters);
}
/// <summary>
/// Format a list of OAuth scopes.
/// </summary>
/// <param name="scopes">List of scopes.</param>
/// <returns>Formatted scopes.</returns>
protected virtual string FormatScope(IEnumerable<string> scopes)
=> string.Join(" ", scopes); // OAuth2 3.3 space separated
/// <summary>
/// Format the <see cref="OAuthOptions.Scope"/> property.
/// </summary>
/// <returns>Formatted scopes.</returns>
/// <remarks>Subclasses should rather override <see cref="FormatScope(IEnumerable{string})"/>.</remarks>
protected virtual string FormatScope()
{
// OAuth2 3.3 space separated
return string.Join(" ", Options.Scope);
}
=> FormatScope(Options.Scope);
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
{
public class OpenIdConnectChallengeProperties : OAuthChallengeProperties
{
/// <summary>
/// The parameter key for the "max_age" argument being used for a challenge request.
/// </summary>
public static readonly string MaxAgeKey = OpenIdConnectParameterNames.MaxAge;
/// <summary>
/// The parameter key for the "prompt" argument being used for a challenge request.
/// </summary>
public static readonly string PromptKey = OpenIdConnectParameterNames.Prompt;
public OpenIdConnectChallengeProperties()
{ }
public OpenIdConnectChallengeProperties(IDictionary<string, string> items)
: base(items)
{ }
public OpenIdConnectChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
: base(items, parameters)
{ }
/// <summary>
/// The "max_age" parameter value being used for a challenge request.
/// </summary>
public TimeSpan? MaxAge
{
get => GetParameter<TimeSpan?>(MaxAgeKey);
set => SetParameter(MaxAgeKey, value);
}
/// <summary>
/// The "prompt" parameter value being used for a challenge request.
/// </summary>
public string Prompt
{
get => GetParameter<string>(PromptKey);
set => SetParameter(PromptKey, value);
}
}
}

View File

@ -329,15 +329,16 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
RedirectUri = BuildRedirectUri(Options.CallbackPath),
Resource = Options.Resource,
ResponseType = Options.ResponseType,
Prompt = Options.Prompt,
Scope = string.Join(" ", Options.Scope)
Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
};
// Add the 'max_age' parameter to the authentication request if MaxAge is not null.
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
if (Options.MaxAge.HasValue)
var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
if (maxAge.HasValue)
{
message.MaxAge = Convert.ToInt64(Math.Floor((Options.MaxAge.Value).TotalSeconds))
message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
.ToString(CultureInfo.InvariantCulture);
}
@ -783,7 +784,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
/// <param name="properties">The authentication properties.</param>
/// <returns><see cref="HandleRequestResult"/> which is used to determine if the remote authentication was successful.</returns>
protected virtual async Task<HandleRequestResult> GetUserInformationAsync(
OpenIdConnectMessage message, JwtSecurityToken jwt,
OpenIdConnectMessage message, JwtSecurityToken jwt,
ClaimsPrincipal principal, AuthenticationProperties properties)
{
var userInfoEndpoint = _configuration?.UserInfoEndpoint;

View File

@ -426,15 +426,15 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
{
var server = CreateServer(
app => { },
services => services.AddAuthentication().AddFacebook(o => {
services => services.AddAuthentication().AddFacebook(o =>
{
o.AppId = "whatever";
o.AppSecret = "whatever";
o.SignInScheme = FacebookDefaults.AuthenticationScheme;
}),
context =>
async context =>
{
// Gross
context.ChallengeAsync("Facebook").GetAwaiter().GetResult();
await context.ChallengeAsync("Facebook");
return true;
});
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
@ -446,14 +446,14 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
{
var server = CreateServer(
app => { },
services => services.AddAuthentication(o => o.DefaultScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => {
services => services.AddAuthentication(o => o.DefaultScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o =>
{
o.AppId = "whatever";
o.AppSecret = "whatever";
}),
context =>
async context =>
{
// Gross
context.ChallengeAsync("Facebook").GetAwaiter().GetResult();
await context.ChallengeAsync("Facebook");
return true;
});
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
@ -465,14 +465,14 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
{
var server = CreateServer(
app => { },
services => services.AddAuthentication(o => o.DefaultSignInScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => {
services => services.AddAuthentication(o => o.DefaultSignInScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o =>
{
o.AppId = "whatever";
o.AppSecret = "whatever";
}),
context =>
async context =>
{
// Gross
context.ChallengeAsync("Facebook").GetAwaiter().GetResult();
await context.ChallengeAsync("Facebook");
return true;
});
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
@ -498,10 +498,9 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
var server = CreateServer(
app => { },
services => services.AddAuthentication().AddFacebook(o => o.SignInScheme = "Whatever"),
context =>
async context =>
{
// REVIEW: Gross.
Assert.Throws<ArgumentException>("AppId", () => context.ChallengeAsync("Facebook").GetAwaiter().GetResult());
await Assert.ThrowsAsync<ArgumentException>("AppId", () => context.ChallengeAsync("Facebook"));
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
@ -514,10 +513,9 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
var server = CreateServer(
app => { },
services => services.AddAuthentication().AddFacebook(o => o.AppId = "Whatever"),
context =>
async context =>
{
// REVIEW: Gross.
Assert.Throws<ArgumentException>("AppSecret", () => context.ChallengeAsync("Facebook").GetAwaiter().GetResult());
await Assert.ThrowsAsync<ArgumentException>("AppSecret", () => context.ChallengeAsync("Facebook"));
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
@ -550,10 +548,9 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
};
});
},
context =>
async context =>
{
// REVIEW: Gross.
context.ChallengeAsync("Facebook").GetAwaiter().GetResult();
await context.ChallengeAsync("Facebook");
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
@ -562,6 +559,97 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
Assert.Contains("custom=test", query);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsConfigured()
{
var server = CreateServer(
app => app.UseAuthentication(),
services =>
{
services.AddAuthentication().AddFacebook(o =>
{
o.AppId = "Test App Id";
o.AppSecret = "Test App Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
},
async context =>
{
await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme);
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=foo,bar", res.Headers.Location.Query);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsOverwritten()
{
var server = CreateServer(
app => app.UseAuthentication(),
services =>
{
services.AddAuthentication().AddFacebook(o =>
{
o.AppId = "Test App Id";
o.AppSecret = "Test App Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
},
async context =>
{
var properties = new OAuthChallengeProperties();
properties.SetScope("baz", "qux");
await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties);
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz,qux", res.Headers.Location.Query);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties()
{
var server = CreateServer(
app => app.UseAuthentication(),
services =>
{
services.AddAuthentication().AddFacebook(o =>
{
o.AppId = "Test App Id";
o.AppSecret = "Test App Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
},
async context =>
{
var properties = new AuthenticationProperties();
properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties);
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz,qux", res.Headers.Location.Query);
}
[Fact]
public async Task NestedMapWillNotAffectRedirect()
{
@ -620,7 +708,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location);
Assert.Contains("response_type=code", location);
Assert.Contains("client_id=", location);
Assert.Contains("redirect_uri="+ UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location);
Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location);
Assert.Contains("scope=", location);
Assert.Contains("state=", location);
}
@ -643,10 +731,9 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
o.AppSecret = "Test App Secret";
});
},
context =>
async context =>
{
// REVIEW: gross
context.ChallengeAsync("Facebook").GetAwaiter().GetResult();
await context.ChallengeAsync("Facebook");
return true;
});
var transaction = await server.SendAsync("http://example.com/challenge");
@ -672,7 +759,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddFacebook(o =>
.AddFacebook(o =>
{
o.AppId = "Test App Id";
o.AppSecret = "Test App Secret";
@ -728,7 +815,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
Assert.Contains("&access_token=", finalUserInfoEndpoint);
}
private static TestServer CreateServer(Action<IApplicationBuilder> configure, Action<IServiceCollection> configureServices, Func<HttpContext, bool> handler)
private static TestServer CreateServer(Action<IApplicationBuilder> configure, Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler)
{
var builder = new WebHostBuilder()
.Configure(app =>
@ -736,7 +823,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook
configure?.Invoke(app);
app.Use(async (context, next) =>
{
if (handler == null || !handler(context))
if (handler == null || !await handler(context))
{
await next();
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
@ -545,43 +546,160 @@ namespace Microsoft.AspNetCore.Authentication.Google
}
[Fact]
public async Task ChallengeWillUseAuthenticationPropertiesAsParameters()
public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments()
{
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
//AutomaticChallenge = true
o.StateDataFormat = stateFormat;
},
context =>
{
var req = context.Request;
var res = context.Response;
if (req.Path == new PathString("/challenge2"))
{
var req = context.Request;
var res = context.Response;
if (req.Path == new PathString("/challenge2"))
return context.ChallengeAsync("Google", new GoogleChallengeProperties
{
return context.ChallengeAsync("Google", new AuthenticationProperties(
new Dictionary<string, string>()
{
{ "scope", "https://www.googleapis.com/auth/plus.login" },
{ "access_type", "offline" },
{ "approval_prompt", "force" },
{ "prompt", "consent" },
{ "login_hint", "test@example.com" },
{ "include_granted_scopes", "false" }
}));
}
Scope = new string[] { "openid", "https://www.googleapis.com/auth/plus.login" },
AccessType = "offline",
ApprovalPrompt = "force",
Prompt = "consent",
LoginHint = "test@example.com",
IncludeGrantedScopes = false,
});
}
return Task.FromResult<object>(null);
});
return Task.FromResult<object>(null);
});
var transaction = await server.SendAsync("https://example.com/challenge2");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
var query = transaction.Response.Headers.Location.Query;
Assert.Contains("scope=" + UrlEncoder.Default.Encode("https://www.googleapis.com/auth/plus.login"), query);
Assert.Contains("access_type=offline", query);
Assert.Contains("approval_prompt=force", query);
Assert.Contains("prompt=consent", query);
Assert.Contains("include_granted_scopes=false", query);
Assert.Contains("login_hint=" + UrlEncoder.Default.Encode("test@example.com"), query);
// verify query arguments
var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
Assert.Equal("openid https://www.googleapis.com/auth/plus.login", query["scope"]);
Assert.Equal("offline", query["access_type"]);
Assert.Equal("force", query["approval_prompt"]);
Assert.Equal("consent", query["prompt"]);
Assert.Equal("false", query["include_granted_scopes"]);
Assert.Equal("test@example.com", query["login_hint"]);
// verify that the passed items were not serialized
var stateProperties = stateFormat.Unprotect(query["state"]);
Assert.DoesNotContain("scope", stateProperties.Items.Keys);
Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
}
[Fact]
public async Task ChallengeWillUseAuthenticationPropertiesItemsAsParameters()
{
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.StateDataFormat = stateFormat;
},
context =>
{
var req = context.Request;
var res = context.Response;
if (req.Path == new PathString("/challenge2"))
{
return context.ChallengeAsync("Google", new AuthenticationProperties(new Dictionary<string, string>()
{
{ "scope", "https://www.googleapis.com/auth/plus.login" },
{ "access_type", "offline" },
{ "approval_prompt", "force" },
{ "prompt", "consent" },
{ "login_hint", "test@example.com" },
{ "include_granted_scopes", "false" }
}));
}
return Task.FromResult<object>(null);
});
var transaction = await server.SendAsync("https://example.com/challenge2");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
// verify query arguments
var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]);
Assert.Equal("offline", query["access_type"]);
Assert.Equal("force", query["approval_prompt"]);
Assert.Equal("consent", query["prompt"]);
Assert.Equal("false", query["include_granted_scopes"]);
Assert.Equal("test@example.com", query["login_hint"]);
// verify that the passed items were not serialized
var stateProperties = stateFormat.Unprotect(query["state"]);
Assert.DoesNotContain("scope", stateProperties.Items.Keys);
Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
}
[Fact]
public async Task ChallengeWillUseAuthenticationPropertiesItemsAsQueryArgumentsButParametersWillOverwrite()
{
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.StateDataFormat = stateFormat;
},
context =>
{
var req = context.Request;
var res = context.Response;
if (req.Path == new PathString("/challenge2"))
{
return context.ChallengeAsync("Google", new GoogleChallengeProperties(new Dictionary<string, string>
{
["scope"] = "https://www.googleapis.com/auth/plus.login",
["access_type"] = "offline",
["include_granted_scopes"] = "false",
["approval_prompt"] = "force",
["prompt"] = "login",
["login_hint"] = "this-will-be-overwritten@example.com",
})
{
Prompt = "consent",
LoginHint = "test@example.com",
});
}
return Task.FromResult<object>(null);
});
var transaction = await server.SendAsync("https://example.com/challenge2");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
// verify query arguments
var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]);
Assert.Equal("offline", query["access_type"]);
Assert.Equal("force", query["approval_prompt"]);
Assert.Equal("consent", query["prompt"]);
Assert.Equal("false", query["include_granted_scopes"]);
Assert.Equal("test@example.com", query["login_hint"]);
// verify that the passed items were not serialized
var stateProperties = stateFormat.Unprotect(query["state"]);
Assert.DoesNotContain("scope", stateProperties.Items.Keys);
Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
}
[Fact]

View File

@ -525,6 +525,57 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
Assert.Contains("state=", location);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsConfigured()
{
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
var transaction = await server.SendAsync("http://example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=foo%20bar", res.Headers.Location.Query);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsOverwritten()
{
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
var transaction = await server.SendAsync("http://example.com/challengeWithOtherScope");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
[Fact]
public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties()
{
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.Scope.Clear();
o.Scope.Add("foo");
o.Scope.Add("bar");
});
var transaction = await server.SendAsync("http://example.com/challengeWithOtherScopeWithBaseAuthenticationProperties");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
[Fact]
public async Task AuthenticatedEventCanGetRefreshToken()
{
@ -608,6 +659,18 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
{
await context.ChallengeAsync("Microsoft");
}
else if (req.Path == new PathString("/challengeWithOtherScope"))
{
var properties = new OAuthChallengeProperties();
properties.SetScope("baz", "qux");
await context.ChallengeAsync("Microsoft", properties);
}
else if (req.Path == new PathString("/challengeWithOtherScopeWithBaseAuthenticationProperties"))
{
var properties = new AuthenticationProperties();
properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
await context.ChallengeAsync("Microsoft", properties);
}
else if (req.Path == new PathString("/me"))
{
res.Describe(context.User);

View File

@ -0,0 +1,149 @@
using System;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Xunit;
namespace Microsoft.AspNetCore.Authentication.Test
{
public class OAuthChallengePropertiesTest
{
[Fact]
public void ScopeProperty()
{
var properties = new OAuthChallengeProperties
{
Scope = new string[] { "foo", "bar" }
};
Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]);
}
[Fact]
public void ScopeProperty_NullValue()
{
var properties = new OAuthChallengeProperties();
properties.Parameters["scope"] = new string[] { "foo", "bar" };
Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
properties.Scope = null;
Assert.Null(properties.Scope);
}
[Fact]
public void SetScope()
{
var properties = new OAuthChallengeProperties();
properties.SetScope("foo", "bar");
Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]);
}
[Fact]
public void OidcMaxAge()
{
var properties = new OpenIdConnectChallengeProperties()
{
MaxAge = TimeSpan.FromSeconds(200)
};
Assert.Equal(TimeSpan.FromSeconds(200), properties.MaxAge);
}
[Fact]
public void OidcMaxAge_NullValue()
{
var properties = new OpenIdConnectChallengeProperties();
properties.Parameters["max_age"] = TimeSpan.FromSeconds(500);
Assert.Equal(TimeSpan.FromSeconds(500), properties.MaxAge);
properties.MaxAge = null;
Assert.Null(properties.MaxAge);
}
[Fact]
public void OidcPrompt()
{
var properties = new OpenIdConnectChallengeProperties()
{
Prompt = "login"
};
Assert.Equal("login", properties.Prompt);
Assert.Equal("login", properties.Parameters["prompt"]);
}
[Fact]
public void OidcPrompt_NullValue()
{
var properties = new OpenIdConnectChallengeProperties();
properties.Parameters["prompt"] = "consent";
Assert.Equal("consent", properties.Prompt);
properties.Prompt = null;
Assert.Null(properties.Prompt);
}
[Fact]
public void GoogleProperties()
{
var properties = new GoogleChallengeProperties()
{
AccessType = "offline",
ApprovalPrompt = "force",
LoginHint = "test@example.com",
Prompt = "login",
};
Assert.Equal("offline", properties.AccessType);
Assert.Equal("offline", properties.Parameters["access_type"]);
Assert.Equal("force", properties.ApprovalPrompt);
Assert.Equal("force", properties.Parameters["approval_prompt"]);
Assert.Equal("test@example.com", properties.LoginHint);
Assert.Equal("test@example.com", properties.Parameters["login_hint"]);
Assert.Equal("login", properties.Prompt);
Assert.Equal("login", properties.Parameters["prompt"]);
}
[Fact]
public void GoogleProperties_NullValues()
{
var properties = new GoogleChallengeProperties();
properties.Parameters["access_type"] = "offline";
properties.Parameters["approval_prompt"] = "force";
properties.Parameters["login_hint"] = "test@example.com";
properties.Parameters["prompt"] = "login";
Assert.Equal("offline", properties.AccessType);
Assert.Equal("force", properties.ApprovalPrompt);
Assert.Equal("test@example.com", properties.LoginHint);
Assert.Equal("login", properties.Prompt);
properties.AccessType = null;
Assert.Null(properties.AccessType);
properties.ApprovalPrompt = null;
Assert.Null(properties.ApprovalPrompt);
properties.LoginHint = null;
Assert.Null(properties.LoginHint);
properties.Prompt = null;
Assert.Null(properties.Prompt);
}
[Fact]
public void GoogleIncludeGrantedScopes()
{
var properties = new GoogleChallengeProperties()
{
IncludeGrantedScopes = true
};
Assert.True(properties.IncludeGrantedScopes);
Assert.Equal(true, properties.Parameters["include_granted_scopes"]);
properties.IncludeGrantedScopes = false;
Assert.False(properties.IncludeGrantedScopes);
Assert.Equal(false, properties.Parameters["include_granted_scopes"]);
properties.IncludeGrantedScopes = null;
Assert.Null(properties.IncludeGrantedScopes);
}
}
}

View File

@ -572,6 +572,88 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
Assert.Contains("path=/", correlation);
}
[Fact]
public async Task RedirectToAuthorizeEndpoint_HasScopeAsConfigured()
{
var server = CreateServer(
s => s.AddAuthentication().AddOAuth(
"Weblie",
opt =>
{
ConfigureDefaults(opt);
opt.Scope.Clear();
opt.Scope.Add("foo");
opt.Scope.Add("bar");
}),
async ctx =>
{
await ctx.ChallengeAsync("Weblie");
return true;
});
var transaction = await server.SendAsync("https://www.example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=foo%20bar", res.Headers.Location.Query);
}
[Fact]
public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwritten()
{
var server = CreateServer(
s => s.AddAuthentication().AddOAuth(
"Weblie",
opt =>
{
ConfigureDefaults(opt);
opt.Scope.Clear();
opt.Scope.Add("foo");
opt.Scope.Add("bar");
}),
async ctx =>
{
var properties = new OAuthChallengeProperties();
properties.SetScope("baz", "qux");
await ctx.ChallengeAsync("Weblie", properties);
return true;
});
var transaction = await server.SendAsync("https://www.example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
[Fact]
public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwrittenWithBaseAuthenticationProperties()
{
var server = CreateServer(
s => s.AddAuthentication().AddOAuth(
"Weblie",
opt =>
{
ConfigureDefaults(opt);
opt.Scope.Clear();
opt.Scope.Add("foo");
opt.Scope.Add("bar");
}),
async ctx =>
{
var properties = new AuthenticationProperties();
properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
await ctx.ChallengeAsync("Weblie", properties);
return true;
});
var transaction = await server.SendAsync("https://www.example.com/challenge");
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
private void ConfigureDefaults(OAuthOptions o)
{
o.ClientId = "Test Id";

View File

@ -51,14 +51,14 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
[Fact]
public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled()
{
var setting = new TestSettings(opt =>
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.DisableTelemetry = true;
});
var server = setting.CreateTestServer();
var server = settings.CreateTestServer();
var transaction = await server.SendAsync(ChallengeEndpoint);
var res = transaction.Response;
@ -425,6 +425,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(
res.Headers.Location,
OpenIdConnectParameterNames.MaxAge);
@ -446,9 +447,170 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(
res.Headers.Location,
OpenIdConnectParameterNames.MaxAge);
}
[Fact]
public async Task Challenge_HasExpectedPromptParam()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.Prompt = "consent";
});
var server = settings.CreateTestServer();
var transaction = await server.SendAsync(ChallengeEndpoint);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location, OpenIdConnectParameterNames.Prompt);
Assert.Contains("prompt=consent", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenPromptParam()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.Prompt = "consent";
});
var properties = new OpenIdConnectChallengeProperties()
{
Prompt = "login",
};
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("prompt=login", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenPromptParamFromBaseAuthenticationProperties()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.Prompt = "consent";
});
var properties = new AuthenticationProperties();
properties.SetParameter(OpenIdConnectChallengeProperties.PromptKey, "login");
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("prompt=login", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenScopeParam()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.Scope.Clear();
opt.Scope.Add("foo");
opt.Scope.Add("bar");
});
var properties = new OpenIdConnectChallengeProperties();
properties.SetScope("baz", "qux");
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenScopeParamFromBaseAuthenticationProperties()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.Scope.Clear();
opt.Scope.Add("foo");
opt.Scope.Add("bar");
});
var properties = new AuthenticationProperties();
properties.SetParameter(OpenIdConnectChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenMaxAgeParam()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.MaxAge = TimeSpan.FromSeconds(500);
});
var properties = new OpenIdConnectChallengeProperties()
{
MaxAge = TimeSpan.FromSeconds(1234),
};
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("max_age=1234", res.Headers.Location.Query);
}
[Fact]
public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationPropertiesm()
{
var settings = new TestSettings(opt =>
{
opt.ClientId = "Test Id";
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.MaxAge = TimeSpan.FromSeconds(500);
});
var properties = new AuthenticationProperties();
properties.SetParameter(OpenIdConnectChallengeProperties.MaxAgeKey, TimeSpan.FromSeconds(1234));
var server = settings.CreateTestServer(properties);
var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
settings.ValidateChallengeRedirect(res.Headers.Location);
Assert.Contains("max_age=1234", res.Headers.Location.Query);
}
}
}

View File

@ -206,6 +206,9 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
case OpenIdConnectParameterNames.MaxAge:
ValidateMaxAge(actualValues, errors, htmlEncoded);
break;
case OpenIdConnectParameterNames.Prompt:
ValidatePrompt(actualValues, errors, htmlEncoded);
break;
default:
throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\".");
}
@ -284,6 +287,9 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
}
}
private void ValidatePrompt(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
ValidateParameter(OpenIdConnectParameterNames.Prompt, _options.Prompt, actualParams, errors, htmlEncoded);
private void ValidateParameter(
string parameterName,
string expectedValue,