Skip to content

Commit

Permalink
Merge pull request #1424 from DuendeSoftware/joe/par
Browse files Browse the repository at this point in the history
Add support for PAR
  • Loading branch information
brockallen authored Oct 24, 2023
2 parents dcc3cfa + 83c0b76 commit 7b8ec53
Show file tree
Hide file tree
Showing 187 changed files with 82,274 additions and 276 deletions.
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.3"/>
<PackageReference Update="xunit.runner.visualstudio" Version="2.5.3" 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

0 comments on commit 7b8ec53

Please sign in to comment.