Add parameters for Microsoft Account Authentication (#11059)

Co-Authored-By: campersau <buchholz.bastian@googlemail.com>
This commit is contained in:
Kahbazi 2019-06-13 00:37:44 +04:30 committed by Chris Ross
parent 639e3dd07b
commit 402394347d
4 changed files with 210 additions and 1 deletions

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
public partial class MicrosoftAccountHandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>
{
public MicrosoftAccountHandler(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions> options, Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, Microsoft.AspNetCore.Authentication.ISystemClock clock) : base (default(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>), default(Microsoft.Extensions.Logging.ILoggerFactory), default(System.Text.Encodings.Web.UrlEncoder), default(Microsoft.AspNetCore.Authentication.ISystemClock)) { }
protected override string BuildChallengeUrl(Microsoft.AspNetCore.Authentication.AuthenticationProperties properties, string redirectUri) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket> CreateTicketAsync(System.Security.Claims.ClaimsIdentity identity, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties, Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse tokens) { throw null; }
}
@ -21,6 +22,20 @@ namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
{
public MicrosoftAccountOptions() { }
}
public partial class MicrosoftChallengeProperties : Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties
{
public static readonly string DomainHintKey;
public static readonly string LoginHintKey;
public static readonly string PromptKey;
public static readonly string ResponseModeKey;
public MicrosoftChallengeProperties() { }
public MicrosoftChallengeProperties(System.Collections.Generic.IDictionary<string, string> items) { }
public MicrosoftChallengeProperties(System.Collections.Generic.IDictionary<string, string> items, System.Collections.Generic.IDictionary<string, object> parameters) { }
public string DomainHint { get { throw null; } set { } }
public string LoginHint { get { throw null; } set { } }
public string Prompt { get { throw null; } set { } }
public string ResponseMode { get { throw null; } set { } }
}
}
namespace Microsoft.Extensions.DependencyInjection
{

View File

@ -1,13 +1,18 @@
// 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.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -15,6 +20,8 @@ namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
{
public class MicrosoftAccountHandler : OAuthHandler<MicrosoftAccountOptions>
{
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
public MicrosoftAccountHandler(IOptionsMonitor<MicrosoftAccountOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
@ -38,5 +45,77 @@ namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
}
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
var queryStrings = new Dictionary<string, string>
{
{ "client_id", Options.ClientId },
{ "response_type", "code" },
{ "redirect_uri", redirectUri }
};
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.ScopeKey, FormatScope, Options.Scope);
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.ResponseModeKey);
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.DomainHintKey);
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.LoginHintKey);
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.PromptKey);
if (Options.UsePkce)
{
var bytes = new byte[32];
CryptoRandom.GetBytes(bytes);
var codeVerifier = Base64UrlTextEncoder.Encode(bytes);
// Store this for use during the code redemption.
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
using var sha256 = SHA256.Create();
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
queryStrings[OAuthConstants.CodeChallengeKey] = codeChallenge;
queryStrings[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
}
var state = Options.StateDataFormat.Protect(properties);
queryStrings.Add("state", state);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
}
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)
=> AddQueryString(queryStrings, properties, name, x => x, defaultValue);
}
}

View File

@ -0,0 +1,78 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.OAuth;
namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
{
/// <summary>
/// See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference
/// </summary>
public class MicrosoftChallengeProperties : OAuthChallengeProperties
{
/// <summary>
/// The parameter key for the "response_mode" argument being used for a challenge request.
/// </summary>
public static readonly string ResponseModeKey = "response_mode";
/// <summary>
/// The parameter key for the "domain_hint" argument being used for a challenge request.
/// </summary>
public static readonly string DomainHintKey = "domain_hint";
/// <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 PromptKey = "prompt";
public MicrosoftChallengeProperties()
{ }
public MicrosoftChallengeProperties(IDictionary<string, string> items)
: base(items)
{ }
public MicrosoftChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
: base(items, parameters)
{ }
/// <summary>
/// The "response_mode" parameter value being used for a challenge request.
/// </summary>
public string ResponseMode
{
get => GetParameter<string>(ResponseModeKey);
set => SetParameter(ResponseModeKey, value);
}
/// <summary>
/// The "domain_hint" parameter value being used for a challenge request.
/// </summary>
public string DomainHint
{
get => GetParameter<string>(DomainHintKey);
set => SetParameter(DomainHintKey, 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>(PromptKey);
set => SetParameter(PromptKey, value);
}
}
}

View File

@ -244,6 +244,36 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken"));
}
[Fact]
public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments()
{
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("MicrosoftTest"));
var server = CreateServer(o =>
{
o.ClientId = "Test Id";
o.ClientSecret = "Test Secret";
o.StateDataFormat = stateFormat;
});
var transaction = await server.SendAsync("https://example.com/challenge");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
// verify query arguments
var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
Assert.Equal("https://graph.microsoft.com/user.read", query["scope"]);
Assert.Equal("consumers", query["domain_hint"]);
Assert.Equal("username", query["login_hint"]);
Assert.Equal("select_account", query["prompt"]);
Assert.Equal("query", query["response_mode"]);
// verify that the passed items were not serialized
var stateProperties = stateFormat.Unprotect(query["state"]);
Assert.DoesNotContain("scope", stateProperties.Items.Keys);
Assert.DoesNotContain("domain_hint", stateProperties.Items.Keys);
Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
Assert.DoesNotContain("response_mode", stateProperties.Items.Keys);
}
[Fact]
public async Task PkceSentToTokenEndpoint()
{
@ -324,7 +354,14 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
var res = context.Response;
if (req.Path == new PathString("/challenge"))
{
await context.ChallengeAsync("Microsoft", new AuthenticationProperties() { RedirectUri = "/me" } );
await context.ChallengeAsync("Microsoft", new MicrosoftChallengeProperties
{
Prompt = "select_account",
LoginHint = "username",
DomainHint = "consumers",
ResponseMode = "query",
RedirectUri = "/me"
});
}
else if (req.Path == new PathString("/challengeWithOtherScope"))
{