diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/UserInteractionOptions.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
index 3d6ba568d..c76d66a3e 100644
--- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
+++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
@@ -1,8 +1,10 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Open.IdentityServer.Extensions;
+using System.Collections.Generic;
namespace Open.IdentityServer.Configuration;
@@ -106,4 +108,28 @@ public class UserInteractionOptions
/// The device verification user code parameter.
///
public string DeviceVerificationUserCodeParameter { get; set; } = Constants.UIConstants.DefaultRoutePathParams.UserCode;
+
+ ///
+ /// Gets or sets the create account URL. If a local URL, the value must start with a leading slash.
+ ///
+ ///
+ /// The create account URL.
+ ///
+ public string CreateAccountUrl { get; set; }
+
+ ///
+ /// Gets or sets the create account return URL parameter.
+ ///
+ ///
+ /// The create account return URL parameter.
+ ///
+ public string CreateAccountReturnUrlParameter { get; set; } = Constants.UIConstants.DefaultRoutePathParams.CreateAccount;
+
+ ///
+ /// Gets or sets the supported prompt modes.
+ ///
+ ///
+ /// The supported prompt modes.
+ ///
+ public List SupportedPromptModes { get; set; } = Constants.SupportedPromptModes;
}
\ No newline at end of file
diff --git a/src/Open.IdentityServer/src/Configuration/IdentityServerApplicationBuilderExtensions.cs b/src/Open.IdentityServer/src/Configuration/IdentityServerApplicationBuilderExtensions.cs
index a953d5587..348d04ebd 100644
--- a/src/Open.IdentityServer/src/Configuration/IdentityServerApplicationBuilderExtensions.cs
+++ b/src/Open.IdentityServer/src/Configuration/IdentityServerApplicationBuilderExtensions.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
@@ -12,6 +13,7 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
+using Open.IdentityServer;
namespace Microsoft.AspNetCore.Builder;
@@ -132,6 +134,12 @@ private static void ValidateOptions(IdentityServerOptions options, ILogger logge
if (options.UserInteraction.ConsentReturnUrlParameter.IsMissing()) throw new InvalidOperationException("ConsentReturnUrlParameter is not configured");
if (options.UserInteraction.CustomRedirectReturnUrlParameter.IsMissing()) throw new InvalidOperationException("CustomRedirectReturnUrlParameter is not configured");
+ if (options.UserInteraction.CreateAccountUrl.IsPresent())
+ {
+ if (options.UserInteraction.CreateAccountReturnUrlParameter.IsMissing()) throw new InvalidOperationException("CreateAccountReturnUrlParameter is not configured");
+ options.UserInteraction.SupportedPromptModes.Add(OidcConstants.PromptModes.Create);
+ }
+
if (options.Authentication.CheckSessionCookieName.IsMissing()) throw new InvalidOperationException("CheckSessionCookieName is not configured");
if (options.Cors.CorsPolicyName.IsMissing()) throw new InvalidOperationException("CorsPolicyName is not configured");
diff --git a/src/Open.IdentityServer/src/Constants.cs b/src/Open.IdentityServer/src/Constants.cs
index 487ef5540..2409bf64e 100644
--- a/src/Open.IdentityServer/src/Constants.cs
+++ b/src/Open.IdentityServer/src/Constants.cs
@@ -113,6 +113,9 @@ public static class SigningAlgorithms
OidcConstants.PromptModes.SelectAccount
};
+ public const string PromptProcessed = OidcConstants.AuthorizeRequest.Prompt + "_processed";
+ public const string MaxAgeProcessed = OidcConstants.AuthorizeRequest.MaxAge + "_processed";
+
public static class KnownAcrValues
{
public const string HomeRealm = "idp:";
@@ -177,6 +180,7 @@ public static class DefaultRoutePathParams
{
public const string Error = "errorId";
public const string Login = "returnUrl";
+ public const string CreateAccount = "returnUrl";
public const string Consent = "returnUrl";
public const string Logout = "logoutId";
public const string EndSessionCallback = "endSessionId";
diff --git a/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs
index 8555cd39a..28998eea4 100644
--- a/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs
+++ b/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs
@@ -76,6 +76,9 @@ public override async Task ProcessAsync(HttpContext context)
try
{
+ parameters.Add(Constants.PromptProcessed, "true");
+ parameters.Add(Constants.MaxAgeProcessed, "true");
+
var result = await ProcessAuthorizeRequestAsync(parameters, user, consent?.Data);
Logger.LogTrace("End Authorize Request. Result type: {0}", result?.GetType().ToString() ?? "-none-");
diff --git a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs
index 9e795e041..0c6fb500f 100644
--- a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs
+++ b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs
@@ -95,6 +95,10 @@ internal async Task ProcessAuthorizeRequestAsync(NameValueColle
{
return new LoginPageResult(request);
}
+ if (interactionResult.IsCreateAccount)
+ {
+ return new CreateAccountPageResult(request);
+ }
if (interactionResult.IsConsent)
{
return new ConsentPageResult(request);
diff --git a/src/Open.IdentityServer/src/Endpoints/Results/CreateAccountPageResult.cs b/src/Open.IdentityServer/src/Endpoints/Results/CreateAccountPageResult.cs
new file mode 100644
index 000000000..95eb9f559
--- /dev/null
+++ b/src/Open.IdentityServer/src/Endpoints/Results/CreateAccountPageResult.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Rock Solid Knowledge Ltd. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Threading.Tasks;
+using Open.IdentityServer.Validation;
+using Open.IdentityServer.Extensions;
+using Open.IdentityServer.Configuration;
+using Open.IdentityServer.Stores;
+using Microsoft.AspNetCore.Http;
+
+namespace Open.IdentityServer.Endpoints.Results;
+
+///
+/// Result for login page
+///
+///
+public class CreateAccountPageResult : ReturnUrlResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The request.
+ /// request
+ public CreateAccountPageResult(ValidatedAuthorizeRequest request):
+ base(request) { }
+
+ internal CreateAccountPageResult(
+ ValidatedAuthorizeRequest request,
+ IdentityServerOptions options,
+ IAuthorizationParametersMessageStore authorizationParametersMessageStore = null):
+ base(request, options, authorizationParametersMessageStore) { }
+
+ ///
+ /// Executes the result.
+ ///
+ /// The HTTP context.
+ public override async Task ExecuteAsync(HttpContext context)
+ {
+ Init(context);
+ var createUrl = Options.UserInteraction.CreateAccountUrl;
+ var returnUrl = await BuildReturnUrl(context, createUrl.IsLocalUrl());
+
+ var url = createUrl.AddQueryString(Options.UserInteraction.CreateAccountReturnUrlParameter, returnUrl);
+ context.Response.RedirectToAbsoluteUrl(url);
+ }
+}
\ No newline at end of file
diff --git a/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs b/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs
index 31f86870b..d12d60a53 100644
--- a/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs
+++ b/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs
@@ -7,10 +7,13 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
+#nullable enable
+
namespace Open.IdentityServer.Extensions;
internal static class StringExtensions
@@ -47,7 +50,7 @@ public static IEnumerable FromSpaceSeparatedString(this string input)
return input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
- public static List ParseScopesString(this string scopes)
+ public static List? ParseScopesString(this string? scopes)
{
if (scopes.IsMissing())
{
@@ -67,13 +70,13 @@ public static List ParseScopesString(this string scopes)
}
[DebuggerStepThrough]
- public static bool IsMissing(this string value)
+ public static bool IsMissing([NotNullWhen(false)] this string? value)
{
return string.IsNullOrWhiteSpace(value);
}
[DebuggerStepThrough]
- public static bool IsMissingOrTooLong(this string value, int maxLength)
+ public static bool IsMissingOrTooLong(this string? value, int maxLength)
{
if (string.IsNullOrWhiteSpace(value))
{
@@ -89,13 +92,13 @@ public static bool IsMissingOrTooLong(this string value, int maxLength)
}
[DebuggerStepThrough]
- public static bool IsPresent(this string value)
+ public static bool IsPresent([NotNullWhen(true)] this string? value)
{
return !string.IsNullOrWhiteSpace(value);
}
[DebuggerStepThrough]
- public static string EnsureLeadingSlash(this string url)
+ public static string? EnsureLeadingSlash(this string? url)
{
if (url != null && !url.StartsWith("/"))
{
@@ -106,7 +109,7 @@ public static string EnsureLeadingSlash(this string url)
}
[DebuggerStepThrough]
- public static string EnsureTrailingSlash(this string url)
+ public static string? EnsureTrailingSlash(this string? url)
{
if (url != null && !url.EndsWith("/"))
{
@@ -117,7 +120,7 @@ public static string EnsureTrailingSlash(this string url)
}
[DebuggerStepThrough]
- public static string RemoveLeadingSlash(this string url)
+ public static string? RemoveLeadingSlash(this string? url)
{
if (url != null && url.StartsWith("/"))
{
@@ -128,7 +131,7 @@ public static string RemoveLeadingSlash(this string url)
}
[DebuggerStepThrough]
- public static string RemoveTrailingSlash(this string url)
+ public static string? RemoveTrailingSlash(this string? url)
{
if (url != null && url.EndsWith("/"))
{
@@ -139,9 +142,9 @@ public static string RemoveTrailingSlash(this string url)
}
[DebuggerStepThrough]
- public static string CleanUrlPath(this string url)
+ public static string CleanUrlPath(this string? url)
{
- if (String.IsNullOrWhiteSpace(url)) url = "/";
+ if (string.IsNullOrWhiteSpace(url)) url = "/";
if (url != "/" && url.EndsWith("/"))
{
@@ -153,7 +156,7 @@ public static string CleanUrlPath(this string url)
[DebuggerStepThrough]
// Clone of UrlHelperBase.CheckIsLocalUrl from https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs
- public static bool IsLocalUrl(this string url)
+ public static bool IsLocalUrl(this string? url)
{
if (string.IsNullOrEmpty(url))
{
@@ -246,7 +249,7 @@ public static string AddHashFragment(this string url, string query)
}
[DebuggerStepThrough]
- public static NameValueCollection ReadQueryStringAsNameValueCollection(this string url)
+ public static NameValueCollection ReadQueryStringAsNameValueCollection(this string? url)
{
if (url != null)
{
@@ -266,7 +269,7 @@ public static NameValueCollection ReadQueryStringAsNameValueCollection(this stri
return new NameValueCollection();
}
- public static string GetOrigin(this string url)
+ public static string? GetOrigin(this string? url)
{
if (url != null)
{
diff --git a/src/Open.IdentityServer/src/Extensions/ValidatedAuthorizeRequestExtensions.cs b/src/Open.IdentityServer/src/Extensions/ValidatedAuthorizeRequestExtensions.cs
index 4d64fc3a5..91ee50cbe 100644
--- a/src/Open.IdentityServer/src/Extensions/ValidatedAuthorizeRequestExtensions.cs
+++ b/src/Open.IdentityServer/src/Extensions/ValidatedAuthorizeRequestExtensions.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
@@ -18,16 +19,6 @@ namespace Open.IdentityServer.Validation;
///
public static class ValidatedAuthorizeRequestExtensions
{
- ///
- /// Removes the prompt parameter from the request.
- ///
- /// The validated authorize request.
- public static void RemovePrompt(this ValidatedAuthorizeRequest request)
- {
- request.PromptModes = Enumerable.Empty();
- request.Raw.Remove(OidcConstants.AuthorizeRequest.Prompt);
- }
-
///
/// Gets the first ACR value that starts with the specified prefix, with the prefix removed.
///
diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs
index 6d79f657c..3c8457dd4 100644
--- a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs
+++ b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs
@@ -39,7 +39,7 @@ public class AuthorizeInteractionResponseGenerator : IAuthorizeInteractionRespon
/// The clock
///
protected readonly TimeProvider Clock;
-
+
///
/// The telemetry
///
@@ -56,7 +56,7 @@ public class AuthorizeInteractionResponseGenerator : IAuthorizeInteractionRespon
public AuthorizeInteractionResponseGenerator(
TimeProvider clock,
ILogger logger,
- IConsentService consent,
+ IConsentService consent,
IProfileService profile,
ITelemetryService telemetry)
{
@@ -64,7 +64,7 @@ public AuthorizeInteractionResponseGenerator(
Logger = logger;
Consent = consent;
Profile = profile;
- Telemetry = telemetry;
+ Telemetry = telemetry;
}
///
@@ -78,8 +78,8 @@ public virtual async Task ProcessInteractionAsync(Validated
using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this);
Logger.LogTrace("ProcessInteractionAsync");
- if (consent != null &&
- consent.Granted == false &&
+ if (consent != null &&
+ consent.Granted == false &&
consent.Error.HasValue)
{
// special case when anonymous user has issued an error prior to authenticating
@@ -93,7 +93,7 @@ public virtual async Task ProcessInteractionAsync(Validated
AuthorizationError.LoginRequired => OidcConstants.AuthorizeErrors.LoginRequired,
_ => OidcConstants.AuthorizeErrors.AccessDenied
};
-
+
return new InteractionResponse
{
Error = error,
@@ -101,11 +101,15 @@ public virtual async Task ProcessInteractionAsync(Validated
};
}
- var result = await ProcessLoginAsync(request);
-
- if (!result.IsLogin && !result.IsError && !result.IsRedirect)
+ var result = await ProcessCreateAsync(request);
+ if (!result.IsCreateAccount && !result.IsError && !result.IsRedirect)
{
- result = await ProcessConsentAsync(request, consent);
+ result = await ProcessLoginAsync(request);
+
+ if (!result.IsLogin && !result.IsError && !result.IsRedirect)
+ {
+ result = await ProcessConsentAsync(request, consent);
+ }
}
if ((result.IsLogin || result.IsConsent || result.IsRedirect) && request.PromptModes.Contains(OidcConstants.PromptModes.None))
@@ -115,7 +119,7 @@ public virtual async Task ProcessInteractionAsync(Validated
result = new InteractionResponse
{
Error = result.IsLogin ? OidcConstants.AuthorizeErrors.LoginRequired :
- result.IsConsent ? OidcConstants.AuthorizeErrors.ConsentRequired :
+ result.IsConsent ? OidcConstants.AuthorizeErrors.ConsentRequired :
OidcConstants.AuthorizeErrors.InteractionRequired
};
}
@@ -135,16 +139,12 @@ protected internal virtual async Task ProcessLoginAsync(Val
{
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();
-
return new InteractionResponse { IsLogin = true };
}
// unauthenticated user
var isAuthenticated = request.Subject.IsAuthenticated();
-
+
// user de-activated
bool isActive = false;
@@ -152,7 +152,7 @@ protected internal virtual async Task ProcessLoginAsync(Val
{
var isActiveCtx = new IsActiveContext(request.Subject, request.Client, IdentityServerConstants.ProfileIsActiveCallers.AuthorizeEndpoint);
await Profile.IsActiveAsync(isActiveCtx);
-
+
isActive = isActiveCtx.IsActive;
}
@@ -206,7 +206,7 @@ protected internal virtual async Task ProcessLoginAsync(Val
}
}
// check external idp restrictions if user not using local idp
- else if (request.Client.IdentityProviderRestrictions != null &&
+ else if (request.Client.IdentityProviderRestrictions != null &&
request.Client.IdentityProviderRestrictions.Any() &&
!request.Client.IdentityProviderRestrictions.Contains(currentIdp))
{
@@ -231,6 +231,28 @@ protected internal virtual async Task ProcessLoginAsync(Val
return new InteractionResponse();
}
+ ///
+ /// Processes the create account logic.
+ ///
+ /// The request.
+ /// A task that resolves to an indicating whether the create account screen should be shown.
+ /// is .
+ protected internal virtual Task ProcessCreateAsync(ValidatedAuthorizeRequest request)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+
+ var response = new InteractionResponse();
+
+ if (request.PromptModes.Contains(OidcConstants.PromptModes.Create))
+ {
+ Logger.LogInformation("Showing create account: request contains prompt=create");
+
+ response.IsCreateAccount = true;
+ }
+
+ return Task.FromResult(response);
+ }
+
///
/// Processes the consent logic.
///
@@ -294,7 +316,7 @@ protected internal virtual async Task ProcessConsentAsync(V
AuthorizationError.LoginRequired => OidcConstants.AuthorizeErrors.LoginRequired,
_ => OidcConstants.AuthorizeErrors.AccessDenied
};
-
+
response.Error = error;
response.ErrorDescription = consent.ErrorDescription;
}
diff --git a/src/Open.IdentityServer/src/ResponseHandling/Models/InteractionResponse.cs b/src/Open.IdentityServer/src/ResponseHandling/Models/InteractionResponse.cs
index 67eac3bf7..592a7ace1 100644
--- a/src/Open.IdentityServer/src/ResponseHandling/Models/InteractionResponse.cs
+++ b/src/Open.IdentityServer/src/ResponseHandling/Models/InteractionResponse.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
@@ -19,6 +20,14 @@ public class InteractionResponse
///
public bool IsLogin { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the user should create an account.
+ ///
+ ///
+ /// true if this instance is create; otherwise, false.
+ ///
+ public bool IsCreateAccount { get; set; }
+
///
/// Gets or sets a value indicating whether the user must consent.
///
diff --git a/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs
index b098abe5a..c778de650 100644
--- a/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs
+++ b/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs
@@ -727,20 +727,32 @@ private async Task ValidateOptionalParametersA
var prompt = request.Raw.Get(OidcConstants.AuthorizeRequest.Prompt);
if (prompt.IsPresent())
{
- var prompts = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries);
- if (prompts.All(p => Constants.SupportedPromptModes.Contains(p)))
+ var promptProcessed = request.Raw.Get(Constants.PromptProcessed);
+
+ if (!promptProcessed.IsPresent())
{
- if (prompts.Contains(OidcConstants.PromptModes.None) && prompts.Length > 1)
+ var prompts = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (prompts.All(p => _options.UserInteraction.SupportedPromptModes.Contains(p)))
{
- LogError("prompt contains 'none' and other values. 'none' should be used by itself.", request);
+ if (prompts.Contains(OidcConstants.PromptModes.None) && prompts.Length > 1)
+ {
+ LogError("prompt contains 'none' and other values. 'none' should be used by itself.", request);
+ return Invalid(request, description: "Invalid prompt");
+ }
+
+ if (prompts.Contains(OidcConstants.PromptModes.Create) && prompts.Length > 1)
+ {
+ LogError("prompt contains 'create' and other values. 'create' should be used by itself.", request);
+ return Invalid(request, description: "Invalid prompt");
+ }
+
+ request.PromptModes = prompts;
+ }
+ else
+ {
+ LogError("prompt contains unsupported values " + prompt, request);
return Invalid(request, description: "Invalid prompt");
}
-
- request.PromptModes = prompts;
- }
- else
- {
- _logger.LogDebug("Unsupported prompt mode - ignored: " + prompt);
}
}
@@ -779,11 +791,21 @@ private async Task ValidateOptionalParametersA
var maxAge = request.Raw.Get(OidcConstants.AuthorizeRequest.MaxAge);
if (maxAge.IsPresent())
{
- if (int.TryParse(maxAge, out var seconds))
+ var maxAgeProcessed = request.Raw.Get(Constants.MaxAgeProcessed);
+
+ if (!maxAgeProcessed.IsPresent())
{
- if (seconds >= 0)
+ if (int.TryParse(maxAge, out var seconds))
{
- request.MaxAge = seconds;
+ if (seconds >= 0)
+ {
+ request.MaxAge = seconds;
+ }
+ else
+ {
+ LogError("Invalid max_age.", request);
+ return Invalid(request, description: "Invalid max_age");
+ }
}
else
{
@@ -791,11 +813,6 @@ private async Task ValidateOptionalParametersA
return Invalid(request, description: "Invalid max_age");
}
}
- else
- {
- LogError("Invalid max_age.", request);
- return Invalid(request, description: "Invalid max_age");
- }
}
//////////////////////////////////////////////////////////
diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Common/IdentityServerPipeline.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Common/IdentityServerPipeline.cs
index f03d6b190..8577ce422 100644
--- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Common/IdentityServerPipeline.cs
+++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Common/IdentityServerPipeline.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
#nullable enable
@@ -36,6 +37,8 @@ public class IdentityServerPipeline
public const string LoginPage = BaseUrl + "/account/login";
public const string ConsentPage = BaseUrl + "/account/consent";
public const string ErrorPage = BaseUrl + "/home/error";
+ public const string CreatePageRelative = "/account/create";
+ public const string CreatePage = BaseUrl + CreatePageRelative;
public const string DeviceAuthorization = BaseUrl + "/connect/deviceauthorization";
public const string DiscoveryEndpoint = BaseUrl + "/.well-known/openid-configuration";
@@ -182,11 +185,16 @@ public void ConfigureApp(IApplicationBuilder app)
{
path.Run(ctx => OnError(ctx));
});
+ app.Map(CreatePageRelative, path =>
+ {
+ path.Run(ctx => OnCreate(ctx));
+ });
OnPostConfigure(app);
}
public bool LoginWasCalled { get; set; }
+ public string? LoginReturnUrl { get; set; }
public AuthorizationRequest? LoginRequest { get; set; }
public ClaimsPrincipal? Subject { get; set; }
public bool FollowLoginReturnUrl { get; set; }
@@ -201,7 +209,8 @@ private async Task OnLogin(HttpContext ctx)
private async Task ReadLoginRequest(HttpContext ctx)
{
var interaction = ctx.RequestServices.GetRequiredService();
- LoginRequest = await interaction.GetAuthorizationContextAsync(ctx.Request.Query["returnUrl"].FirstOrDefault());
+ LoginReturnUrl = ctx.Request.Query["returnUrl"].FirstOrDefault();
+ LoginRequest = await interaction.GetAuthorizationContextAsync(LoginReturnUrl);
}
private async Task IssueLoginCookie(HttpContext ctx)
@@ -277,6 +286,21 @@ private async Task OnError(HttpContext ctx)
await ReadErrorMessage(ctx);
}
+ public bool CreateWasCalled { get; set; }
+ public AuthorizationRequest? CreateRequest { get; set; }
+
+ private async Task OnCreate(HttpContext ctx)
+ {
+ CreateWasCalled = true;
+ await ReadCreateMessage(ctx);
+ }
+
+ private async Task ReadCreateMessage(HttpContext ctx)
+ {
+ var interaction = ctx.RequestServices.GetRequiredService();
+ CreateRequest = await interaction.GetAuthorizationContextAsync(ctx.Request.Query["returnUrl"].FirstOrDefault());
+ }
+
private async Task ReadErrorMessage(HttpContext ctx)
{
var interaction = ctx.RequestServices.GetRequiredService();
diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs
index 15320f0b0..8217f541f 100644
--- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs
+++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/AuthorizeTests.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
@@ -18,6 +19,7 @@
using Open.IdentityServer.Test;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
+using Open.IdentityServer.Configuration;
namespace IdentityServer.IntegrationTests.Endpoints.Authorize;
@@ -1166,6 +1168,89 @@ public async Task code_flow_with_fragment_response_type_should_be_allowed()
_mockPipeline.LoginWasCalled.Should().BeTrue();
}
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task unsupported_prompt_should_return_error()
+ {
+ var url = _mockPipeline.CreateAuthorizeUrl(
+ clientId: "client1",
+ responseType: "id_token",
+ scope: "openid profile",
+ redirectUri: "https://client1/callback",
+ state: "123_state",
+ nonce: "123_nonce",
+ extra: new Parameters
+ {
+ { "prompt", "unsupported" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.ErrorWasCalled.Should().BeTrue();
+ }
+
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task prompt_create_and_login_should_return_error()
+ {
+ _mockPipeline.OnPreConfigureServices += services =>
+ {
+ services.PostConfigure(options =>
+ {
+ options.UserInteraction.SupportedPromptModes.Add(OidcConstants.PromptModes.Create);
+ });
+ };
+ _mockPipeline.Initialize();
+
+ 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 Parameters
+ {
+ { "prompt", "create login" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.ErrorWasCalled.Should().BeTrue();
+ }
+
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task prompt_create_should_show_create_account_page()
+ {
+ _mockPipeline.OnPreConfigureServices += services =>
+ {
+ services.PostConfigure(options =>
+ {
+ options.UserInteraction.CreateAccountUrl = IdentityServerPipeline.CreatePageRelative;
+ });
+ };
+ _mockPipeline.Initialize();
+
+ var url = _mockPipeline.CreateAuthorizeUrl(
+ clientId: "client1",
+ responseType: "id_token",
+ scope: "openid profile",
+ redirectUri: "https://client1/callback",
+ state: "123_state",
+ nonce: "123_nonce",
+ extra: new Parameters
+ {
+ { "prompt", "create" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.CreateWasCalled.Should().BeTrue();
+ _mockPipeline.CreateRequest.PromptModes.Should().Contain("create");
+ }
[Fact]
[Trait("Category", Category)]
@@ -1174,19 +1259,98 @@ public async Task prompt_login_should_show_login_page()
await _mockPipeline.LoginAsync("bob");
var url = _mockPipeline.CreateAuthorizeUrl(
- clientId: "client3",
+ clientId: "client1",
responseType: "id_token",
scope: "openid profile",
- redirectUri: "https://client3/callback",
+ redirectUri: "https://client1/callback",
state: "123_state",
nonce: "123_nonce",
extra: new Parameters
{
- { "popup", "login" },
+ { "prompt", "login" },
}
);
await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
_mockPipeline.LoginWasCalled.Should().BeTrue();
+ _mockPipeline.LoginRequest.PromptModes.Should().Contain("login");
+ }
+
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task prompt_login_should_allow_user_to_login_and_return()
+ {
+ 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 Parameters
+ {
+ { "prompt", "login" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.BrowserClient.AllowAutoRedirect = false;
+ var 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_show_login_page()
+ {
+ 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 Parameters
+ {
+ { "max_age", "0" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.LoginWasCalled.Should().BeTrue();
+ _mockPipeline.LoginRequest.Parameters.Get(OidcConstants.AuthorizeRequest.MaxAge).Should().Be("0");
+ }
+
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task max_age_0_should_allow_user_to_login_and_return()
+ {
+ 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 Parameters
+ {
+ { "max_age", "0" },
+ }
+ );
+ await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _mockPipeline.BrowserClient.AllowAutoRedirect = false;
+ var 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=");
}
}
\ No newline at end of file
diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs
index 9dc34351f..53dc0223e 100644
--- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs
+++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs
@@ -2,25 +2,26 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Security.Claims;
-using System.Security.Cryptography.X509Certificates;
-using System.Text.Json;
-using System.Threading.Tasks;
using AwesomeAssertions;
using IdentityServer.IntegrationTests.Common;
using IdentityServer.IntegrationTests.Utility;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Logging;
+using Microsoft.IdentityModel.Tokens;
using Open.IdentityServer;
using Open.IdentityServer.Configuration;
using Open.IdentityServer.Models;
using Open.IdentityServer.Test;
-using Microsoft.IdentityModel.JsonWebTokens;
-using Microsoft.IdentityModel.Logging;
-using Microsoft.IdentityModel.Tokens;
using Open.IdentityServer.Utility;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.Json;
+using System.Threading.Tasks;
using Xunit;
namespace IdentityServer.IntegrationTests.Endpoints.Authorize;
@@ -1169,4 +1170,44 @@ public async Task both_request_and_request_uri_params_should_fail()
_mockPipeline.JwtRequestMessageHandler.InvokeWasCalled.Should().BeFalse();
}
+
+ [Fact]
+ [Trait("Category", Category)]
+ public async Task prompt_login_should_allow_user_to_login_and_return()
+ {
+ _mockPipeline.Options.Endpoints.EnableJwtRequestUri = true;
+
+ var requestJwt = CreateRequestJwt(
+ issuer: _client.ClientId,
+ audience: IdentityServerPipeline.BaseUrl,
+ credential: new X509SigningCredentials(TestCert.Load()),
+ claims:
+ [
+ new Claim("client_id", _client.ClientId),
+ new Claim("response_type", "id_token"),
+ new Claim("scope", "openid profile"),
+ new Claim("state", "123state"),
+ new Claim("nonce", "123nonce"),
+ new Claim("redirect_uri", "https://client/callback"),
+ new Claim("prompt", "login")
+ ]);
+ _mockPipeline.JwtRequestMessageHandler.Response.Content = new StringContent(requestJwt);
+
+ await _mockPipeline.LoginAsync("bob");
+
+ var url = _mockPipeline.CreateAuthorizeUrl(
+ clientId: _client.ClientId,
+ responseType: "id_token",
+ extra: new Parameters
+ {
+ { "request", requestJwt }
+ });
+ var response = await _mockPipeline.BrowserClient.GetAsync(url, TestContext.Current.CancellationToken);
+
+ _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://client/callback");
+ response.Headers.Location.ToString().Should().Contain("id_token=");
+ }
}
\ No newline at end of file
diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs
index 06fc17f4d..c3fce0580 100644
--- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs
+++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs
@@ -1,4 +1,5 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
#nullable enable
@@ -11,13 +12,13 @@ namespace IdentityServer.IntegrationTests.Utility;
internal static class InternalStringExtensions
{
[DebuggerStepThrough]
- public static bool IsMissing(this string value)
+ public static bool IsMissing([NotNullWhen(false)] this string? value)
{
return string.IsNullOrWhiteSpace(value);
}
[DebuggerStepThrough]
- public static bool IsPresent(this string value)
+ public static bool IsPresent([NotNullWhen(true)] this string? value)
{
return !(value.IsMissing());
}
diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs
index 8a9dbdb93..3f5523515 100644
--- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs
+++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs
@@ -258,13 +258,13 @@ public async Task prompt_select_account_should_sign_in()
}
[Fact]
- public async Task prompt_for_signin_should_remove_prompt_from_raw_url()
+ public async Task prompt_for_signin_should_not_remove_prompt_from_raw_url()
{
var request = new ValidatedAuthorizeRequest
{
ClientId = "foo",
Subject = new IdentityServerUser("123").CreatePrincipal(),
- PromptModes = new[] { OidcConstants.PromptModes.Login },
+ PromptModes = [OidcConstants.PromptModes.Login],
Raw = new NameValueCollection
{
{ OidcConstants.AuthorizeRequest.Prompt, OidcConstants.PromptModes.Login }
@@ -273,6 +273,6 @@ public async Task prompt_for_signin_should_remove_prompt_from_raw_url()
var result = await _subject.ProcessLoginAsync(request);
- request.Raw.AllKeys.Should().NotContain(OidcConstants.AuthorizeRequest.Prompt);
+ request.Raw.AllKeys.Should().Contain(OidcConstants.AuthorizeRequest.Prompt);
}
}
\ No newline at end of file
diff --git a/src/Storage/src/Extensions/StringsExtensions.cs b/src/Storage/src/Extensions/StringsExtensions.cs
index 4aec2bf9b..8c46339be 100644
--- a/src/Storage/src/Extensions/StringsExtensions.cs
+++ b/src/Storage/src/Extensions/StringsExtensions.cs
@@ -1,21 +1,25 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
namespace Open.IdentityServer.Extensions;
internal static class StringExtensions
{
[DebuggerStepThrough]
- public static bool IsMissing(this string value)
+ public static bool IsMissing([NotNullWhen(false)] this string? value)
{
return string.IsNullOrWhiteSpace(value);
}
[DebuggerStepThrough]
- public static bool IsPresent(this string value)
+ public static bool IsPresent([NotNullWhen(true)] this string? value)
{
return !string.IsNullOrWhiteSpace(value);
}