parent
4300f498c7
commit
75e0115de9
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
|
|||
AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint;
|
||||
TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint;
|
||||
UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint;
|
||||
UsePkce = true;
|
||||
Scope.Add("https://graph.microsoft.com/user.read");
|
||||
|
||||
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
|
||||
|
|
|
|||
|
|
@ -28,6 +28,20 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
public System.Collections.Generic.ICollection<string> Scope { get { throw null; } set { } }
|
||||
public virtual void SetScope(params string[] scopes) { }
|
||||
}
|
||||
public partial class OAuthCodeExchangeContext
|
||||
{
|
||||
public OAuthCodeExchangeContext(Microsoft.AspNetCore.Authentication.AuthenticationProperties properties, string code, string redirectUri) { }
|
||||
public string Code { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Authentication.AuthenticationProperties Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public string RedirectUri { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public static partial class OAuthConstants
|
||||
{
|
||||
public static readonly string CodeChallengeKey;
|
||||
public static readonly string CodeChallengeMethodKey;
|
||||
public static readonly string CodeChallengeMethodS256;
|
||||
public static readonly string CodeVerifierKey;
|
||||
}
|
||||
public partial class OAuthCreatingTicketContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>
|
||||
{
|
||||
public OAuthCreatingTicketContext(System.Security.Claims.ClaimsPrincipal principal, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties, Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions options, System.Net.Http.HttpClient backchannel, Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse tokens, System.Text.Json.JsonElement user) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions)) { }
|
||||
|
|
@ -64,7 +78,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
protected virtual 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; }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
protected virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri) { throw null; }
|
||||
protected virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse> ExchangeCodeAsync(Microsoft.AspNetCore.Authentication.OAuth.OAuthCodeExchangeContext context) { throw null; }
|
||||
protected virtual string FormatScope() { throw null; }
|
||||
protected virtual string FormatScope(System.Collections.Generic.IEnumerable<string> scopes) { throw null; }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
|
|
@ -83,6 +97,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
public System.Collections.Generic.ICollection<string> Scope { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties> StateDataFormat { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public string TokenEndpoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool UsePkce { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public string UserInformationEndpoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public override void Validate() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Authentication.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information used to perform the code exchange.
|
||||
/// </summary>
|
||||
public class OAuthCodeExchangeContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="OAuthCodeExchangeContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
|
||||
/// <param name="code">The code returned from the authorization endpoint.</param>
|
||||
/// <param name="redirectUri">The redirect uri used in the authorization request.</param>
|
||||
public OAuthCodeExchangeContext(AuthenticationProperties properties, string code, string redirectUri)
|
||||
{
|
||||
Properties = properties;
|
||||
Code = code;
|
||||
RedirectUri = redirectUri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State for the authentication flow.
|
||||
/// </summary>
|
||||
public AuthenticationProperties Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The code returned from the authorization endpoint.
|
||||
/// </summary>
|
||||
public string Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The redirect uri used in the authorization request.
|
||||
/// </summary>
|
||||
public string RedirectUri { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Authentication.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants used in the OAuth protocol
|
||||
/// </summary>
|
||||
public static class OAuthConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// code_verifier defined in https://tools.ietf.org/html/rfc7636
|
||||
/// </summary>
|
||||
public static readonly string CodeVerifierKey = "code_verifier";
|
||||
|
||||
/// <summary>
|
||||
/// code_challenge defined in https://tools.ietf.org/html/rfc7636
|
||||
/// </summary>
|
||||
public static readonly string CodeChallengeKey = "code_challenge";
|
||||
|
||||
/// <summary>
|
||||
/// code_challenge_method defined in https://tools.ietf.org/html/rfc7636
|
||||
/// </summary>
|
||||
public static readonly string CodeChallengeMethodKey = "code_challenge_method";
|
||||
|
||||
/// <summary>
|
||||
/// S256 defined in https://tools.ietf.org/html/rfc7636
|
||||
/// </summary>
|
||||
public static readonly string CodeChallengeMethodS256 = "S256";
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using System.Globalization;
|
|||
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;
|
||||
|
|
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
{
|
||||
public class OAuthHandler<TOptions> : RemoteAuthenticationHandler<TOptions> where TOptions : OAuthOptions, new()
|
||||
{
|
||||
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
|
||||
protected HttpClient Backchannel => Options.Backchannel;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -99,77 +101,84 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
return HandleRequestResult.Fail("Code was not found.", properties);
|
||||
}
|
||||
|
||||
using (var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)))
|
||||
var codeExchangeContext = new OAuthCodeExchangeContext(properties, code, BuildRedirectUri(Options.CallbackPath));
|
||||
using var tokens = await ExchangeCodeAsync(codeExchangeContext);
|
||||
|
||||
if (tokens.Error != null)
|
||||
{
|
||||
if (tokens.Error != null)
|
||||
return HandleRequestResult.Fail(tokens.Error, properties);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(tokens.AccessToken))
|
||||
{
|
||||
return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(ClaimsIssuer);
|
||||
|
||||
if (Options.SaveTokens)
|
||||
{
|
||||
var authTokens = new List<AuthenticationToken>();
|
||||
|
||||
authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
|
||||
if (!string.IsNullOrEmpty(tokens.RefreshToken))
|
||||
{
|
||||
return HandleRequestResult.Fail(tokens.Error, properties);
|
||||
authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(tokens.AccessToken))
|
||||
if (!string.IsNullOrEmpty(tokens.TokenType))
|
||||
{
|
||||
return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
|
||||
authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(ClaimsIssuer);
|
||||
|
||||
if (Options.SaveTokens)
|
||||
if (!string.IsNullOrEmpty(tokens.ExpiresIn))
|
||||
{
|
||||
var authTokens = new List<AuthenticationToken>();
|
||||
|
||||
authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
|
||||
if (!string.IsNullOrEmpty(tokens.RefreshToken))
|
||||
int value;
|
||||
if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tokens.TokenType))
|
||||
{
|
||||
authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tokens.ExpiresIn))
|
||||
{
|
||||
int value;
|
||||
if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||
// https://www.w3.org/TR/xmlschema-2/#dateTime
|
||||
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
|
||||
var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
|
||||
authTokens.Add(new AuthenticationToken
|
||||
{
|
||||
// https://www.w3.org/TR/xmlschema-2/#dateTime
|
||||
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
|
||||
var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
|
||||
authTokens.Add(new AuthenticationToken
|
||||
{
|
||||
Name = "expires_at",
|
||||
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
|
||||
});
|
||||
}
|
||||
Name = "expires_at",
|
||||
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
|
||||
});
|
||||
}
|
||||
|
||||
properties.StoreTokens(authTokens);
|
||||
}
|
||||
|
||||
var ticket = await CreateTicketAsync(identity, properties, tokens);
|
||||
if (ticket != null)
|
||||
{
|
||||
return HandleRequestResult.Success(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
|
||||
}
|
||||
properties.StoreTokens(authTokens);
|
||||
}
|
||||
|
||||
var ticket = await CreateTicketAsync(identity, properties, tokens);
|
||||
if (ticket != null)
|
||||
{
|
||||
return HandleRequestResult.Success(ticket);
|
||||
}
|
||||
else
|
||||
{
|
||||
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
|
||||
protected virtual async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
|
||||
{
|
||||
var tokenRequestParameters = new Dictionary<string, string>()
|
||||
{
|
||||
{ "client_id", Options.ClientId },
|
||||
{ "redirect_uri", redirectUri },
|
||||
{ "redirect_uri", context.RedirectUri },
|
||||
{ "client_secret", Options.ClientSecret },
|
||||
{ "code", code },
|
||||
{ "code", context.Code },
|
||||
{ "grant_type", "authorization_code" },
|
||||
};
|
||||
|
||||
// PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see BuildChallengeUrl
|
||||
if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
|
||||
{
|
||||
tokenRequestParameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
|
||||
context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey);
|
||||
}
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
|
||||
|
|
@ -241,15 +250,33 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
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>
|
||||
{
|
||||
{ "client_id", Options.ClientId },
|
||||
{ "scope", scope },
|
||||
{ "response_type", "code" },
|
||||
{ "redirect_uri", redirectUri },
|
||||
{ "state", state },
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
parameters[OAuthConstants.CodeChallengeKey] = codeChallenge;
|
||||
parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
|
||||
}
|
||||
|
||||
parameters["state"] = Options.StateDataFormat.Protect(properties);
|
||||
|
||||
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,5 +102,11 @@ namespace Microsoft.AspNetCore.Authentication.OAuth
|
|||
/// Gets or sets the type used to secure data handled by the middleware.
|
||||
/// </summary>
|
||||
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the use of the Proof Key for Code Exchange (PKCE) standard. See https://tools.ietf.org/html/rfc7636.
|
||||
/// The default value is `false` but derived handlers should enable this if their provider supports it.
|
||||
/// </summary>
|
||||
public bool UsePkce { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
public Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties> StateDataFormat { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Authentication.ISecureDataFormat<string> StringDataFormat { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.IdentityModel.Tokens.TokenValidationParameters TokenValidationParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool UsePkce { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool UseTokenLifetime { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public override void Validate() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"SocialSample": {
|
||||
"OpenIdConnectSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
|
|
|
|||
|
|
@ -45,16 +45,22 @@ namespace OpenIdConnectSample
|
|||
.AddCookie()
|
||||
.AddOpenIdConnect(o =>
|
||||
{
|
||||
/*
|
||||
o.ClientId = Configuration["oidc:clientid"];
|
||||
o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow
|
||||
o.Authority = Configuration["oidc:authority"];
|
||||
*/
|
||||
// https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs
|
||||
o.ClientId = "server.hybrid";
|
||||
o.ClientSecret = "secret"; // for code flow
|
||||
o.Authority = "https://demo.identityserver.io/";
|
||||
|
||||
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
|
||||
o.SaveTokens = true;
|
||||
o.GetClaimsFromUserInfoEndpoint = true;
|
||||
o.AccessDeniedPath = "/access-denied-from-remote";
|
||||
|
||||
o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce");
|
||||
// o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce");
|
||||
|
||||
o.Events = new OpenIdConnectEvents()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ using System.Text;
|
|||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
|
@ -30,7 +32,6 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>, IAuthenticationSignOutHandler
|
||||
{
|
||||
private const string NonceProperty = "N";
|
||||
|
||||
private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
|
||||
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
|
||||
|
|
@ -366,6 +367,24 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
|
||||
};
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636
|
||||
if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
|
||||
{
|
||||
var bytes = new byte[32];
|
||||
CryptoRandom.GetBytes(bytes);
|
||||
var codeVerifier = Base64UrlTextEncoder.Encode(bytes);
|
||||
|
||||
// Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
|
||||
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
|
||||
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
|
||||
|
||||
message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
|
||||
message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
|
||||
}
|
||||
|
||||
// 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
|
||||
var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
|
||||
|
|
@ -1097,6 +1116,13 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]
|
||||
};
|
||||
|
||||
// PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see HandleChallengeAsyncInternal
|
||||
if (properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
|
||||
{
|
||||
tokenEndpointRequest.Parameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
|
||||
properties.Items.Remove(OAuthConstants.CodeVerifierKey);
|
||||
}
|
||||
|
||||
var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties)
|
||||
{
|
||||
ProtocolMessage = authorizationResponse,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,14 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
set => _nonceCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the use of the Proof Key for Code Exchange (PKCE) standard.
|
||||
/// This only applies when the <see cref="ResponseType"/> is set to <see cref="OpenIdConnectResponseType.Code"/>.
|
||||
/// See https://tools.ietf.org/html/rfc7636.
|
||||
/// The default value is `true`.
|
||||
/// </summary>
|
||||
public bool UsePkce { get; set; } = true;
|
||||
|
||||
private class OpenIdConnectNonceCookieBuilder : RequestPathBaseCookieBuilder
|
||||
{
|
||||
private readonly OpenIdConnectOptions _options;
|
||||
|
|
|
|||
|
|
@ -50,159 +50,168 @@ namespace SocialSample
|
|||
// https://developers.facebook.com/apps/
|
||||
// https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#login
|
||||
.AddFacebook(o =>
|
||||
{
|
||||
o.AppId = Configuration["facebook:appid"];
|
||||
o.AppSecret = Configuration["facebook:appsecret"];
|
||||
o.Scope.Add("email");
|
||||
o.Fields.Add("name");
|
||||
o.Fields.Add("email");
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// You must first create an app with Google and add its ID and Secret to your user-secrets.
|
||||
// https://console.developers.google.com/project
|
||||
// https://developers.google.com/identity/protocols/OAuth2WebServer
|
||||
// https://developers.google.com/+/web/people/
|
||||
.AddOAuth("Google-AccessToken", "Google AccessToken only", o =>
|
||||
{
|
||||
o.ClientId = Configuration["google:clientid"];
|
||||
o.ClientSecret = Configuration["google:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-google-token");
|
||||
o.AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint;
|
||||
o.TokenEndpoint = GoogleDefaults.TokenEndpoint;
|
||||
o.Scope.Add("openid");
|
||||
o.Scope.Add("profile");
|
||||
o.Scope.Add("email");
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
o.AppId = Configuration["facebook:appid"];
|
||||
o.AppSecret = Configuration["facebook:appsecret"];
|
||||
o.Scope.Add("email");
|
||||
o.Fields.Add("name");
|
||||
o.Fields.Add("email");
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// You must first create an app with Google and add its ID and Secret to your user-secrets.
|
||||
// https://console.developers.google.com/project
|
||||
// https://developers.google.com/identity/protocols/OAuth2WebServer
|
||||
// https://developers.google.com/+/web/people/
|
||||
.AddGoogle(o =>
|
||||
{
|
||||
o.ClientId = Configuration["google:clientid"];
|
||||
o.ClientSecret = Configuration["google:clientsecret"];
|
||||
o.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response
|
||||
o.AccessType = "offline";
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
o.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url");
|
||||
o.ClaimActions.Remove(ClaimTypes.GivenName);
|
||||
})
|
||||
o.ClientId = Configuration["google:clientid"];
|
||||
o.ClientSecret = Configuration["google:clientsecret"];
|
||||
o.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response
|
||||
o.AccessType = "offline";
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
o.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url");
|
||||
o.ClaimActions.Remove(ClaimTypes.GivenName);
|
||||
})
|
||||
// You must first create an app with Twitter and add its key and Secret to your user-secrets.
|
||||
// https://apps.twitter.com/
|
||||
// https://developer.twitter.com/en/docs/basics/authentication/api-reference/access_token
|
||||
.AddTwitter(o =>
|
||||
{
|
||||
o.ConsumerKey = Configuration["twitter:consumerkey"];
|
||||
o.ConsumerSecret = Configuration["twitter:consumersecret"];
|
||||
// http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370
|
||||
// http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1
|
||||
o.RetrieveUserDetails = true;
|
||||
o.SaveTokens = true;
|
||||
o.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri);
|
||||
o.Events = new TwitterEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
o.ConsumerKey = Configuration["twitter:consumerkey"];
|
||||
o.ConsumerSecret = Configuration["twitter:consumersecret"];
|
||||
// http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370
|
||||
// http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1
|
||||
o.RetrieveUserDetails = true;
|
||||
o.SaveTokens = true;
|
||||
o.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri);
|
||||
o.Events = new TwitterEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
/* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs.
|
||||
Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL:
|
||||
Therefore, to authenticate through microsoft accounts, try out the sample using the following URL:
|
||||
https://localhost:44318/
|
||||
*/
|
||||
// You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets.
|
||||
// https://apps.dev.microsoft.com/
|
||||
.AddOAuth("Microsoft-AccessToken", "Microsoft AccessToken only", o =>
|
||||
{
|
||||
o.ClientId = Configuration["microsoftaccount:clientid"];
|
||||
o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-microsoft-token");
|
||||
o.AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint;
|
||||
o.TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint;
|
||||
o.Scope.Add("https://graph.microsoft.com/user.read");
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets.
|
||||
// https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-app-registration/
|
||||
// https://apps.dev.microsoft.com/
|
||||
.AddMicrosoftAccount(o =>
|
||||
{
|
||||
o.ClientId = Configuration["microsoftaccount:clientid"];
|
||||
o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
|
||||
o.SaveTokens = true;
|
||||
o.Scope.Add("offline_access");
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// You must first create an app with GitHub and add its ID and Secret to your user-secrets.
|
||||
// https://github.com/settings/applications/
|
||||
.AddOAuth("GitHub-AccessToken", "GitHub AccessToken only", o =>
|
||||
{
|
||||
o.ClientId = Configuration["github-token:clientid"];
|
||||
o.ClientSecret = Configuration["github-token:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-github-token");
|
||||
o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
||||
o.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
o.ClientId = Configuration["microsoftaccount:clientid"];
|
||||
o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
|
||||
o.SaveTokens = true;
|
||||
o.Scope.Add("offline_access");
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// You must first create an app with GitHub and add its ID and Secret to your user-secrets.
|
||||
// https://github.com/settings/applications/
|
||||
.AddOAuth("GitHub", "Github", o =>
|
||||
{
|
||||
o.ClientId = Configuration["github:clientid"];
|
||||
o.ClientSecret = Configuration["github:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-github");
|
||||
o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
||||
o.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||
o.UserInformationEndpoint = "https://api.github.com/user";
|
||||
o.ClaimsIssuer = "OAuth2-Github";
|
||||
o.SaveTokens = true;
|
||||
// Retrieving user information is unique to each provider.
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
|
||||
o.ClaimActions.MapJsonKey("urn:github:name", "name");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
|
||||
o.ClaimActions.MapJsonKey("urn:github:url", "url");
|
||||
o.Events = new OAuthEvents
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure,
|
||||
OnCreatingTicket = async context =>
|
||||
o.ClientId = Configuration["github:clientid"];
|
||||
o.ClientSecret = Configuration["github:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-github");
|
||||
o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
||||
o.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||
o.UserInformationEndpoint = "https://api.github.com/user";
|
||||
o.ClaimsIssuer = "OAuth2-Github";
|
||||
o.SaveTokens = true;
|
||||
// Retrieving user information is unique to each provider.
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
|
||||
o.ClaimActions.MapJsonKey("urn:github:name", "name");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
|
||||
o.ClaimActions.MapJsonKey("urn:github:url", "url");
|
||||
o.Events = new OAuthEvents
|
||||
{
|
||||
// Get the GitHub user
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
|
||||
OnRemoteFailure = HandleOnRemoteFailure,
|
||||
OnCreatingTicket = async context =>
|
||||
{
|
||||
context.RunClaimActions(user.RootElement);
|
||||
// Get the GitHub user
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
|
||||
{
|
||||
context.RunClaimActions(user.RootElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
})
|
||||
// You must first create an app with GitHub and add its ID and Secret to your user-secrets.
|
||||
// https://github.com/settings/applications/
|
||||
.AddOAuth("GitHub-AccessToken", "GitHub AccessToken only", o =>
|
||||
{
|
||||
o.ClientId = Configuration["github-token:clientid"];
|
||||
o.ClientSecret = Configuration["github-token:clientsecret"];
|
||||
o.CallbackPath = new PathString("/signin-github-token");
|
||||
o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
||||
o.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
||||
o.SaveTokens = true;
|
||||
o.Events = new OAuthEvents()
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure
|
||||
};
|
||||
})
|
||||
// https://demo.identityserver.io/
|
||||
.AddOAuth("IdentityServer", "Identity Server", o =>
|
||||
{
|
||||
o.ClientId = "server.code";
|
||||
o.ClientSecret = "secret";
|
||||
o.CallbackPath = new PathString("/signin-identityserver");
|
||||
o.AuthorizationEndpoint = "https://demo.identityserver.io/connect/authorize";
|
||||
o.TokenEndpoint = "https://demo.identityserver.io/connect/token";
|
||||
o.UserInformationEndpoint = "https://demo.identityserver.io/connect/userinfo";
|
||||
o.ClaimsIssuer = "IdentityServer";
|
||||
o.SaveTokens = true;
|
||||
o.UsePkce = true;
|
||||
// Retrieving user information is unique to each provider.
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
|
||||
o.ClaimActions.MapJsonKey("email_verified", "email_verified");
|
||||
o.ClaimActions.MapJsonKey(ClaimTypes.Uri, "website");
|
||||
o.Scope.Add("openid");
|
||||
o.Scope.Add("profile");
|
||||
o.Scope.Add("email");
|
||||
o.Scope.Add("offline_access");
|
||||
o.Events = new OAuthEvents
|
||||
{
|
||||
OnRemoteFailure = HandleOnRemoteFailure,
|
||||
OnCreatingTicket = async context =>
|
||||
{
|
||||
// Get the user
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
|
||||
{
|
||||
context.RunClaimActions(user.RootElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleOnRemoteFailure(RemoteFailureContext context)
|
||||
|
|
@ -210,14 +219,15 @@ namespace SocialSample
|
|||
context.Response.StatusCode = 500;
|
||||
context.Response.ContentType = "text/html";
|
||||
await context.Response.WriteAsync("<html><body>");
|
||||
await context.Response.WriteAsync("A remote failure has occurred: " + UrlEncoder.Default.Encode(context.Failure.Message) + "<br>");
|
||||
await context.Response.WriteAsync("A remote failure has occurred: <br>" +
|
||||
context.Failure.Message.Split(Environment.NewLine).Select(s => HtmlEncoder.Default.Encode(s) + "<br>").Aggregate((s1, s2) => s1 + s2));
|
||||
|
||||
if (context.Properties != null)
|
||||
{
|
||||
await context.Response.WriteAsync("Properties:<br>");
|
||||
foreach (var pair in context.Properties.Items)
|
||||
{
|
||||
await context.Response.WriteAsync($"-{ UrlEncoder.Default.Encode(pair.Key)}={ UrlEncoder.Default.Encode(pair.Value)}<br>");
|
||||
await context.Response.WriteAsync($"-{ HtmlEncoder.Default.Encode(pair.Key)}={ HtmlEncoder.Default.Encode(pair.Value)}<br>");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +305,8 @@ namespace SocialSample
|
|||
|
||||
var currentAuthType = user.Identities.First().AuthenticationType;
|
||||
if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType)
|
||||
|| string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType))
|
||||
|| string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType)
|
||||
|| string.Equals("IdentityServer", currentAuthType))
|
||||
{
|
||||
var refreshToken = authProperties.GetTokenValue("refresh_token");
|
||||
|
||||
|
|
@ -472,6 +483,10 @@ namespace SocialSample
|
|||
{
|
||||
return Task.FromResult<OAuthOptions>(context.RequestServices.GetRequiredService<IOptionsMonitor<FacebookOptions>>().Get(currentAuthType));
|
||||
}
|
||||
else if (string.Equals("IdentityServer", currentAuthType))
|
||||
{
|
||||
return Task.FromResult<OAuthOptions>(context.RequestServices.GetRequiredService<IOptionsMonitor<OAuthOptions>>().Get(currentAuthType));
|
||||
}
|
||||
|
||||
throw new NotImplementedException(currentAuthType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,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 System;
|
||||
|
|
@ -119,6 +120,8 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
|
|||
Assert.Contains("redirect_uri=", location);
|
||||
Assert.Contains("scope=", location);
|
||||
Assert.Contains("state=", location);
|
||||
Assert.Contains("code_challenge=", location);
|
||||
Assert.Contains("code_challenge_method=S256", location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -241,6 +244,74 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
|
|||
Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PkceSentToTokenEndpoint()
|
||||
{
|
||||
var server = CreateServer(o =>
|
||||
{
|
||||
o.ClientId = "Test Client Id";
|
||||
o.ClientSecret = "Test Client Secret";
|
||||
o.BackchannelHttpHandler = new TestHttpMessageHandler
|
||||
{
|
||||
Sender = req =>
|
||||
{
|
||||
if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
{
|
||||
var body = req.Content.ReadAsStringAsync().Result;
|
||||
var form = new FormReader(body);
|
||||
var entries = form.ReadForm();
|
||||
Assert.Equal("Test Client Id", entries["client_id"]);
|
||||
Assert.Equal("https://example.com/signin-microsoft", entries["redirect_uri"]);
|
||||
Assert.Equal("Test Client Secret", entries["client_secret"]);
|
||||
Assert.Equal("TestCode", entries["code"]);
|
||||
Assert.Equal("authorization_code", entries["grant_type"]);
|
||||
Assert.False(string.IsNullOrEmpty(entries["code_verifier"]));
|
||||
|
||||
return ReturnJsonResponse(new
|
||||
{
|
||||
access_token = "Test Access Token",
|
||||
expire_in = 3600,
|
||||
token_type = "Bearer",
|
||||
});
|
||||
}
|
||||
else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://graph.microsoft.com/v1.0/me")
|
||||
{
|
||||
return ReturnJsonResponse(new
|
||||
{
|
||||
id = "Test User ID",
|
||||
displayName = "Test Name",
|
||||
givenName = "Test Given Name",
|
||||
surname = "Test Family Name",
|
||||
mail = "Test email"
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
var transaction = await server.SendAsync("https://example.com/challenge");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
var locationUri = transaction.Response.Headers.Location;
|
||||
Assert.StartsWith("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", locationUri.AbsoluteUri);
|
||||
|
||||
var queryParams = QueryHelpers.ParseQuery(locationUri.Query);
|
||||
Assert.False(string.IsNullOrEmpty(queryParams["code_challenge"]));
|
||||
Assert.Equal("S256", queryParams["code_challenge_method"]);
|
||||
|
||||
var nonceCookie = transaction.SetCookie.Single();
|
||||
nonceCookie = nonceCookie.Substring(0, nonceCookie.IndexOf(';'));
|
||||
|
||||
transaction = await server.SendAsync(
|
||||
"https://example.com/signin-microsoft?code=TestCode&state=" + queryParams["state"],
|
||||
nonceCookie);
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.StartsWith(".AspNetCore.Correlation.Microsoft.", transaction.SetCookie[0]);
|
||||
Assert.StartsWith(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
}
|
||||
|
||||
private static TestServer CreateServer(Action<MicrosoftAccountOptions> configureOptions)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
|
|
@ -253,7 +324,7 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
|
|||
var res = context.Response;
|
||||
if (req.Path == new PathString("/challenge"))
|
||||
{
|
||||
await context.ChallengeAsync("Microsoft");
|
||||
await context.ChallengeAsync("Microsoft", new AuthenticationProperties() { RedirectUri = "/me" } );
|
||||
}
|
||||
else if (req.Path == new PathString("/challengeWithOtherScope"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,6 +48,67 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
OpenIdConnectParameterNames.VersionTelemetry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task ChallengeIncludesPkceIfRequested(bool include)
|
||||
{
|
||||
var settings = new TestSettings(
|
||||
opt =>
|
||||
{
|
||||
opt.Authority = TestServerBuilder.DefaultAuthority;
|
||||
opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
|
||||
opt.ResponseType = OpenIdConnectResponseType.Code;
|
||||
opt.ClientId = "Test Id";
|
||||
opt.UsePkce = include;
|
||||
});
|
||||
|
||||
var server = settings.CreateTestServer();
|
||||
var transaction = await server.SendAsync(ChallengeEndpoint);
|
||||
|
||||
var res = transaction.Response;
|
||||
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
|
||||
Assert.NotNull(res.Headers.Location);
|
||||
|
||||
if (include)
|
||||
{
|
||||
Assert.Contains("code_challenge=", res.Headers.Location.Query);
|
||||
Assert.Contains("code_challenge_method=S256", res.Headers.Location.Query);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.DoesNotContain("code_challenge=", res.Headers.Location.Query);
|
||||
Assert.DoesNotContain("code_challenge_method=", res.Headers.Location.Query);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OpenIdConnectResponseType.Token)]
|
||||
[InlineData(OpenIdConnectResponseType.IdToken)]
|
||||
[InlineData(OpenIdConnectResponseType.CodeIdToken)]
|
||||
public async Task ChallengeDoesNotIncludePkceForOtherResponseTypes(string responseType)
|
||||
{
|
||||
var settings = new TestSettings(
|
||||
opt =>
|
||||
{
|
||||
opt.Authority = TestServerBuilder.DefaultAuthority;
|
||||
opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
|
||||
opt.ResponseType = responseType;
|
||||
opt.ClientId = "Test Id";
|
||||
opt.UsePkce = true;
|
||||
});
|
||||
|
||||
var server = settings.CreateTestServer();
|
||||
var transaction = await server.SendAsync(ChallengeEndpoint);
|
||||
|
||||
var res = transaction.Response;
|
||||
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
|
||||
Assert.NotNull(res.Headers.Location);
|
||||
|
||||
Assert.DoesNotContain("code_challenge=", res.Headers.Location.Query);
|
||||
Assert.DoesNotContain("code_challenge_method=", res.Headers.Location.Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled()
|
||||
{
|
||||
|
|
@ -613,4 +674,4 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
|||
Assert.Contains("max_age=1234", res.Headers.Location.Query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue