-
Notifications
You must be signed in to change notification settings - Fork 358
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1424 from DuendeSoftware/joe/par
Add support for PAR
- Loading branch information
Showing
187 changed files
with
82,274 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} | ||
} |
Oops, something went wrong.