Skip to content

Commit

Permalink
Merge pull request #147 from DuendeSoftware/joe/par
Browse files Browse the repository at this point in the history
Add PAR sample
  • Loading branch information
brockallen authored Nov 16, 2023
2 parents d0f9b3a + 6222cc3 commit 0eb6b7b
Show file tree
Hide file tree
Showing 55 changed files with 39,857 additions and 9 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2


- name: Setup net8
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.0.x'

- name: Setup net6
uses: actions/setup-dotnet@v1
with:
Expand Down
31 changes: 31 additions & 0 deletions IdentityServer/v6/Basics/IdentityServer/src/Clients.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,37 @@ public static class Clients
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "scope1", "scope2" }
},

///////////////////////////////////////////
// MVC PAR Sample
//////////////////////////////////////////
new Client
{
ClientId = "mvc.par",
ClientName = "MVC PAR Client",

ClientSecrets =
{
new Secret("secret".Sha256())
},

RequireRequestObject = false,

AllowedGrantTypes = GrantTypes.Code,

RequirePushedAuthorization = true,

// Note that redirect uris are optional for PAR clients when the
// AllowUnregisteredPushedRedirectUris flag is enabled
// RedirectUris = { "https://localhost:44300/signin-oidc" },

FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" },

AllowOfflineAccess = true,

AllowedScopes = { "openid", "profile", "scope1", "scope2" }
},
};
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<PackageReference Include="Duende.IdentityServer" Version="6.0.0" />
<PackageReference Include="Duende.IdentityServer" Version="7.0.0-preview.2" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.


using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;

Expand All @@ -17,13 +18,13 @@ public override void OnResultExecuting(ResultExecutingContext context)
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
{
context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.HttpContext.Response.Headers.Append("X-Content-Type-Options", "nosniff");
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options"))
{
context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
context.HttpContext.Response.Headers.Append("X-Frame-Options", "SAMEORIGIN");
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
Expand All @@ -36,19 +37,19 @@ public override void OnResultExecuting(ResultExecutingContext context)
// once for standards compliant browsers
if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
{
context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp);
context.HttpContext.Response.Headers.Append("Content-Security-Policy", csp);
}
// and once again for IE
if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy"))
{
context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp);
context.HttpContext.Response.Headers.Append("X-Content-Security-Policy", csp);
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
var referrer_policy = "no-referrer";
if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy"))
{
context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy);
context.HttpContext.Response.Headers.Append("Referrer-Policy", referrer_policy);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions IdentityServer/v6/Basics/IdentityServer/src/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public void ConfigureServices(IServiceCollection services)

// see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/api_scopes
options.EmitStaticAudienceClaim = true;
options.PushedAuthorization.AllowUnregisteredPushedRedirectUris = true;
})
.AddTestUsers(TestUsers.Users);

Expand Down
34 changes: 34 additions & 0 deletions IdentityServer/v6/Basics/MvcPar/MvcPar.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "src\Client.csproj", "{8A7B3FC1-BD45-4679-A3D9-FE2E06F783CF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServerHost", "..\IdentityServer\src\IdentityServerHost.csproj", "{5F6BD9CA-DC99-4085-88E3-7FDF8D60903E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleApi", "..\Apis\SimpleApi\SimpleApi.csproj", "{48D8CF98-12BF-4700-8727-61996C498A0A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8A7B3FC1-BD45-4679-A3D9-FE2E06F783CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7B3FC1-BD45-4679-A3D9-FE2E06F783CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7B3FC1-BD45-4679-A3D9-FE2E06F783CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7B3FC1-BD45-4679-A3D9-FE2E06F783CF}.Release|Any CPU.Build.0 = Release|Any CPU
{5F6BD9CA-DC99-4085-88E3-7FDF8D60903E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F6BD9CA-DC99-4085-88E3-7FDF8D60903E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F6BD9CA-DC99-4085-88E3-7FDF8D60903E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F6BD9CA-DC99-4085-88E3-7FDF8D60903E}.Release|Any CPU.Build.0 = Release|Any CPU
{48D8CF98-12BF-4700-8727-61996C498A0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48D8CF98-12BF-4700-8727-61996C498A0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48D8CF98-12BF-4700-8727-61996C498A0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48D8CF98-12BF-4700-8727-61996C498A0A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
20 changes: 20 additions & 0 deletions IdentityServer/v6/Basics/MvcPar/src/Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<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" Version="8.0.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
</ItemGroup>

<!-- Constants and helpers -->
<ItemGroup>
<Compile Include="..\..\Shared\Constants.cs">
<Link>Shared\Constants.cs</Link>
</Compile>
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions IdentityServer/v6/Basics/MvcPar/src/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;

namespace Client.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");
var json = JsonDocument.Parse(response);

ViewBag.Json = JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true });
return View();
}
}
}
142 changes: 142 additions & 0 deletions IdentityServer/v6/Basics/MvcPar/src/ParOidcEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace Client
{
public class ParOidcEvents(HttpClient httpClient, IDiscoveryCache discoveryCache, ILogger<ParOidcEvents> logger) : OpenIdConnectEvents
{
private readonly HttpClient _httpClient = httpClient;
private readonly IDiscoveryCache _discoveryCache = discoveryCache;
private readonly ILogger<ParOidcEvents> _logger = logger;

public override async Task RedirectToIdentityProvider(RedirectContext context)
{
var clientId = context.ProtocolMessage.ClientId;

// Construct the state parameter and add it to the protocol message
// so that we include it in the pushed authorization request
SetStateParameterForParRequest(context);

// Make the actual pushed authorization request
var parResponse = await PushAuthorizationParameters(context, clientId);

// 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).
context.HandleResponse();

// 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(
"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))
{
_logger.LogWarning("The redirect URI is not well-formed. The URI is: '{AuthenticationRequestUrl}'.", 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}");
}

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)
{
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 0eb6b7b

Please sign in to comment.