Skip to content

Add parameters for Microsoft Account Authentication #11059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,28 @@ public static partial class MicrosoftAccountDefaults
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; }
}
public partial class MicrosoftAccountOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions
{
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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
// 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;

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)
{ }
Expand All @@ -38,5 +45,77 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIden
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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link to the doc that defines each of these keys.

{
/// <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);
}
}
}
39 changes: 38 additions & 1 deletion src/Security/Authentication/test/MicrosoftAccountTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,36 @@ public async Task AuthenticatedEventCanGetRefreshToken()
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()
{
Expand Down Expand Up @@ -324,7 +354,14 @@ private static TestServer CreateServer(Action<MicrosoftAccountOptions> configure
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"))
{
Expand Down