Skip to content
This repository was archived by the owner on Feb 11, 2025. It is now read-only.

Commit d39cf0c

Browse files
authored
Merge pull request #551 from IdentityModel/joe/par
PAR Support
2 parents 0d5186e + 26998a6 commit d39cf0c

17 files changed

+834
-132
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using IdentityModel.Internal;
5+
using System;
6+
using System.Net.Http;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace IdentityModel.Client;
11+
12+
/// <summary>
13+
/// HttpClient extensions for OIDC discovery
14+
/// </summary>
15+
public static class HttpClientPushedAuthorizationExtensions
16+
{
17+
/// <summary>
18+
/// Sends a pushed authorization request
19+
/// </summary>
20+
/// <param name="client">The HTTP client.</param>
21+
/// <param name="request"></param>
22+
/// <param name="cancellationToken">The cancellation token.</param>
23+
/// <returns></returns>
24+
public static Task<PushedAuthorizationResponse> PushAuthorizationAsync(this HttpClient client, PushedAuthorizationRequest request, CancellationToken cancellationToken = default)
25+
{
26+
if(request.Parameters.ContainsKey(OidcConstants.AuthorizeRequest.RequestUri))
27+
{
28+
throw new ArgumentException("request_uri cannot be used in a pushed authorization request", "request_uri");
29+
}
30+
31+
var clone = request.Clone();
32+
33+
// client id is always required, and will be added by the call to
34+
// Prepare() for other client credential styles.
35+
if(request.ClientCredentialStyle == ClientCredentialStyle.AuthorizationHeader)
36+
{
37+
clone.Parameters.AddRequired(OidcConstants.AuthorizeRequest.ClientId, request.ClientId);
38+
}
39+
40+
if (request.Request.IsPresent() || request.Parameters.ContainsKey(OidcConstants.AuthorizeRequest.Request))
41+
{
42+
clone.Parameters.AddRequired(OidcConstants.AuthorizeRequest.Request, request.Request);
43+
}
44+
else
45+
{
46+
clone.Parameters.AddRequired(OidcConstants.AuthorizeRequest.ResponseType, request.ResponseType);
47+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Scope, request.Scope);
48+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.RedirectUri, request.RedirectUri);
49+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.State, request.State);
50+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Nonce, request.Nonce);
51+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.LoginHint, request.LoginHint);
52+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.AcrValues, request.AcrValues);
53+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Prompt, request.Prompt);
54+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.ResponseMode, request.ResponseMode);
55+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.CodeChallenge, request.CodeChallenge);
56+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.CodeChallengeMethod, request.CodeChallengeMethod);
57+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Display, request.Display);
58+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.MaxAge, request.MaxAge.ToString());
59+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.UiLocales, request.UiLocales);
60+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.IdTokenHint, request.IdTokenHint);
61+
foreach(var resource in request.Resource ?? [])
62+
{
63+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Resource, resource, allowDuplicates: true);
64+
}
65+
clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.DPoPKeyThumbprint, request.DPoPKeyThumbprint);
66+
}
67+
68+
return PushAuthorizationAsync(client, clone, cancellationToken);
69+
}
70+
71+
internal static async Task<PushedAuthorizationResponse> PushAuthorizationAsync(this HttpMessageInvoker client, ProtocolRequest request, CancellationToken cancellationToken = default)
72+
{
73+
request.Prepare();
74+
request.Method = HttpMethod.Post;
75+
76+
HttpResponseMessage response;
77+
try
78+
{
79+
response = await client.SendAsync(request, cancellationToken).ConfigureAwait();
80+
}
81+
catch (OperationCanceledException)
82+
{
83+
throw;
84+
}
85+
catch (Exception ex)
86+
{
87+
return ProtocolResponse.FromException<PushedAuthorizationResponse>(ex);
88+
}
89+
90+
return await ProtocolResponse.FromHttpResponseAsync<PushedAuthorizationResponse>(response).ConfigureAwait();
91+
}
92+
}

src/Client/Extensions/RequestUrlExtensions.cs

+29-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
33

4+
using IdentityModel.Internal;
5+
46
namespace IdentityModel.Client;
57

68
/// <summary>
@@ -22,7 +24,7 @@ public static string Create(this RequestUrl request, Parameters parameters)
2224
/// <summary>
2325
/// Creates an authorize URL.
2426
/// </summary>
25-
/// <param name="request">The request.</param>
27+
/// <param name="request">The instance of the RequestUrl helper class.</param>
2628
/// <param name="clientId">The client identifier.</param>
2729
/// <param name="responseType">The response type.</param>
2830
/// <param name="scope">The scope.</param>
@@ -39,11 +41,12 @@ public static string Create(this RequestUrl request, Parameters parameters)
3941
/// <param name="maxAge">The max age.</param>
4042
/// <param name="uiLocales">The ui locales.</param>
4143
/// <param name="idTokenHint">The id_token hint.</param>
44+
/// <param name="requestUri">The request uri.</param>
4245
/// <param name="extra">Extra parameters.</param>
4346
/// <returns></returns>
4447
public static string CreateAuthorizeUrl(this RequestUrl request,
4548
string clientId,
46-
string responseType,
49+
string? responseType = null,
4750
string? scope = null,
4851
string? redirectUri = null,
4952
string? state = null,
@@ -58,28 +61,37 @@ public static string CreateAuthorizeUrl(this RequestUrl request,
5861
int? maxAge = null,
5962
string? uiLocales = null,
6063
string? idTokenHint = null,
64+
string? requestUri = null,
6165
Parameters? extra = null)
6266
{
6367
var values = new Parameters
6468
{
6569
{ OidcConstants.AuthorizeRequest.ClientId, clientId },
66-
{ OidcConstants.AuthorizeRequest.ResponseType, responseType }
6770
};
6871

69-
values.AddOptional(OidcConstants.AuthorizeRequest.Scope, scope);
70-
values.AddOptional(OidcConstants.AuthorizeRequest.RedirectUri, redirectUri);
71-
values.AddOptional(OidcConstants.AuthorizeRequest.State, state);
72-
values.AddOptional(OidcConstants.AuthorizeRequest.Nonce, nonce);
73-
values.AddOptional(OidcConstants.AuthorizeRequest.LoginHint, loginHint);
74-
values.AddOptional(OidcConstants.AuthorizeRequest.AcrValues, acrValues);
75-
values.AddOptional(OidcConstants.AuthorizeRequest.Prompt, prompt);
76-
values.AddOptional(OidcConstants.AuthorizeRequest.ResponseMode, responseMode);
77-
values.AddOptional(OidcConstants.AuthorizeRequest.CodeChallenge, codeChallenge);
78-
values.AddOptional(OidcConstants.AuthorizeRequest.CodeChallengeMethod, codeChallengeMethod);
79-
values.AddOptional(OidcConstants.AuthorizeRequest.Display, display);
80-
values.AddOptional(OidcConstants.AuthorizeRequest.MaxAge, maxAge?.ToString());
81-
values.AddOptional(OidcConstants.AuthorizeRequest.UiLocales, uiLocales);
82-
values.AddOptional(OidcConstants.AuthorizeRequest.IdTokenHint, idTokenHint);
72+
if (requestUri.IsPresent())
73+
{
74+
values.AddRequired(OidcConstants.AuthorizeRequest.RequestUri, requestUri);
75+
}
76+
else
77+
{
78+
values.AddRequired(OidcConstants.AuthorizeRequest.ResponseType, responseType);
79+
values.AddOptional(OidcConstants.AuthorizeRequest.Scope, scope);
80+
values.AddOptional(OidcConstants.AuthorizeRequest.RedirectUri, redirectUri);
81+
values.AddOptional(OidcConstants.AuthorizeRequest.State, state);
82+
values.AddOptional(OidcConstants.AuthorizeRequest.Nonce, nonce);
83+
values.AddOptional(OidcConstants.AuthorizeRequest.LoginHint, loginHint);
84+
values.AddOptional(OidcConstants.AuthorizeRequest.AcrValues, acrValues);
85+
values.AddOptional(OidcConstants.AuthorizeRequest.Prompt, prompt);
86+
values.AddOptional(OidcConstants.AuthorizeRequest.ResponseMode, responseMode);
87+
values.AddOptional(OidcConstants.AuthorizeRequest.CodeChallenge, codeChallenge);
88+
values.AddOptional(OidcConstants.AuthorizeRequest.CodeChallengeMethod, codeChallengeMethod);
89+
values.AddOptional(OidcConstants.AuthorizeRequest.Display, display);
90+
values.AddOptional(OidcConstants.AuthorizeRequest.MaxAge, maxAge?.ToString());
91+
values.AddOptional(OidcConstants.AuthorizeRequest.UiLocales, uiLocales);
92+
values.AddOptional(OidcConstants.AuthorizeRequest.IdTokenHint, idTokenHint);
93+
values.AddOptional(OidcConstants.AuthorizeRequest.RequestUri, requestUri);
94+
}
8395

8496
return request.Create(values.Merge(extra));
8597
}

src/Client/Messages/DiscoveryDocumentResponse.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ protected override Task InitializeAsync(object? initializationData = null)
7676
public string? EndSessionEndpoint => TryGetString(OidcConstants.Discovery.EndSessionEndpoint);
7777
public string? CheckSessionIframe => TryGetString(OidcConstants.Discovery.CheckSessionIframe);
7878
public string? RegistrationEndpoint => TryGetString(OidcConstants.Discovery.RegistrationEndpoint);
79+
public string? PushedAuthorizationRequestEndpoint => TryGetString(OidcConstants.Discovery.PushedAuthorizationRequestEndpoint);
7980
public bool? FrontChannelLogoutSupported => TryGetBoolean(OidcConstants.Discovery.FrontChannelLogoutSupported);
8081
public bool? FrontChannelLogoutSessionSupported => TryGetBoolean(OidcConstants.Discovery.FrontChannelLogoutSessionSupported);
8182
public IEnumerable<string> GrantTypesSupported => TryGetStringArray(OidcConstants.Discovery.GrantTypesSupported);
@@ -88,7 +89,8 @@ protected override Task InitializeAsync(object? initializationData = null)
8889
public IEnumerable<string> TokenEndpointAuthenticationMethodsSupported => TryGetStringArray(OidcConstants.Discovery.TokenEndpointAuthenticationMethodsSupported);
8990
public IEnumerable<string> BackchannelTokenDeliveryModesSupported => TryGetStringArray(OidcConstants.Discovery.BackchannelTokenDeliveryModesSupported);
9091
public bool? BackchannelUserCodeParameterSupported => TryGetBoolean(OidcConstants.Discovery.BackchannelUserCodeParameterSupported);
91-
92+
public bool? RequirePushedAuthorizationRequests => TryGetBoolean(OidcConstants.Discovery.RequirePushedAuthorizationRequests);
93+
9294
// generic
9395
public JsonElement TryGetValue(string name) => Json.TryGetValue(name);
9496
public string? TryGetString(string name) => Json.TryGetString(name);

src/Client/Messages/Parameters.cs

+23-17
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public bool ContainsKey(string key)
129129
public void AddOptional(string key, string? value, bool allowDuplicates = false)
130130
{
131131
if (key.IsMissing()) throw new ArgumentNullException(nameof(key));
132-
132+
if (value.IsMissing()) return;
133133
if (allowDuplicates == false)
134134
{
135135
if (ContainsKey(key))
@@ -138,42 +138,48 @@ public void AddOptional(string key, string? value, bool allowDuplicates = false)
138138
}
139139
}
140140

141-
if (value.IsPresent())
142-
{
143-
Add(key, value!);
144-
}
141+
Add(key, value!);
145142
}
146143

147144
/// <summary>
148-
/// Adds a required parameter
145+
/// Ensures that a required parameter is present, adding it if necessary.
149146
/// </summary>
150147
/// <param name="key">The key.</param>
151148
/// <param name="value">The value.</param>
152-
/// <param name="allowDuplicates">Allow multiple values of the same parameter.</param>
149+
/// <param name="allowDuplicates">Allow multiple distinct values for a duplicated parameter key.</param>
153150
/// <param name="allowEmptyValue">Allow an empty value.</param>
154151
/// <exception cref="ArgumentNullException"></exception>
155152
/// <exception cref="InvalidOperationException"></exception>
156153
/// <exception cref="ArgumentException"></exception>
157154
public void AddRequired(string key, string? value, bool allowDuplicates = false, bool allowEmptyValue = false)
158155
{
159156
if (key.IsMissing()) throw new ArgumentNullException(nameof(key));
160-
161-
if (allowDuplicates == false)
157+
158+
var valuePresent = value.IsPresent();
159+
var parameterPresent = ContainsKey(key);
160+
161+
if(!valuePresent && !parameterPresent && !allowEmptyValue)
162162
{
163-
if (ContainsKey(key))
163+
// Don't throw if we have a value already in the parameters
164+
// to make it more convenient for callers.
165+
throw new ArgumentException("Parameter is required", key);
166+
}
167+
else if (valuePresent && parameterPresent && !allowDuplicates)
168+
{
169+
if(this[key].Contains(value))
164170
{
165-
throw new InvalidOperationException($"Duplicate parameter: {key}");
171+
// The parameters are already in the desired state (the required
172+
// parameter key already has the specified value), so we don't
173+
// throw an error
174+
return;
166175
}
176+
throw new InvalidOperationException($"Duplicate parameter: {key}");
167177
}
168-
169-
if (value.IsPresent() || allowEmptyValue)
178+
179+
if (valuePresent || allowEmptyValue)
170180
{
171181
Add(key, value!);
172182
}
173-
else
174-
{
175-
throw new ArgumentException("Parameter is required", key);
176-
}
177183
}
178184

179185
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
6+
namespace IdentityModel.Client;
7+
8+
/// <summary>
9+
/// Models the parameters that can be pushed in a Pushed Authorization Request.
10+
/// </summary>
11+
/// <seealso cref="ProtocolRequest" />
12+
public class PushedAuthorizationRequest : ProtocolRequest
13+
{
14+
/// <summary>
15+
/// Gets or sets the response_type protocol parameter.
16+
/// </summary>
17+
public string? ResponseType { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the scope protocol parameter.
21+
/// </summary>
22+
public string? Scope { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the redirect_uri protocol parameter.
26+
/// </summary>
27+
public string? RedirectUri { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the state protocol parameter.
31+
/// </summary>
32+
public string? State { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the nonce protocol parameter.
36+
/// </summary>
37+
public string? Nonce { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets the login_hint protocol parameter.
41+
/// </summary>
42+
public string? LoginHint { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the acr_values protocol parameter.
46+
/// </summary>
47+
public string? AcrValues { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the prompt protocol parameter.
51+
/// </summary>
52+
public string? Prompt { get; set; }
53+
54+
/// <summary>
55+
/// Gets or sets the response_mode protocol parameter.
56+
/// </summary>
57+
public string? ResponseMode { get; set; }
58+
59+
/// <summary>
60+
/// Gets or sets the code_challenge protocol parameter.
61+
/// </summary>
62+
public string? CodeChallenge { get; set; }
63+
64+
/// <summary>
65+
/// Gets or sets the code_challenge_method protocol parameter.
66+
/// </summary>
67+
public string? CodeChallengeMethod { get; set; }
68+
69+
/// <summary>
70+
/// Gets or sets the display protocol parameter.
71+
/// </summary>
72+
public string? Display { get; set; }
73+
74+
/// <summary>
75+
/// Gets or sets the max_age protocol parameter.
76+
/// </summary>
77+
public int? MaxAge { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets the ui_locales protocol parameter.
81+
/// </summary>
82+
public string? UiLocales { get; set; }
83+
84+
/// <summary>
85+
/// Gets or sets the id_token_hint protocol parameter.
86+
/// </summary>
87+
public string? IdTokenHint { get; set; }
88+
89+
/// <summary>
90+
/// Gets or sets the resource protocol parameter.
91+
/// </summary>
92+
public ICollection<string> Resource { get; set; } = new HashSet<string>();
93+
94+
/// <summary>
95+
/// Gets or sets the dpop_jkt protocol parameter.
96+
/// </summary>
97+
public string? DPoPKeyThumbprint { get; set; }
98+
99+
/// <summary>
100+
/// Gets or sets the request protocol parameter.
101+
/// </summary>
102+
public string? Request { get; set; }
103+
104+
105+
/// <summary>
106+
/// Copies properties from a request into a Parameters collection.
107+
/// </summary>
108+
/// <param name="targetParameters">The parameters to copy into.</param>
109+
public Parameters MergeInto(Parameters targetParameters)
110+
{
111+
112+
return targetParameters;
113+
}
114+
}

0 commit comments

Comments
 (0)