[ApiAuthorization] Small fixes to adjust for the code on the SPA templates (#7090)

* Fix solution file
* Fixes the post_logout_redirect_uri parameter on the DefaultClientRequestParametersProvider
* Propagate state on ending session autoredirect
* Update defaults for local-spa profile to align them with template code
* Disable explicit support for WebApplications, it can still be enabled normally by configuring IS and we haven't evaluated the scenario E2E.
This commit is contained in:
Javier Calvarro Nelson 2019-01-29 10:27:16 -08:00 committed by GitHub
parent 3e3441481c
commit e0b264f1c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1298 additions and 605 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.ApiAuthorization.IdentityServer
@ -32,10 +32,5 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
/// The application is a native application like a mobile or desktop application.
/// </summary>
public const string NativeApp = "NativeApp";
/// <summary>
/// The application is a web application.
/// </summary>
internal const string WebApplication = "WebApplication";
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -12,8 +12,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
{
internal class ConfigureClients : IConfigureOptions<ApiAuthorizationOptions>
{
private const string DefaultLocalSPARelativeRedirectUri = "";
private const string DefaultLocalSPARelativePostLogoutRedirectUri = "";
private const string DefaultLocalSPARelativeRedirectUri = "/authentication/login-callback";
private const string DefaultLocalSPARelativePostLogoutRedirectUri = "/authentication/logout-callback";
private readonly IConfiguration _configuration;
private readonly ILogger<ConfigureClients> _logger;
@ -50,9 +50,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
case ApplicationProfiles.SPA:
yield return GetSPA(name, definition);
break;
//case ApplicationProfiles.WebApplication:
// yield return GetWebApplication(name, definition);
// break;
case ApplicationProfiles.IdentityServerSPA:
yield return GetLocalSPA(name, definition);
break;
@ -66,47 +63,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
}
}
private Client GetWebApplication(string name, ClientDefinition definition)
{
if (definition.RedirectUri == null ||
!Uri.TryCreate(definition.RedirectUri, UriKind.Absolute, out var redirectUri))
{
throw new InvalidOperationException($"The redirect uri " +
$"'{definition.RedirectUri}' for '{name}' is invalid. " +
$"The redirect URI must be an absolute url.");
}
if (definition.LogoutUri == null ||
!Uri.TryCreate(definition.LogoutUri, UriKind.Absolute, out var postLogouturi))
{
throw new InvalidOperationException($"The logout uri " +
$"'{definition.LogoutUri}' for '{name}' is invalid. " +
$"The logout URI must be an absolute url.");
}
if (!string.Equals(
redirectUri.GetLeftPart(UriPartial.Authority),
postLogouturi.GetLeftPart(UriPartial.Authority),
StringComparison.Ordinal))
{
throw new InvalidOperationException($"The redirect uri and the logout uri " +
$"for '{name}' have a different scheme, host or port.");
}
if (definition.ClientSecret == null)
{
throw new InvalidOperationException($"The configuration for '{name}' does not contain a client secret. " +
$"Client secrets are required for web applications.");
}
return ClientBuilder.WebApplication(name)
.WithRedirectUri(definition.RedirectUri)
.WithLogoutRedirectUri(definition.LogoutUri)
.FromConfiguration()
.WithClientSecret(definition.ClientSecret)
.Build();
}
private Client GetSPA(string name, ClientDefinition definition)
{
if (definition.RedirectUri == null ||

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -12,8 +12,10 @@ using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
{
@ -67,7 +69,13 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
await ctx.SignOutAsync();
}
return new RedirectResult(result.ValidatedRequest.PostLogOutUri);
var postLogOutUri = result.ValidatedRequest.PostLogOutUri;
if (result.ValidatedRequest.State != null)
{
postLogOutUri = QueryHelpers.AddQueryString(postLogOutUri, OpenIdConnectParameterNames.State, result.ValidatedRequest.State);
}
return new RedirectResult(postLogOutUri);
}
else
{

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 IdentityServer4.Extensions;
@ -43,9 +43,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
case ApplicationProfiles.NativeApp:
responseType = "code";
break;
//case ApplicationProfiles.WebApplication:
// responseType = "id_token code";
// break;
default:
throw new InvalidOperationException($"Invalid application type '{type}' for '{clientId}'.");
}
@ -55,7 +52,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
["authority"] = authority,
["client_id"] = client.ClientId,
["redirect_uri"] = UrlFactory.GetAbsoluteUrl(context, client.RedirectUris.First()),
["post_logout_redirect_uri"] = UrlFactory.GetAbsoluteUrl(context, client.RedirectUris.First()),
["post_logout_redirect_uri"] = UrlFactory.GetAbsoluteUrl(context, client.PostLogoutRedirectUris.First()),
["response_type"] = responseType,
["scope"] = string.Join(" ", client.AllowedScopes)
};

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -65,21 +65,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
.WithScopes(IdentityServerConstants.StandardScopes.OfflineAccess);
}
/// <summary>
/// Creates a new builder for an externally registered web application.
/// </summary>
/// <param name="clientId">The client id for the web application.</param>
/// <returns>A <see cref="ClientBuilder"/>.</returns>
internal static ClientBuilder WebApplication(string clientId)
{
var client = CreateClient(clientId);
return new ClientBuilder(client)
.WithApplicationProfile(ApplicationProfiles.WebApplication)
.WithAllowedGrants(GrantTypes.HybridAndClientCredentials)
.WithScopes(IdentityServerConstants.StandardScopes.OfflineAccess);
}
/// <summary>
/// Initializes a new instance of <see cref="ClientBuilder"/>.
/// </summary>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 IdentityServer4.Models;
@ -110,17 +110,5 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
configure(app);
Add(app.Build());
}
/// <summary>
/// Adds an externally registered web application..
/// </summary>
/// <param name="clientId">The client id for the web application.</param>
/// <param name="configure">The <see cref="Action{ClientBuilder}"/> to configure the web application.</param>
public void AddWebApplication(string clientId, Action<ClientBuilder> configure)
{
var app = ClientBuilder.WebApplication(clientId);
configure(app);
Add(app.Build());
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
.Build());
// Act
configureClientScopes.PostConfigure(Extensions.Options.Options.DefaultName, options);
configureClientScopes.PostConfigure(Options.DefaultName, options);
// Assert
foreach (var client in options.Clients)
@ -63,7 +64,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
.Build());
// Act
configureClientScopes.PostConfigure(Extensions.Options.Options.DefaultName, options);
configureClientScopes.PostConfigure(Options.DefaultName, options);
// Assert
var spaClient = Assert.Single(options.Clients, c => c.ClientId == "TestSPA");

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 IdentityServer4;
@ -58,13 +58,12 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
Assert.Equal("MyClient", client.ClientId);
Assert.Equal("MyClient", client.ClientName);
Assert.True(client.AllowAccessTokensViaBrowser);
Assert.Equal(new[] { "" }, client.RedirectUris.ToArray());
Assert.Equal(new[] { "" }, client.PostLogoutRedirectUris.ToArray());
Assert.Equal(new[] { "/authentication/login-callback" }, client.RedirectUris.ToArray());
Assert.Equal(new[] { "/authentication/logout-callback" }, client.PostLogoutRedirectUris.ToArray());
Assert.Empty(client.AllowedCorsOrigins);
Assert.False(client.RequireConsent);
Assert.Empty(client.ClientSecrets);
Assert.Equal(GrantTypes.Implicit.ToArray(), client.AllowedGrantTypes.ToArray());
//Assert.Equal(expectedScopes, client.AllowedScopes.ToArray());
}
[Fact]
@ -101,7 +100,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
Assert.Equal(GrantTypes.Code.ToArray(), client.AllowedGrantTypes.ToArray());
Assert.True(client.RequirePkce);
Assert.False(client.AllowPlainTextPkce);
//Assert.Equal(expectedScopes, client.AllowedScopes.ToArray());
}
[Fact]
@ -142,43 +140,6 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration
Assert.False(client.RequireConsent);
Assert.Empty(client.ClientSecrets);
Assert.Equal(GrantTypes.Implicit.ToArray(), client.AllowedGrantTypes.ToArray());
//Assert.Equal(expectedScopes, client.AllowedScopes.ToArray());
}
[Fact]
public void GetClients_ReadsWebAppFromConfiguration()
{
// Arrange
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>
{
["MyClient:Profile"] = "IdentityServerSPA"
}).Build();
var resources = Array.Empty<ApiResource>();
var expectedScopes = new[]
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
};
var clientLoader = new ConfigureClients(config, new TestLogger<ConfigureClients>());
// Act
var clients = clientLoader.GetClients();
// Assert
var client = Assert.Single(clients);
Assert.Equal("MyClient", client.ClientId);
Assert.Equal("MyClient", client.ClientName);
Assert.True(client.AllowAccessTokensViaBrowser);
Assert.Equal(new[] { "" }, client.RedirectUris.ToArray());
Assert.Equal(new[] { "" }, client.PostLogoutRedirectUris.ToArray());
Assert.Empty(client.AllowedCorsOrigins);
Assert.False(client.RequireConsent);
Assert.Empty(client.ClientSecrets);
Assert.Equal(GrantTypes.Implicit.ToArray(), client.AllowedGrantTypes.ToArray());
//Assert.Equal(expectedScopes, client.AllowedScopes.ToArray());
}
[Fact]
@ -209,8 +170,8 @@ var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<str
Assert.Equal("MyClient", client.ClientId);
Assert.Equal("MyClient", client.ClientName);
Assert.True(client.AllowAccessTokensViaBrowser);
Assert.Equal(new[] { "" }, client.RedirectUris.ToArray());
Assert.Equal(new[] { "" }, client.PostLogoutRedirectUris.ToArray());
Assert.Equal(new[] { "/authentication/login-callback" }, client.RedirectUris.ToArray());
Assert.Equal(new[] { "/authentication/logout-callback" }, client.PostLogoutRedirectUris.ToArray());
Assert.Empty(client.AllowedCorsOrigins);
Assert.False(client.RequireConsent);
Assert.Empty(client.ClientSecrets);

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Collections.Generic;
using System.Collections.Specialized;
using System.Security.Claims;
using System.Threading.Tasks;
@ -11,9 +12,11 @@ using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -79,7 +82,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
ValidatedRequest = new ValidatedEndSessionRequest()
{
Client = ClientBuilder.IdentityServerSPA("MySPA").Build(),
PostLogOutUri = "https://www.example.com/logout"
PostLogOutUri = "https://www.example.com/logout",
State = "appState"
}
});
@ -99,10 +103,11 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
// Assert
Assert.NotNull(response);
var redirect = Assert.IsType<AutoRedirectEndSessionEndpoint.RedirectResult>(response);
Assert.Equal("https://www.example.com/logout", redirect.Url);
Assert.Equal("https://www.example.com/logout?state=appState", redirect.Url);
await response.ExecuteAsync(ctx);
Assert.Equal(StatusCodes.Status302Found, ctx.Response.StatusCode);
Assert.Equal("https://www.example.com/logout", ctx.Response.Headers[HeaderNames.Location]);
Assert.Equal("https://www.example.com/logout?state=appState", ctx.Response.Headers[HeaderNames.Location]);
}
[Fact]

View File

@ -0,0 +1,60 @@
// 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 IdentityServer4.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Extensions
{
public class DefaultClientRequestParametersProviderTests
{
[Fact]
public void GetClientParameters_ReturnsParametersForExistingClients()
{
// Arrange
var absoluteUrlFactory = new Mock<IAbsoluteUrlFactory>();
absoluteUrlFactory.Setup(auf => auf.GetAbsoluteUrl(It.IsAny<HttpContext>(), It.IsAny<string>()))
.Returns<HttpContext, string>((_, s) => Uri.IsWellFormedUriString(s, UriKind.Absolute) ? s : new Uri(new Uri("http://localhost/"), s).ToString());
var options = Options.Create(new ApiAuthorizationOptions());
options.Value.Clients.AddIdentityServerSPA("SPA", cb =>
cb.WithScopes("a/b", "c/d")
.WithRedirectUri("authentication/login-callback")
.WithLogoutRedirectUri("authentication/logout-callback"));
var context = new DefaultHttpContext();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
context.RequestServices = new ServiceCollection()
.AddSingleton(new IdentityServerOptions())
.BuildServiceProvider();
var clientRequestParametersProvider =
new DefaultClientRequestParametersProvider(
absoluteUrlFactory.Object,
options);
var expectedParameters = new Dictionary<string, string>
{
["authority"] = "http://localhost",
["client_id"] = "SPA",
["redirect_uri"] = "http://localhost/authentication/login-callback",
["post_logout_redirect_uri"] = "http://localhost/authentication/logout-callback",
["response_type"] = "id_token token",
["scope"] = "a/b c/d"
};
// Act
var result = clientRequestParametersProvider.GetClientParameters(context, "SPA");
// Assert
Assert.Equal(expectedParameters, result);
}
}
}

File diff suppressed because it is too large Load Diff