diff --git a/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs b/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs index a1cb98566..352161ea6 100644 --- a/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs +++ b/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs @@ -5,6 +5,7 @@ #nullable enable using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.ResponseHandling; using System.Collections.Generic; namespace Duende.IdentityServer.Configuration; @@ -132,8 +133,13 @@ public class UserInteractionOptions public bool AllowOriginInReturnUrl { get; set; } /// - /// The collection of OIDC prompt modes supported and that will be published in discovery. - /// The value "create" is omitted unless the CreateAccountUrl value is set. + /// The collection of OIDC prompt modes supported and that will be published + /// in discovery. By default, this includes all values in . If the option is set, then the "create" value is also + /// included. If additional prompt values are added, a customized is also required to + /// handle those values. /// - internal ICollection PromptValuesSupported { get; set; } = new HashSet(Constants.SupportedPromptModes); + public ICollection PromptValuesSupported { get; set; } = new HashSet(Constants.SupportedPromptModes); } diff --git a/test/IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs b/test/IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs index 0b7a8f93a..f79c14b97 100644 --- a/test/IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs +++ b/test/IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs @@ -1485,14 +1485,72 @@ public async Task custom_request_should_have_authorization_params(Type storeType _mockPipeline.CustomRequest.Parameters.AllKeys.Should().Contain("foo"); _mockPipeline.CustomRequest.Parameters["foo"].Should().Be("bar"); } + + [Fact] + public async Task custom_prompt_values_should_raise_error_with_default_interaction_service() + { + _mockPipeline.Options.UserInteraction.PromptValuesSupported.Add("custom-prompt"); + 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 { prompt = "custom-prompt" } + ); + + _mockPipeline.BrowserClient.AllowAutoRedirect = false; + + Func a = () => _mockPipeline.BrowserClient.GetAsync(url); + await a.Should().ThrowAsync(); + } + + [Fact] + public async Task custom_prompt_value_should_be_passed_to_custom_interaction_service() + { + var mockAuthzInteractionService = new MockAuthzInteractionService(); + mockAuthzInteractionService.Response.RedirectUrl = "/custom"; + _mockPipeline.OnPostConfigureServices += services => + { + services.AddTransient(typeof(IAuthorizeInteractionResponseGenerator), svc => mockAuthzInteractionService); + }; + _mockPipeline.Initialize(); + + _mockPipeline.Options.UserInteraction.PromptValuesSupported.Add("custom-prompt"); + + 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 { prompt = "custom-prompt" } + ); + + _mockPipeline.BrowserClient.AllowAutoRedirect = false; + + var response = await _mockPipeline.BrowserClient.GetAsync(url); + response.Headers.Location.GetLeftPart(UriPartial.Path).Should().Be("https://server/custom"); + mockAuthzInteractionService.Request.PromptModes.Should() + .Contain("custom-prompt").And + .HaveCount(1); + } } public class MockAuthzInteractionService : IAuthorizeInteractionResponseGenerator { public InteractionResponse Response { get; set; } = new InteractionResponse(); + public ValidatedAuthorizeRequest Request { get; internal set; } public Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null) { + Request = request; return Task.FromResult(Response); } } \ No newline at end of file