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 PAR sample #147

Merged
merged 2 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading