Skip to content

Commit

Permalink
Merge pull request #1568 from DuendeSoftware/joe/merge-7.0.5-forward
Browse files Browse the repository at this point in the history
merge 7.0.5 forward into main
  • Loading branch information
brockallen authored Jun 3, 2024
2 parents d3d1a5a + a9cd0ff commit 954a0c1
Show file tree
Hide file tree
Showing 15 changed files with 300 additions and 23 deletions.
17 changes: 1 addition & 16 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,19 +1,4 @@
<Project>

<!--<PropertyGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<FrameworkVersion>6.0.0</FrameworkVersion>
<ExtensionsVersion>6.0.0</ExtensionsVersion>
<EntityFrameworkVersion>6.0.0</EntityFrameworkVersion>
<WilsonVersion>6.10.0</WilsonVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net7.0'">
<FrameworkVersion>7.0.0</FrameworkVersion>
<ExtensionsVersion>7.0.0</ExtensionsVersion>
<EntityFrameworkVersion>7.0.0</EntityFrameworkVersion>
<WilsonVersion>6.15.1</WilsonVersion>
</PropertyGroup>-->

<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<FrameworkVersion>8.0.3</FrameworkVersion>
<ExtensionsVersion>8.0.0</ExtensionsVersion>
Expand Down Expand Up @@ -79,7 +64,7 @@
<PackageReference Update="OpenTelemetry" Version="1.8.1" />
<PackageReference Update="OpenTelemetry.Exporter.Console" Version="1.8.1" />
<PackageReference Update="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Update="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.8.0-rc.1" />
<PackageReference Update="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-alpha.1" />
<PackageReference Update="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Update="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Update="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

using Duende.IdentityServer.Infrastructure;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
Expand All @@ -21,12 +24,29 @@ public ConfigureOpenIdConnectOptions(string[] schemes, IServiceProvider serviceP
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}

private static bool warnedInMemory = false;

public void PostConfigure(string name, OpenIdConnectOptions options)
{
// no schemes means configure them all
if (_schemes.Length == 0 || _schemes.Contains(name))
{
options.StateDataFormat = new DistributedCacheStateDataFormatter(_serviceProvider, name);
}

if (!warnedInMemory)
{
var distributedCacheService = _serviceProvider.GetRequiredService<IDistributedCache>();

if (distributedCacheService is MemoryDistributedCache)
{
var logger = _serviceProvider
.GetRequiredService<ILogger<ConfigureOpenIdConnectOptions>>();

logger.LogInformation("You have enabled the OidcStateDataFormatterCache but the distributed cache registered is the default memory based implementation. This will store any OIDC state in memory on the server that initiated the request. If the response is processed on another server it will fail. If you are running in production, you want to switch to a real distributed cache that is shared between all nodes.");

warnedInMemory = true;
}
}
}
}
6 changes: 6 additions & 0 deletions src/IdentityServer/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public static class SigningAlgorithms
/// </summary>
public const string ProcessedPrompt = "suppressed_" + OidcConstants.AuthorizeRequest.Prompt;

/// <summary>
/// The name of the parameter passed to the authorize callback to indicate
/// max age that have already been used.
/// </summary>
public const string ProcessedMaxAge = "suppressed_" + OidcConstants.AuthorizeRequest.MaxAge;

public static class KnownAcrValues
{
public const string HomeRealm = "idp:";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ public async Task WriteHttpResponse(AuthorizeInteractionPageResult result, HttpC
returnUrl = returnUrl
.AddQueryString(OidcConstants.AuthorizeRequest.RequestUri, requestUri)
.AddQueryString(OidcConstants.AuthorizeRequest.ClientId, result.Request.ClientId);
var processedPrompt = result.Request.Raw[Constants.ProcessedPrompt];
if (processedPrompt != null)
{
returnUrl = returnUrl.AddQueryString(Constants.ProcessedPrompt, processedPrompt);
}
var processedMaxAge = result.Request.Raw[Constants.ProcessedMaxAge];
if (processedMaxAge != null)
{
returnUrl = returnUrl.AddQueryString(Constants.ProcessedMaxAge, processedMaxAge);
}
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Collections.Specialized;
using System.Globalization;

#pragma warning disable 1591

Expand Down Expand Up @@ -49,6 +50,15 @@ public static void RemovePrompt(this ValidatedAuthorizeRequest request)
}).ToArray();
}

public static void RemoveMaxAge(this ValidatedAuthorizeRequest request)
{
if (request.MaxAge.HasValue)
{
request.Raw.Add(Constants.ProcessedMaxAge, request.MaxAge.Value.ToString(CultureInfo.InvariantCulture));
request.MaxAge = null;
}
}

public static string GetPrefixedAcrValue(this ValidatedAuthorizeRequest request, string prefix)
{
var value = request.AuthenticationContextReferenceClasses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,34 @@ protected internal virtual Task<InteractionResponse> ProcessCreateAccountAsync(V
protected internal virtual async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
{
using var activity = Tracing.BasicActivitySource.StartActivity("AuthorizeInteractionResponseGenerator.ProcessLogin");


// prompt=login, prompt=select_account, and max_age=0
//
// These parameters are all treated the same, and force a login.
// max_age=0 being equivalent to prompt=login is explicitly in the spec,
// and while selecting from multiple accounts would take a significant
// amount of work to implement, the user interaction would happen on the
// login page.
bool showLoginBecauseOfPrompt = false; // we need this flag because we want to check for (and suppress) either OR BOTH of the prompt and max_age params
if (request.PromptModes.Contains(OidcConstants.PromptModes.Login) ||
request.PromptModes.Contains(OidcConstants.PromptModes.SelectAccount))
{
Logger.LogInformation("Showing login: request contains prompt={0}", request.PromptModes.ToSpaceSeparatedString());

// remove prompt so when we redirect back in from login page
// we won't think we need to force a prompt again
request.RemovePrompt();

showLoginBecauseOfPrompt = true;
}
if (request.MaxAge == 0)
{
Logger.LogInformation("Showing login: request contains max_age=0.");
// remove max_age=0 so when we redirect back in from login page
// we won't think we need to force a prompt again
request.RemoveMaxAge();
showLoginBecauseOfPrompt = true;
}
if (showLoginBecauseOfPrompt)
{
return new InteractionResponse { IsLogin = true };
}

Expand Down
10 changes: 9 additions & 1 deletion src/IdentityServer/Services/Default/DefaultEventService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,18 @@ protected virtual bool CanRaiseEvent(Event evt)
/// <returns></returns>
protected virtual async Task PrepareEventAsync(Event evt)
{
evt.ActivityId = Context.HttpContext.TraceIdentifier;
evt.TimeStamp = Clock.UtcNow.DateTime;
evt.ProcessId = Process.GetCurrentProcess().Id;

if (Context.HttpContext?.TraceIdentifier != null)
{
evt.ActivityId = Context.HttpContext.TraceIdentifier;
}
else
{
evt.ActivityId = "unknown";
}

if (Context.HttpContext?.Connection.LocalIpAddress != null)
{
evt.LocalIpAddress = Context.HttpContext.Connection.LocalIpAddress.ToString() + ":" + Context.HttpContext.Connection.LocalPort;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,13 @@ private async Task<AuthorizeRequestValidationResult> ValidateOptionalParametersA
}
}

var processed_max_age = request.Raw.Get(Constants.ProcessedMaxAge);
if(processed_max_age.IsPresent())
{
request.MaxAge = null;
// TODO - Consider adding an OriginalMaxAge property for consistency with prompt.
}

//////////////////////////////////////////////////////////
// check login_hint
//////////////////////////////////////////////////////////
Expand Down
13 changes: 13 additions & 0 deletions src/IdentityServer/Validation/Default/RequestObjectValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,20 @@ private static bool IsParRequestUri(string requestUri)
// Record the reference value, so we can know that PAR did happen
request.PushedAuthorizationReferenceValue = GetReferenceValue(request);
// Copy the PAR into the raw request so that validation will use the pushed parameters
// But keep the query parameters we add that indicate that we have processed
// prompt and max_age, as those are not pushed
var processedPrompt = request.Raw[Constants.ProcessedPrompt];
var processedMaxAge = request.Raw[Constants.ProcessedMaxAge];

request.Raw = pushedAuthorizationRequest.PushedParameters;
if (processedPrompt != null)
{
request.Raw[Constants.ProcessedPrompt] = processedPrompt;
}
if (processedMaxAge != null)
{
request.Raw[Constants.ProcessedMaxAge] = processedMaxAge;
}

var bindingError = ValidatePushedAuthorizationBindingToClient(pushedAuthorizationRequest, request);
if (bindingError != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class IdentityServerPipeline
public const string LoginPage = BaseUrl + "/account/login";
public const string LogoutPage = BaseUrl + "/account/logout";
public const string ConsentPage = BaseUrl + "/account/consent";
public const string CreateAccountPage = BaseUrl + "/account/create";

public const string ErrorPage = BaseUrl + "/home/error";

public const string DeviceAuthorization = BaseUrl + "/connect/deviceauthorization";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,6 @@ public async Task code_flow_with_fragment_response_type_should_be_allowed()
_mockPipeline.LoginWasCalled.Should().BeTrue();
}


[Fact]
[Trait("Category", Category)]
public async Task prompt_login_should_show_login_page_and_preserve_prompt_values()
Expand All @@ -1259,7 +1258,27 @@ public async Task prompt_login_should_show_login_page_and_preserve_prompt_values
_mockPipeline.LoginWasCalled.Should().BeTrue();
_mockPipeline.LoginRequest.PromptModes.Should().Contain("login");
}


[Fact]
[Trait("Category", Category)]
public async Task max_age_0_should_show_login_page()
{
await _mockPipeline.LoginAsync("bob");

var url = _mockPipeline.CreateAuthorizeUrl(
clientId: "client3",
responseType: "id_token",
scope: "openid profile",
redirectUri: "https://client3/callback",
state: "123_state",
nonce: "123_nonce",
extra:new { max_age = "0" }
);
var response = await _mockPipeline.BrowserClient.GetAsync(url);

_mockPipeline.LoginWasCalled.Should().BeTrue();
}

[Fact]
[Trait("Category", Category)]
public async Task prompt_login_should_allow_user_to_login_and_complete_authorization()
Expand All @@ -1278,7 +1297,33 @@ public async Task prompt_login_should_allow_user_to_login_and_complete_authoriza

var response = await _mockPipeline.BrowserClient.GetAsync(url);

// this simulates the login page returning to the returnUrl whichi is the authorize callback page
// this simulates the login page returning to the returnUrl which is the authorize callback page
_mockPipeline.BrowserClient.AllowAutoRedirect = false;
response = await _mockPipeline.BrowserClient.GetAsync(IdentityServerPipeline.BaseUrl + _mockPipeline.LoginReturnUrl);
response.StatusCode.Should().Be(HttpStatusCode.Redirect);
response.Headers.Location.ToString().Should().StartWith("https://client1/callback");
response.Headers.Location.ToString().Should().Contain("id_token=");
}

[Fact]
[Trait("Category", Category)]
public async Task max_age_0_should_allow_user_to_login_and_complete_authorization()
{
await _mockPipeline.LoginAsync("bob");

var url = _mockPipeline.CreateAuthorizeUrl(
clientId: "client1",
responseType: "id_token",
scope: "openid profile",
redirectUri: "https://client1/callback",
state: "123_state",
nonce: "123_nonce",
extra: new { max_age = "0" }
);

var response = await _mockPipeline.BrowserClient.GetAsync(url);

// this simulates the login page returning to the returnUrl which is the authorize callback page
_mockPipeline.BrowserClient.AllowAutoRedirect = false;
response = await _mockPipeline.BrowserClient.GetAsync(IdentityServerPipeline.BaseUrl + _mockPipeline.LoginReturnUrl);
response.StatusCode.Should().Be(HttpStatusCode.Redirect);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using FluentAssertions;
using IdentityModel;
using IntegrationTests.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -190,6 +191,63 @@ public async Task pushed_authorization_with_a_request_uri_fails(string requestUr
.Should().Be(OidcConstants.AuthorizeErrors.InvalidRequest);
}


[Theory]
[InlineData("prompt", "login")]
[InlineData("prompt", "select_account")]
[InlineData("prompt", "create")]
[InlineData("max_age", "0")]
public async Task prompt_login_can_be_used_with_pushed_authorization(string parameterName, string parameterValue)
{
// Login before we start (we expect to still be prompted to login because of the prompt param)
_mockPipeline.Options.UserInteraction.CreateAccountUrl = IdentityServerPipeline.CreateAccountPage;
_mockPipeline.Options.UserInteraction.PromptValuesSupported.Add(OidcConstants.PromptModes.Create);
await _mockPipeline.LoginAsync("bob");
_mockPipeline.BrowserClient.AllowAutoRedirect = false;

// Push Authorization
var expectedCallback = _client.RedirectUris.First();
var expectedState = "123_state";
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(
redirectUri: expectedCallback,
state: expectedState,
extra: new Dictionary<string, string>
{
{ parameterName, parameterValue }
}
);
statusCode.Should().Be(HttpStatusCode.Created);

// Authorize using pushed request
var authorizeUrl = _mockPipeline.CreateAuthorizeUrl(
clientId: "client1",
extra: new
{
request_uri = parJson.RootElement.GetProperty("request_uri").GetString()
});
var authorizeResponse = await _mockPipeline.BrowserClient.GetAsync(authorizeUrl);

// Verify that authorize redirects to login
authorizeResponse.Should().Be302Found();
var isPromptCreate = parameterName == "prompt" && parameterValue == "create";
var expectedLocation = isPromptCreate ? IdentityServerPipeline.CreateAccountPage : IdentityServerPipeline.LoginPage;
authorizeResponse.Headers.Location.ToString().ToLower().Should().Match($"{expectedLocation.ToLower()}*");

// Verify that the UI prompts the user at this point
var uiResponse = await _mockPipeline.BrowserClient.GetAsync(authorizeResponse.Headers.Location);
uiResponse.Should().Be200Ok();

// Now login and return to the return url we were given
var returnPath = isPromptCreate ? _mockPipeline.CreateAccountReturnUrl : _mockPipeline.LoginReturnUrl;
var returnUrl = new Uri(new Uri(IdentityServerPipeline.BaseUrl), returnPath);
await _mockPipeline.LoginAsync("bob");
var authorizeCallbackResponse = await _mockPipeline.BrowserClient.GetAsync(returnUrl);

// The authorize callback should continue back to the application (the prompt parameter is processed so we don't go back to the UI)
authorizeCallbackResponse.Should().Be302Found();
authorizeCallbackResponse.Headers.Location.Should().Be(expectedCallback);
}

private void ConfigureScopesAndResources()
{
_mockPipeline.IdentityScopes.AddRange(new IdentityResource[] {
Expand Down
21 changes: 21 additions & 0 deletions test/IdentityServer.UnitTests/Common/MockEventSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.


using System.Collections.Generic;
using System.Threading.Tasks;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Services;

namespace UnitTests.Common;

internal class MockEventSink : IEventSink
{
public List<Event> Events { get; } = [];

public Task PersistAsync(Event evt)
{
Events.Add(evt);
return Task.CompletedTask;
}
}
Loading

0 comments on commit 954a0c1

Please sign in to comment.