Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for PAR #1424

Merged
merged 64 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
79f94f6
Add PAR to discovery
josephdecock Sep 12, 2023
1618e71
Add PAR options object model
josephdecock Sep 12, 2023
cfe2a2d
PAR endpoint beginnings
josephdecock Sep 13, 2023
c33faf3
Serialize and deserialize parameters into PAR records
josephdecock Sep 13, 2023
8524d14
Create client that uses PAR
josephdecock Sep 13, 2023
09424bb
Create PAR client
josephdecock Sep 13, 2023
bd6c35f
Correct PAR responses
josephdecock Sep 13, 2023
ac12b35
PAR - validate expiration and binding to client
josephdecock Sep 14, 2023
fa835dd
PAR client options and cleanup
josephdecock Sep 14, 2023
afc56c0
Add PAR client to clients sln
josephdecock Sep 14, 2023
c323b41
Merge branch 'main' into joe/par
josephdecock Sep 14, 2023
b3283e1
Beginnings of test suite for PAR
josephdecock Sep 15, 2023
dbfaa95
More PAR testing improvement
josephdecock Sep 15, 2023
2142027
Improve PAR error handling
josephdecock Sep 16, 2023
51ce2e3
Rename request uri property of validated requests
josephdecock Sep 16, 2023
dcdb279
Merge branch 'joe/fluent-assertions-upgrade' into joe/par
josephdecock Sep 18, 2023
674baad
Add FluentAssertions.Web
josephdecock Sep 18, 2023
d2cff91
Misc PAR cleanup
josephdecock Sep 18, 2023
267e0a1
Support JAR + PAR
josephdecock Sep 18, 2023
8dba504
hex encode reference values in PAR request uris
josephdecock Sep 18, 2023
049c654
Validate PAR required global and per-client flag
josephdecock Sep 19, 2023
3cdee17
Fix typo
josephdecock Sep 19, 2023
5988500
Merge branch 'main' into joe/par
josephdecock Sep 19, 2023
7dc7707
First cut of PAR EF store
josephdecock Sep 19, 2023
e18a5ec
EF host and migrations for PAR
josephdecock Sep 19, 2023
1919186
Incrementally applying changes to fix broken test
josephdecock Sep 21, 2023
9993785
Fixed bugs in PAR when using JAR too
josephdecock Sep 22, 2023
1e83a4c
Simplify JAR + PAR
josephdecock Sep 22, 2023
b128447
Enable PAR in the endpoint options
josephdecock Sep 27, 2023
8b0e810
Refactor PAR with response generator, validator
josephdecock Sep 28, 2023
db0e5c8
Use handle generation service in PAR
josephdecock Sep 28, 2023
5ff708a
Store only PAR reference values, not request_uris
josephdecock Sep 28, 2023
439e4f1
Track PAR reference value in ValidatedAuthRequest
josephdecock Sep 28, 2023
40639f2
Add constant for par request_uri prefix
josephdecock Sep 28, 2023
26c0ed9
Rotate PAR instead of using auth parameters store
josephdecock Sep 28, 2023
5b19e8f
Fix broken tests
josephdecock Sep 28, 2023
d56695e
Consume PAR on response from authorize
josephdecock Sep 28, 2023
2787c7a
Fix PAR return urls
josephdecock Sep 28, 2023
c6b22b9
Regenerate PAR schema
josephdecock Sep 28, 2023
b61944b
Add private key jwt authorization to PAR client
josephdecock Sep 29, 2023
d381df9
Use JAR with PAR in sample client
josephdecock Sep 29, 2023
db52f1a
Simplify PAR consumption, and always do it
josephdecock Sep 29, 2023
b799fc0
Add xmldoc for PAR
josephdecock Oct 3, 2023
5fb412d
Add service to handle request object logic
josephdecock Oct 4, 2023
08b474b
Add method for par deserialization and cleanup par validation
josephdecock Oct 4, 2023
adea929
Automated tests of PAR
josephdecock Oct 4, 2023
0cc5fe9
XmlDoc for PAR service
josephdecock Oct 4, 2023
7960b94
Fix up clients sln
josephdecock Oct 4, 2023
02ceb0e
Add indexing to PAR EF
josephdecock Oct 5, 2023
5a883fb
Refactor PAR for clarity
josephdecock Oct 6, 2023
c14c246
Update PAR schema migrations
josephdecock Oct 6, 2023
c844633
Separate PAR and PAR+JAR clients
josephdecock Oct 6, 2023
cb9518d
Clean up TODOs
josephdecock Oct 7, 2023
83cf53f
Add PAR as valid aud in private key jwt
josephdecock Oct 7, 2023
7503dd8
Cleanup stale PARs
josephdecock Oct 9, 2023
66dab04
Remove unused events service from PAR endpoint
josephdecock Oct 20, 2023
51e91c0
Validate pushed parameters in par validator
josephdecock Oct 20, 2023
17bb5b8
Clean up copyright headers
josephdecock Oct 20, 2023
4d8db9c
Fix a typo
josephdecock Oct 20, 2023
d5e98ac
Remove a review note
josephdecock Oct 20, 2023
7417e41
Set default lifetime to be less than 10 minutes
josephdecock Oct 23, 2023
e7856da
PAR - changes from review
josephdecock Oct 23, 2023
6db895d
Clean up some PAR xml doc and nullability checks
josephdecock Oct 23, 2023
83c0b76
PAR xmldoc typo
josephdecock Oct 23, 2023
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
40 changes: 40 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,46 @@
"group": "20-clients",
}
},
{
"name": "client: MvcPar",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-client-MvcPar",
"program": "${workspaceFolder}/clients/src/MvcPar/bin/Debug/net8.0/MvcPar.dll",
"args": [],
"cwd": "${workspaceFolder}/clients/src/MvcPar",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"presentation": {
"hidden": false,
"group": "20-clients",
}
},
{
"name": "client: MvcJarPar",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-client-MvcJarPar",
"program": "${workspaceFolder}/clients/src/MvcJarPar/bin/Debug/net8.0/MvcJarPar.dll",
"args": [],
"cwd": "${workspaceFolder}/clients/src/MvcJarPar",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"presentation": {
"hidden": false,
"group": "20-clients",
}
},
{
"name": "client: MvcHybridBackChannel",
"type": "coreclr",
Expand Down
24 changes: 24 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,30 @@
],
"problemMatcher": "$msCompile"
},
{
"label": "build-client-MvcPar",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/clients/src/MvcPar/MvcPar.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-client-MvcJarPar",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/clients/src/MvcJarPar/MvcJarPar.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-client-MvcHybridBackChannel",
"type": "process",
Expand Down
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

<!--tests -->
<PackageReference Update="FluentAssertions" Version="6.5.1"/>
<PackageReference Update="FluentAssertions.Web" Version="1.2.5"/>
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Update="xunit" Version="2.5.0"/>
<PackageReference Update="xunit.runner.visualstudio" Version="2.5.1" PrivateAssets="All"/>
Expand Down
7 changes: 7 additions & 0 deletions Duende.IdentityServer.Clients.sln
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleDcrClient", "clients
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClientCredentialsFlowDPoP", "clients\src\ConsoleClientCredentialsFlowDPoP\ConsoleClientCredentialsFlowDPoP.csproj", "{5864BB85-B0B3-4061-B7BD-98C67651C1B3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcPar", "clients\src\MvcPar\MvcPar.csproj", "{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -211,6 +213,10 @@ Global
{5864BB85-B0B3-4061-B7BD-98C67651C1B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5864BB85-B0B3-4061-B7BD-98C67651C1B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5864BB85-B0B3-4061-B7BD-98C67651C1B3}.Release|Any CPU.Build.0 = Release|Any CPU
{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -251,6 +257,7 @@ Global
{8F1405C7-CF4D-4780-BDE1-852E743987C6} = {AFE7085F-051E-4829-955F-3426FE643BDD}
{D3FF035B-354A-45B9-B610-31BF2C13B360} = {D027D36B-262B-450A-B444-5B7893B5142E}
{5864BB85-B0B3-4061-B7BD-98C67651C1B3} = {D027D36B-262B-450A-B444-5B7893B5142E}
{9CAFC6C2-0C6D-4246-A3E1-011AA9CB4240} = {158628D7-8B68-451E-AF22-B64F473C5943}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BAD78470-3D66-466E-9C17-2A67F0905B18}
Expand Down
76 changes: 76 additions & 0 deletions clients/src/MvcJarPar/AssertionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Clients;
using IdentityModel;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace MvcJarAndPar
{
public class AssertionService
{
private static string rsaKey =
"""
{
"d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
"dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE","dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
"e":"AQAB",
"kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
"kty":"RSA",
"n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
"p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE","q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
"qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"}
""";

public string CreateClientToken()
{
var now = DateTime.UtcNow;

var token = new JwtSecurityToken(
"mvc.jar.par",
Constants.Authority + "/connect/token",
new List<Claim>()
{
new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
new Claim(JwtClaimTypes.Subject, "mvc.jar.par"),
new Claim(JwtClaimTypes.IssuedAt, now.ToEpochTime().ToString(), ClaimValueTypes.Integer64)
},
now,
now.AddMinutes(1),
new SigningCredentials(new JsonWebKey(rsaKey), "RS256")
);

var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.OutboundClaimTypeMap.Clear();

return tokenHandler.WriteToken(token);
}

public string SignAuthorizationRequest(OpenIdConnectMessage message)
{
var now = DateTime.UtcNow;

var claims = new List<Claim>();
foreach (var parameter in message.Parameters)
{
claims.Add(new Claim(parameter.Key, parameter.Value));
}

var token = new JwtSecurityToken(
"mvc.par",
Constants.Authority,
claims,
now,
now.AddMinutes(1),
new SigningCredentials(new JsonWebKey(rsaKey), "RS256")
);

var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.OutboundClaimTypeMap.Clear();

return tokenHandler.WriteToken(token);
}
}
}
35 changes: 35 additions & 0 deletions clients/src/MvcJarPar/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Clients;
using System.Net.Http;
using System.Threading.Tasks;

namespace MvcJarAndPar.Controllers
{
public class HomeController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;

public HomeController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}

[AllowAnonymous]
public IActionResult Index() => View();

public IActionResult Secure() => View();

public IActionResult Logout() => SignOut("oidc", "cookie");

public async Task<IActionResult> CallApi()
{
var client = _httpClientFactory.CreateClient("client");

var response = await client.GetStringAsync("identity");
ViewBag.Json = response.PrettyPrintJson();

return View();
}
}
}
25 changes: 25 additions & 0 deletions clients/src/MvcJarPar/MvcJarPar.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Duende.AccessTokenManagement.OpenIdConnect" Version="2.0.3"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<PackageReference Include="Serilog.AspNetCore" />

<PackageReference Include="OpenTelemetry"/>
<PackageReference Include="OpenTelemetry.Exporter.Console"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http"/>
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Constants\Constants.csproj"/>
</ItemGroup>

</Project>
126 changes: 126 additions & 0 deletions clients/src/MvcJarPar/OidcEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using IdentityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

namespace MvcJarAndPar
{
public class OidcEvents : OpenIdConnectEvents
{
private readonly HttpClient _httpClient;
private readonly AssertionService _assertionService;
private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";

public OidcEvents(HttpClient httpClient, AssertionService assertionService)
{
_httpClient = httpClient;
_assertionService = assertionService;
}
public override async Task RedirectToIdentityProvider(RedirectContext context)
{
// Save client id, we will need that in our par request
var clientId = context.ProtocolMessage.ClientId;

// Construct State, we also need that (this chunk copied from the OIDC handler)
var message = context.ProtocolMessage;
// When redeeming a code for an AccessToken, this value is needed
context.Properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);
message.State = context.Options.StateDataFormat.Protect(context.Properties);

// Now send our PAR request

var requestObject = _assertionService.SignAuthorizationRequest(context.ProtocolMessage);
var parameters = new Dictionary<string, string>
{
{ "client_id", context.ProtocolMessage.ClientId },
{ "client_assertion_type", OidcConstants.ClientAssertionTypes.JwtBearer },
{ "client_assertion", _assertionService.CreateClientToken() },
{ "request", requestObject }
};
var requestBody = new FormUrlEncodedContent(parameters);

// TODO - use discovery to determine endpoint
var response = await _httpClient.PostAsync("https://localhost:5001/connect/par", requestBody);
// TODO - PAR can fail! Handle errors
var par = await response.Content.ReadFromJsonAsync<ParResponse>();

// Remove all the parameters from the protocol message, and replace with what we got from the PAR response
context.ProtocolMessage.Parameters.Clear();
// Then, set client id and request uri as parameters
context.ProtocolMessage.ClientId = clientId;
context.ProtocolMessage.RequestUri = par.RequestUri;

// Mark the request as handled, because we don't want the normal behavior that attaches state to the outgoing request (we already did that in the PAR request)
context.HandleResponse();

// However, we do want all the rest of the normal behavior, so the below is copied from what the handler normally does after this event
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L477-L511
if (string.IsNullOrEmpty(message.IssuerAddress))
{
throw new InvalidOperationException(
"Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
}

if (context.Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
{
var redirectUri = message.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
// TODO
// Logger.InvalidAuthenticationRequestUrl(redirectUri);
}

context.Response.Redirect(redirectUri);
return;
}
else if (context.Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
{
var content = message.BuildFormPost();
var buffer = Encoding.UTF8.GetBytes(content);

context.Response.ContentLength = buffer.Length;
context.Response.ContentType = "text/html;charset=UTF-8";

// Emit Cache-Control=no-cache to prevent client caching.
context.Response.Headers.CacheControl = "no-cache, no-store";
context.Response.Headers.Pragma = "no-cache";
context.Response.Headers.Expires = HeaderValueEpocDate;

await context.Response.Body.WriteAsync(buffer);
return;
}

throw new NotImplementedException($"An unsupported authentication method has been configured: {context.Options.AuthenticationMethod}");

}

public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
context.TokenEndpointRequest.ClientAssertionType = OidcConstants.ClientAssertionTypes.JwtBearer;
context.TokenEndpointRequest.ClientAssertion = _assertionService.CreateClientToken();

return Task.CompletedTask;
}

public override Task TokenResponseReceived(TokenResponseReceivedContext context)
{
return base.TokenResponseReceived(context);
}

private class ParResponse
{
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }

[JsonPropertyName("request_uri")]
public string RequestUri { get; set; }
}
}
}
Loading
Loading