Skip to content

Commit 4023943

Browse files
Kahbazicampersau
authored andcommitted
Add parameters for Microsoft Account Authentication (dotnet#11059)
Co-Authored-By: campersau <[email protected]>
1 parent 639e3dd commit 4023943

File tree

4 files changed

+210
-1
lines changed

4 files changed

+210
-1
lines changed

src/Security/Authentication/MicrosoftAccount/ref/Microsoft.AspNetCore.Authentication.MicrosoftAccount.netcoreapp3.0.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,28 @@ public static partial class MicrosoftAccountDefaults
1414
public partial class MicrosoftAccountHandler : Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>
1515
{
1616
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)) { }
17+
protected override string BuildChallengeUrl(Microsoft.AspNetCore.Authentication.AuthenticationProperties properties, string redirectUri) { throw null; }
1718
[System.Diagnostics.DebuggerStepThroughAttribute]
1819
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; }
1920
}
2021
public partial class MicrosoftAccountOptions : Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions
2122
{
2223
public MicrosoftAccountOptions() { }
2324
}
25+
public partial class MicrosoftChallengeProperties : Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties
26+
{
27+
public static readonly string DomainHintKey;
28+
public static readonly string LoginHintKey;
29+
public static readonly string PromptKey;
30+
public static readonly string ResponseModeKey;
31+
public MicrosoftChallengeProperties() { }
32+
public MicrosoftChallengeProperties(System.Collections.Generic.IDictionary<string, string> items) { }
33+
public MicrosoftChallengeProperties(System.Collections.Generic.IDictionary<string, string> items, System.Collections.Generic.IDictionary<string, object> parameters) { }
34+
public string DomainHint { get { throw null; } set { } }
35+
public string LoginHint { get { throw null; } set { } }
36+
public string Prompt { get { throw null; } set { } }
37+
public string ResponseMode { get { throw null; } set { } }
38+
}
2439
}
2540
namespace Microsoft.Extensions.DependencyInjection
2641
{

src/Security/Authentication/MicrosoftAccount/src/MicrosoftAccountHandler.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Collections.Generic;
46
using System.Net.Http;
57
using System.Net.Http.Headers;
68
using System.Security.Claims;
9+
using System.Security.Cryptography;
10+
using System.Text;
711
using System.Text.Encodings.Web;
812
using System.Text.Json;
913
using System.Threading.Tasks;
1014
using Microsoft.AspNetCore.Authentication.OAuth;
15+
using Microsoft.AspNetCore.WebUtilities;
1116
using Microsoft.Extensions.Logging;
1217
using Microsoft.Extensions.Options;
1318

1419
namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
1520
{
1621
public class MicrosoftAccountHandler : OAuthHandler<MicrosoftAccountOptions>
1722
{
23+
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
24+
1825
public MicrosoftAccountHandler(IOptionsMonitor<MicrosoftAccountOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
1926
: base(options, logger, encoder, clock)
2027
{ }
@@ -38,5 +45,77 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIden
3845
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
3946
}
4047
}
48+
49+
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
50+
{
51+
var queryStrings = new Dictionary<string, string>
52+
{
53+
{ "client_id", Options.ClientId },
54+
{ "response_type", "code" },
55+
{ "redirect_uri", redirectUri }
56+
};
57+
58+
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.ScopeKey, FormatScope, Options.Scope);
59+
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.ResponseModeKey);
60+
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.DomainHintKey);
61+
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.LoginHintKey);
62+
AddQueryString(queryStrings, properties, MicrosoftChallengeProperties.PromptKey);
63+
64+
if (Options.UsePkce)
65+
{
66+
var bytes = new byte[32];
67+
CryptoRandom.GetBytes(bytes);
68+
var codeVerifier = Base64UrlTextEncoder.Encode(bytes);
69+
70+
// Store this for use during the code redemption.
71+
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
72+
73+
using var sha256 = SHA256.Create();
74+
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
75+
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
76+
77+
queryStrings[OAuthConstants.CodeChallengeKey] = codeChallenge;
78+
queryStrings[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
79+
}
80+
81+
var state = Options.StateDataFormat.Protect(properties);
82+
queryStrings.Add("state", state);
83+
84+
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
85+
}
86+
87+
private void AddQueryString<T>(
88+
IDictionary<string, string> queryStrings,
89+
AuthenticationProperties properties,
90+
string name,
91+
Func<T, string> formatter,
92+
T defaultValue)
93+
{
94+
string value = null;
95+
var parameterValue = properties.GetParameter<T>(name);
96+
if (parameterValue != null)
97+
{
98+
value = formatter(parameterValue);
99+
}
100+
else if (!properties.Items.TryGetValue(name, out value))
101+
{
102+
value = formatter(defaultValue);
103+
}
104+
105+
// Remove the parameter from AuthenticationProperties so it won't be serialized into the state
106+
properties.Items.Remove(name);
107+
108+
if (value != null)
109+
{
110+
queryStrings[name] = value;
111+
}
112+
}
113+
114+
private void AddQueryString(
115+
IDictionary<string, string> queryStrings,
116+
AuthenticationProperties properties,
117+
string name,
118+
string defaultValue = null)
119+
=> AddQueryString(queryStrings, properties, name, x => x, defaultValue);
41120
}
42121
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Collections.Generic;
2+
using Microsoft.AspNetCore.Authentication.OAuth;
3+
4+
namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
5+
{
6+
/// <summary>
7+
/// See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference
8+
/// </summary>
9+
public class MicrosoftChallengeProperties : OAuthChallengeProperties
10+
{
11+
/// <summary>
12+
/// The parameter key for the "response_mode" argument being used for a challenge request.
13+
/// </summary>
14+
public static readonly string ResponseModeKey = "response_mode";
15+
16+
/// <summary>
17+
/// The parameter key for the "domain_hint" argument being used for a challenge request.
18+
/// </summary>
19+
public static readonly string DomainHintKey = "domain_hint";
20+
21+
/// <summary>
22+
/// The parameter key for the "login_hint" argument being used for a challenge request.
23+
/// </summary>
24+
public static readonly string LoginHintKey = "login_hint";
25+
26+
/// <summary>
27+
/// The parameter key for the "prompt" argument being used for a challenge request.
28+
/// </summary>
29+
public static readonly string PromptKey = "prompt";
30+
31+
public MicrosoftChallengeProperties()
32+
{ }
33+
34+
public MicrosoftChallengeProperties(IDictionary<string, string> items)
35+
: base(items)
36+
{ }
37+
38+
public MicrosoftChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
39+
: base(items, parameters)
40+
{ }
41+
42+
/// <summary>
43+
/// The "response_mode" parameter value being used for a challenge request.
44+
/// </summary>
45+
public string ResponseMode
46+
{
47+
get => GetParameter<string>(ResponseModeKey);
48+
set => SetParameter(ResponseModeKey, value);
49+
}
50+
51+
/// <summary>
52+
/// The "domain_hint" parameter value being used for a challenge request.
53+
/// </summary>
54+
public string DomainHint
55+
{
56+
get => GetParameter<string>(DomainHintKey);
57+
set => SetParameter(DomainHintKey, value);
58+
}
59+
60+
/// <summary>
61+
/// The "login_hint" parameter value being used for a challenge request.
62+
/// </summary>
63+
public string LoginHint
64+
{
65+
get => GetParameter<string>(LoginHintKey);
66+
set => SetParameter(LoginHintKey, value);
67+
}
68+
69+
/// <summary>
70+
/// The "prompt" parameter value being used for a challenge request.
71+
/// </summary>
72+
public string Prompt
73+
{
74+
get => GetParameter<string>(PromptKey);
75+
set => SetParameter(PromptKey, value);
76+
}
77+
}
78+
}

src/Security/Authentication/test/MicrosoftAccountTests.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,36 @@ public async Task AuthenticatedEventCanGetRefreshToken()
244244
Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken"));
245245
}
246246

247+
[Fact]
248+
public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments()
249+
{
250+
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("MicrosoftTest"));
251+
var server = CreateServer(o =>
252+
{
253+
o.ClientId = "Test Id";
254+
o.ClientSecret = "Test Secret";
255+
o.StateDataFormat = stateFormat;
256+
});
257+
var transaction = await server.SendAsync("https://example.com/challenge");
258+
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
259+
260+
// verify query arguments
261+
var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
262+
Assert.Equal("https://graph.microsoft.com/user.read", query["scope"]);
263+
Assert.Equal("consumers", query["domain_hint"]);
264+
Assert.Equal("username", query["login_hint"]);
265+
Assert.Equal("select_account", query["prompt"]);
266+
Assert.Equal("query", query["response_mode"]);
267+
268+
// verify that the passed items were not serialized
269+
var stateProperties = stateFormat.Unprotect(query["state"]);
270+
Assert.DoesNotContain("scope", stateProperties.Items.Keys);
271+
Assert.DoesNotContain("domain_hint", stateProperties.Items.Keys);
272+
Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
273+
Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
274+
Assert.DoesNotContain("response_mode", stateProperties.Items.Keys);
275+
}
276+
247277
[Fact]
248278
public async Task PkceSentToTokenEndpoint()
249279
{
@@ -324,7 +354,14 @@ private static TestServer CreateServer(Action<MicrosoftAccountOptions> configure
324354
var res = context.Response;
325355
if (req.Path == new PathString("/challenge"))
326356
{
327-
await context.ChallengeAsync("Microsoft", new AuthenticationProperties() { RedirectUri = "/me" } );
357+
await context.ChallengeAsync("Microsoft", new MicrosoftChallengeProperties
358+
{
359+
Prompt = "select_account",
360+
LoginHint = "username",
361+
DomainHint = "consumers",
362+
ResponseMode = "query",
363+
RedirectUri = "/me"
364+
});
328365
}
329366
else if (req.Path == new PathString("/challengeWithOtherScope"))
330367
{

0 commit comments

Comments
 (0)