Skip to content

Commit

Permalink
Merge pull request #1472 from DuendeSoftware/joe/par-client
Browse files Browse the repository at this point in the history
Clean up PAR client
  • Loading branch information
brockallen authored Nov 16, 2023
2 parents 8afede8 + fd672e1 commit 73dd86a
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,57 +1,53 @@
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;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace MvcPar
{
public class OidcEvents : OpenIdConnectEvents
public class ParOidcEvents(HttpClient httpClient, IDiscoveryCache discoveryCache, ILogger<ParOidcEvents> logger) : OpenIdConnectEvents
{
private readonly HttpClient _httpClient;
private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
private readonly HttpClient _httpClient = httpClient;
private readonly IDiscoveryCache _discoveryCache = discoveryCache;
private readonly ILogger<ParOidcEvents> _logger = logger;

public OidcEvents(HttpClient httpClient)
{
_httpClient = httpClient;
}
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);
// Construct the state parameter and add it to the protocol message
// so that we include it in the pushed authorization request
SetStateParameterForParRequest(context);

// Now send our PAR request
var requestBody = new FormUrlEncodedContent(context.ProtocolMessage.Parameters);
_httpClient.SetBasicAuthentication(clientId, "secret");

// 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>();
// Make the actual pushed authorization request
var parResponse = await PushAuthorizationParameters(context, clientId);

// 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;
// Now replace the parameters that would normally be sent to the
// authorize endpoint with just the client id and PAR request uri.
SetAuthorizeParameters(context, clientId, parResponse);

// 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)
// 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
// Finally redirect to the authorize endpoint
await RedirectToAuthorizeEndpoint(context, context.ProtocolMessage);
}

private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
private async Task RedirectToAuthorizeEndpoint(RedirectContext context, OpenIdConnectMessage message)
{
// This code is copied from the ASP.NET handler. We want most of its
// default behavior related to redirecting to the identity provider,
// except we already pushed the state parameter, so that is left out
// here. See https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L364
if (string.IsNullOrEmpty(message.IssuerAddress))
{
throw new InvalidOperationException(
Expand All @@ -63,8 +59,7 @@ public override async Task RedirectToIdentityProvider(RedirectContext context)
var redirectUri = message.CreateAuthenticationRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
// TODO
// Logger.InvalidAuthenticationRequestUrl(redirectUri);
_logger.LogWarning("The redirect URI is not well-formed. The URI is: '{AuthenticationRequestUrl}'.", redirectUri);
}

context.Response.Redirect(redirectUri);
Expand All @@ -88,7 +83,46 @@ public override async Task RedirectToIdentityProvider(RedirectContext context)
}

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

private async Task<ParResponse> PushAuthorizationParameters(RedirectContext context, string clientId)
{
// Send our PAR request
var requestBody = new FormUrlEncodedContent(context.ProtocolMessage.Parameters);
_httpClient.SetBasicAuthentication(clientId, "secret");

var disco = await _discoveryCache.GetAsync();
if (disco.IsError)
{
throw new Exception(disco.Error);
}
var parEndpoint = disco.TryGetValue("pushed_authorization_request_endpoint").GetString();
var response = await _httpClient.PostAsync(parEndpoint, requestBody);
if (!response.IsSuccessStatusCode)
{
throw new Exception("PAR failure");
}
return await response.Content.ReadFromJsonAsync<ParResponse>();

}

private static void SetAuthorizeParameters(RedirectContext context, string clientId, ParResponse 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 = parResponse.RequestUri;
}

private static OpenIdConnectMessage SetStateParameterForParRequest(RedirectContext context)
{
// 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);
return message;
}

public override Task TokenResponseReceived(TokenResponseReceivedContext context)
Expand Down
32 changes: 5 additions & 27 deletions clients/src/MvcPar/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Microsoft.IdentityModel.Tokens;
using System;
using Microsoft.Extensions.Configuration;
using Duende.AccessTokenManagement;
using IdentityModel.Client;

namespace MvcPar
{
Expand All @@ -20,7 +20,8 @@ public Startup(IConfiguration configuration)

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<OidcEvents>();
services.AddTransient<ParOidcEvents>();
services.AddSingleton<IDiscoveryCache>(_ => new DiscoveryCache(Constants.Authority));

// add MVC
services.AddControllersWithViews();
Expand All @@ -47,7 +48,6 @@ public void ConfigureServices(IServiceCollection services)
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Constants.Authority;
options.RequireHttpsMetadata = false;

options.ClientId = "mvc.par";
options.ClientSecret = "secret";
Expand All @@ -62,13 +62,12 @@ public void ConfigureServices(IServiceCollection services)
options.Scope.Add("resource1.scope1");
options.Scope.Add("offline_access");

// keeps id_token smaller
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.MapInboundClaims = false;

// needed to add JWR / private_key_jwt support
options.EventsType = typeof(OidcEvents);
// needed to add PAR support
options.EventsType = typeof(ParOidcEvents);

options.TokenValidationParameters = new TokenValidationParameters
{
Expand All @@ -87,27 +86,6 @@ public void ConfigureServices(IServiceCollection services)
{
client.BaseAddress = new Uri(Constants.SampleApi);
});

// var apiKey = _configuration["HoneyCombApiKey"];
// var dataset = "IdentityServerDev";
//
// services.AddOpenTelemetryTracing(builder =>
// {
// builder
// //.AddConsoleExporter()
// .SetResourceBuilder(
// ResourceBuilder.CreateDefault()
// .AddService("MVC JAR JWT"))
// //.SetSampler(new AlwaysOnSampler())
// .AddHttpClientInstrumentation()
// .AddAspNetCoreInstrumentation()
// .AddSqlClientInstrumentation()
// .AddOtlpExporter(option =>
// {
// option.Endpoint = new Uri("https://api.honeycomb.io");
// option.Headers = $"x-honeycomb-team={apiKey},x-honeycomb-dataset={dataset}";
// });
// });
}

public void Configure(IApplicationBuilder app)
Expand Down

0 comments on commit 73dd86a

Please sign in to comment.