Add parameters for Microsoft Account Authentication (#11059)
Co-Authored-By: campersau <buchholz.bastian@googlemail.com>
This commit is contained in:
parent
639e3dd07b
commit
402394347d
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue