Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
87951c6
feat: add Pushed Authorization skeleton endpoint and corresponding tests
andrewclymer Jun 5, 2026
8005f8f
feat: Added invoking validation
andrewclymer Jun 6, 2026
95412a3
feat: PAR endpoint handler simple path complete
andrewclymer Jun 11, 2026
69e672b
feat: initial PushedAuthorizationEndpoint logging done
andrewclymer Jun 14, 2026
bf3e4d9
feat: implement PushedAuthorizationRequestValidator with validation l…
andrewclymer Jun 17, 2026
6701fd4
feat: add PAR discovery document generation
andrewclymer Jun 25, 2026
ced3617
feat: adjust formatting in DiscoveryResponseGenerator for clarity
andrewclymer Jun 25, 2026
374e516
feat: rename PushedAuthorizationEndpoint to PushedAuthorizationReques…
andrewclymer Jun 26, 2026
f058de2
feat: add client secret validation to PushedAuthorizationRequestEndpo…
andrewclymer Jun 26, 2026
bb56026
Store abstraction and response generator. Logging outstanding
richardblewett Jul 28, 2026
14b3cd9
Midway through wiring up the Authorization flow with request_uri. Nee…
andrewclymer Aug 28, 2026
50ddc56
Refactored to store NameValueColllection rather than trying to save a…
andrewclymer Aug 28, 2026
4d0c288
First end to end implementation done. still requires logging to be added
andrewclymer Aug 31, 2026
1e5bc73
Supports checking client_id is the same for both the PAR and the auth…
andrewclymer Aug 31, 2026
541896b
Add/updated copyright messages
andrewclymer Aug 31, 2026
b57bd96
Removed some whitepsace
andrewclymer Aug 31, 2026
d2d7899
Rebased off 2.0 main
andrewclymer Sep 1, 2026
3a38f4c
Added telemetry
andrewclymer Sep 1, 2026
8c3a649
Additional logging
andrewclymer Sep 1, 2026
e6b5399
Refactored to put a service layer between the response generator and …
andrewclymer Sep 2, 2026
a999fb0
Ensure no sensitive authentication parameters are persisted
andrewclymer Sep 3, 2026
5837600
Refactored the PAR service to send a request_uri hash to the store as…
andrewclymer Sep 4, 2026
ce4d5c4
Refactored namespace for test and removed commented out code
andrewclymer Sep 5, 2026
ff81325
Fixes PAR path in discovery document, adding an additional //connect
andrewclymer Sep 8, 2026
bfc86d5
Refactor Pushed Authorization components for improved readability and…
andrewclymer Sep 8, 2026
fa97881
Reorganize Pushed Authorization request handling in discovery respons…
andrewclymer Sep 8, 2026
0915db4
Add cache control header to Pushed Authorization response and impleme…
andrewclymer Sep 8, 2026
26f2707
Used extension method for SetNoCache rather than explictly setting th…
andrewclymer Sep 9, 2026
d33f4b6
Forgot to push the test
andrewclymer Sep 9, 2026
4290750
Reverted project reference back to a package reference
andrewclymer Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@

<ItemGroup>
<PackageReference Include="Open.IdentityServer"/>

<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public static IIdentityServerBuilder AddDefaultEndpoints(this IIdentityServerBui
builder.AddEndpoint<TokenRevocationEndpoint>(EndpointNames.Revocation, ProtocolRoutePaths.Revocation.EnsureLeadingSlash());
builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash());
builder.AddEndpoint<UserInfoEndpoint>(EndpointNames.UserInfo, ProtocolRoutePaths.UserInfo.EnsureLeadingSlash());

builder.AddEndpoint<PushedAuthorizationRequestEndpoint>(EndpointNames.PushedAuthorizationRequest, ProtocolRoutePaths.PushedAuthorizationRequest.EnsureLeadingSlash());
return builder;
}

Expand Down Expand Up @@ -182,6 +182,7 @@ public static IIdentityServerBuilder AddPluggableServices(this IIdentityServerBu
builder.Services.TryAddTransient<IBackChannelLogoutService, DefaultBackChannelLogoutService>();
builder.Services.TryAddTransient<IResourceValidator, DefaultResourceValidator>();
builder.Services.TryAddTransient<IScopeParser, DefaultScopeParser>();
builder.Services.TryAddTransient<IPushedAuthorizationRequestService, DefaultPushedAuthorizationRequestService>();

builder.Services.TryAddSingleton<ITelemetryService, DefaultTelemetryService>();

Expand Down Expand Up @@ -218,11 +219,15 @@ public static IIdentityServerBuilder AddValidators(this IIdentityServerBuilder b
builder.Services.TryAddTransient<IClientConfigurationValidator, DefaultClientConfigurationValidator>();
builder.Services.TryAddTransient<IDeviceAuthorizationRequestValidator, DeviceAuthorizationRequestValidator>();
builder.Services.TryAddTransient<IDeviceCodeValidator, DeviceCodeValidator>();

builder.Services.TryAddTransient<IPushedAuthorizationRequestValidator, PushedAuthorizationRequestValidator>();
// optional
builder.Services.TryAddTransient<ICustomTokenValidator, DefaultCustomTokenValidator>();
builder.Services.TryAddTransient<ICustomAuthorizeRequestValidator, DefaultCustomAuthorizeRequestValidator>();

// PAR support
builder.Services
.AddTransientDecorator<IAuthorizeRequestValidator,AuthorizeUsingPushedAuthorizationRequestValidator>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing


return builder;
}

Expand All @@ -241,7 +246,8 @@ public static IIdentityServerBuilder AddResponseGenerators(this IIdentityServerB
builder.Services.TryAddTransient<IDiscoveryResponseGenerator, DiscoveryResponseGenerator>();
builder.Services.TryAddTransient<ITokenRevocationResponseGenerator, TokenRevocationResponseGenerator>();
builder.Services.TryAddTransient<IDeviceAuthorizationResponseGenerator, DeviceAuthorizationResponseGenerator>();

builder.Services.TryAddTransient<IPushedAuthorizationResponseGenerator, PushedAuthorizationResponseGenerator>();

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public static IIdentityServerBuilder AddInMemoryPersistedGrants(this IIdentitySe
{
builder.Services.TryAddSingleton<IPersistedGrantStore, InMemoryPersistedGrantStore>();
builder.Services.TryAddSingleton<IDeviceFlowStore, InMemoryDeviceFlowStore>();

builder.Services.TryAddSingleton<IPushedAuthorizationRequestStore, InMemoryPushedAuthorizationRequestStore>();
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ public class EndpointsOptions
/// <c>true</c> if the device authorization endpoint is enabled; otherwise, <c>false</c>.
/// </value>
public bool EnableDeviceAuthorizationEndpoint { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the pushed authorization endpoint is enabled.
/// </summary>
public bool EnablePushedAuthorizationRequestEndpoint { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,9 @@ public class IdentityServerOptions
/// Gets or sets the enable authorise response issuer param option
/// </summary>
public bool EnableAuthorizeResponseIssuerParam { get; set; } = false;

/// <summary>
/// PAR authorization options

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't match the rest of the file

/// </summary>
public PushedAuthorizationOptions PushedAuthorization {get;} = new PushedAuthorizationOptions();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;

namespace Open.IdentityServer.Configuration;

/// <summary>
/// PAR options
/// </summary>
public class PushedAuthorizationOptions
{
/// <summary>
/// Enforce PAR for all authorization requests
/// </summary>
public bool Required { get; set; } = false;

/// <summary>
/// The lifetime of a PAR request_uri
/// </summary>
public TimeSpan Expiration { get; set; } = TimeSpan.FromSeconds(60);
}
2 changes: 2 additions & 0 deletions src/Open.IdentityServer/src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,15 @@ public static class EndpointNames
public const string EndSession = "Endsession";
public const string CheckSession = "Checksession";
public const string UserInfo = "Userinfo";
public const string PushedAuthorizationRequest = "PushedAuthorizationRequest";
}

public static class ProtocolRoutePaths
{
public const string ConnectPathPrefix = "connect";

public const string Authorize = ConnectPathPrefix + "/authorize";
public const string PushedAuthorizationRequest = ConnectPathPrefix + "/par";
public const string AuthorizeCallback = Authorize + "/callback";
public const string DiscoveryConfiguration = ".well-known/openid-configuration";
public const string DiscoveryWebKeys = DiscoveryConfiguration + "/jwks";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ internal async Task<IEndpointResult> ProcessAuthorizeRequestAsync(NameValueColle
}

var request = result.ValidatedRequest;
return await ProcessValidatedRequest(consent, request);
}

private async Task<IEndpointResult> ProcessValidatedRequest(ConsentResponse consent, ValidatedAuthorizeRequest request)
{
LogRequest(request);

// determine user interaction
Expand Down
117 changes: 117 additions & 0 deletions src/Open.IdentityServer/src/Endpoints/PushedAuthorizationEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// 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.Collections.Specialized;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Open.IdentityServer.Configuration;
using Open.IdentityServer.Endpoints.Results;
using Open.IdentityServer.Hosting;
using Open.IdentityServer.Extensions;
using Open.IdentityServer.ResponseHandling;
using Open.IdentityServer.Services;
using Open.IdentityServer.Validation;

#nullable enable
namespace Open.IdentityServer.Endpoints;

internal class PushedAuthorizationRequestEndpoint(
IdentityServerOptions options,
IClientSecretValidator clientSecretValidator,
IPushedAuthorizationRequestValidator validator ,
IPushedAuthorizationResponseGenerator responseGenerator,
ITelemetryService telemetry,
ILogger<PushedAuthorizationRequestEndpoint> logger) : IEndpointHandler
{
public async Task<IEndpointResult> ProcessAsync(HttpContext requestContext)
{
using ITrace trace = telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this);

if (options.Endpoints.EnablePushedAuthorizationRequestEndpoint == false)
{
return new StatusCodeResult(HttpStatusCode.NotFound);
}

logger.LogDebug("Start processing pushed authorization request");
if (!HttpMethods.IsPost(requestContext.Request.Method))
{
return Error(OidcConstants.TokenErrors.InvalidRequest);
}

ClientSecretValidationResult? clientValidationResult = await clientSecretValidator.ValidateAsync(requestContext);
Comment thread
andrewclymer marked this conversation as resolved.
if (clientValidationResult.IsError)
{
return Error(OidcConstants.TokenErrors.InvalidClient);
}

trace?.AddTag(TelemetryConstants.TagConstants.Client, clientValidationResult.Client.ClientId);

NameValueCollection? parParameters = await ParseForm(requestContext.Request);
if (parParameters == null)
{
return Error(OidcConstants.TokenErrors.InvalidRequest);
}
var validationContext = new PushedAuthorizationRequestValidationContext(parParameters);
return await ProcessRequest(requestContext, validationContext);
}

private async Task<IEndpointResult> ProcessRequest(
HttpContext requestContext,
PushedAuthorizationRequestValidationContext validationContext)
{
PushAuthorizationRequestValidationResult result = await validator
.ValidateAsync(validationContext, requestContext.RequestAborted);

telemetry.CountPushedAuthorizationRequest(
result.ValidatedAuthorizeRequest.ClientId ,
result.IsError ? result.Error : null);

if (result.IsError)
{
logger.LogError("Bad PAR request from {0}: {1}",
result.ValidatedAuthorizeRequest.ClientId,
result.Error);

return new BadRequestResult(result.Error, result.ErrorDescription);
}

PushedAuthorizationResponse response = await responseGenerator
.CreateResponseAsync(result.ValidatedAuthorizeRequest);

logger.LogTrace("End processing pushed authorization request");
return new PushedAuthorizationResult(response);
}

private async Task<NameValueCollection?> ParseForm(HttpRequest request)
{
try
{
IFormCollection form = await request.ReadFormAsync();
NameValueCollection parParameters = form.AsNameValueCollection();

return parParameters;
}
catch (InvalidOperationException )
{
return null;
}
}

private TokenErrorResult Error(string error, string? errorDescription = null, Dictionary<string, object>? custom = null)
{
var response = new TokenErrorResponse
{
Error = error,
ErrorDescription = errorDescription,
Custom = custom
};

logger.LogError("PushedAuthorizationRequest error: {error}:{errorDescriptions}", error, error ?? "-no message-");

return new TokenErrorResult(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Open.IdentityServer.Extensions;
using Open.IdentityServer.Hosting;
using Open.IdentityServer.ResponseHandling;

namespace Open.IdentityServer.Endpoints.Results;

internal record PushedAuthorizationResult(PushedAuthorizationResponse Response) : IEndpointResult
{
public async Task ExecuteAsync(HttpContext context)
Comment thread
andrewclymer marked this conversation as resolved.
{
context.Response.StatusCode = StatusCodes.Status201Created;
context.Response.ContentType = "application/json";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is already set by WriteAsJsonAsync

context.Response.SetNoCache();

await context.Response.WriteAsJsonAsync(Response);
}
}
8 changes: 8 additions & 0 deletions src/Open.IdentityServer/src/IdentityServerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public static class IdentityServerConstants
public const string DefaultCheckSessionCookieName = "idsrv.session";
public const string AccessTokenAudience = "{0}resources";

public static class PushedAuthorizationRequest
{
/// <summary>
/// Standard prefix for the generated URI for a Pushed Authorization Request
/// </summary>
public static readonly string UriRequestPrefix = "urn:ietf:params:oauth:request_uri:";
}

public const string JwtRequestClientKey = "idsrv.jwtrequesturi.client";

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Open.IdentityServer/src/OidcConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,12 @@ public static class Discovery
// DPoP
/// <summary>JSON array of JWS signing algorithms supported by the OP for DPoP proofs.</summary>
public const string DPoPSigningAlgorithmsSupported = "dpop_signing_alg_values_supported";

// PAR
/// <summary> The endpoint to use for PAR </summary>
public const string PushedAuthorizationRequestEndpoint = "pushed_authorization_request_endpoint";
/// <summary> Used to indicate to a client that it MUST use PAR to perform authorization code flow </summary>
public const string RequirePushedAuthorizationRequests = "require_pushed_authorization_requests";
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Open.IdentityServer/src/Open.IdentityServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,9 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Storage\src\Open.IdentityServer.Storage.csproj" />
</ItemGroup>

</Project>
4 changes: 3 additions & 1 deletion src/Open.IdentityServer/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Open.IdentityServer.UnitTests, PublicKey = 002400000480000094000000060200000024000052534131000400000100010057b24455efc2a317afb0644a2169c05644e439985c42cf4eb98706779651801add1da073da8b5e253e8d4335d59b3197bb941ebe943c63f7efbc3005c428f0d69b809e86bdc828fa431fae4b71005f26b52a26a3ee5cf0f6fdf744d4534a7a503683123f58e1082828b018245d2e40d8542f72a623c01490d73a5d3ff94a88c5")]
[assembly: InternalsVisibleTo("Open.IdentityServer.IntegrationTests, PublicKey = 002400000480000094000000060200000024000052534131000400000100010057b24455efc2a317afb0644a2169c05644e439985c42cf4eb98706779651801add1da073da8b5e253e8d4335d59b3197bb941ebe943c63f7efbc3005c428f0d69b809e86bdc828fa431fae4b71005f26b52a26a3ee5cf0f6fdf744d4534a7a503683123f58e1082828b018245d2e40d8542f72a623c01490d73a5d3ff94a88c5")]
[assembly: InternalsVisibleTo("Open.IdentityServer.IntegrationTests, PublicKey = 002400000480000094000000060200000024000052534131000400000100010057b24455efc2a317afb0644a2169c05644e439985c42cf4eb98706779651801add1da073da8b5e253e8d4335d59b3197bb941ebe943c63f7efbc3005c428f0d69b809e86bdc828fa431fae4b71005f26b52a26a3ee5cf0f6fdf744d4534a7a503683123f58e1082828b018245d2e40d8542f72a623c01490d73a5d3ff94a88c5")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(
"DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ string ConstructMtlsEndpoint(string endpoint)
return $"https://{Options.MutualTls.DomainName}.{parts[1]}{endpoint}";
}
}


}
AddPushedAuthorizationRequestEndpoint(baseUrl, entries);
Comment thread
andrewclymer marked this conversation as resolved.
}

// logout
if (Options.Endpoints.EnableEndSessionEndpoint)
{
Expand Down Expand Up @@ -369,10 +372,32 @@ where scope.ShowInDiscoveryDocument
}
}
}


return entries;
}

private void AddPushedAuthorizationRequestEndpoint(string baseUrl, Dictionary<string, object> entries)
{
if (Options.Endpoints.EnablePushedAuthorizationRequestEndpoint == false)
{
return;
}

string parPath = $"{baseUrl}{Constants.ProtocolRoutePaths.PushedAuthorizationRequest}";

entries.Add(OidcConstants.Discovery.PushedAuthorizationRequestEndpoint, parPath);

// If PAR is enabled
if (Options.Endpoints.EnablePushedAuthorizationRequestEndpoint)
{
entries.Add(
OidcConstants.Discovery.RequirePushedAuthorizationRequests,
Options.PushedAuthorization.Required
);
}
}

/// <summary>
/// Creates the JWK document.
/// </summary>
Expand Down
Loading