Skip to content

Commit

Permalink
Add BFF Sample using DPoP
Browse files Browse the repository at this point in the history
  • Loading branch information
josephdecock committed Jun 22, 2023
1 parent 94d524b commit 984d0d7
Show file tree
Hide file tree
Showing 32 changed files with 2,235 additions and 0 deletions.
46 changes: 46 additions & 0 deletions IdentityServer/v6/BFF/DPoP/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"version": "0.2.0",
"compounds": [
{
"name": "Run All",
"configurations": ["BFF", "API"],
"presentation": {
"hidden": false,
"group": "",
"order": 1
}
}
],
"configurations": [
{
"name": "API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-api",
"program": "${workspaceFolder}/DPoP.Api/bin/Debug/net6.0/DPoP.Api.dll",
"args": [],
"cwd": "${workspaceFolder}/DPoP.Api",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"console": "externalTerminal",
},
{
"name": "BFF",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-bff",
"program": "${workspaceFolder}/DPoP.Bff/bin/Debug/net6.0/DPoP.Bff.dll",
"args": [],
"cwd": "${workspaceFolder}/DPoP.Bff",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"console": "externalTerminal",
}
]
}
43 changes: 43 additions & 0 deletions IdentityServer/v6/BFF/DPoP/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/DPoP.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-api",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}\\DPoP.Api\\DPoP.Api.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-bff",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}\\DPoP.Bff\\DPoP.Bff.csproj",

"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]

}
12 changes: 12 additions & 0 deletions IdentityServer/v6/BFF/DPoP/DPoP.Api/DPoP.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<PackageReference Include="IdentityModel" version="6.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.9" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using System;

namespace DPoP.Api;

public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
private readonly string _configScheme;

public ConfigureJwtBearerOptions(string configScheme)
{
_configScheme = configScheme;
}

public void PostConfigure(string name, JwtBearerOptions options)
{
if (_configScheme == name)
{
if (options.EventsType != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.EventsType))
{
throw new Exception("EventsType on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
}
if (options.Events != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.Events.GetType()))
{
throw new Exception("Events on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
}

if (options.Events == null && options.EventsType == null)
{
options.EventsType = typeof(DPoPJwtBearerEvents);
}
}
}
}
81 changes: 81 additions & 0 deletions IdentityServer/v6/BFF/DPoP/DPoP.Api/DPoP/DPoPExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace DPoP.Api;

/// <summary>
/// Extensions methods for DPoP
/// </summary>
static class DPoPExtensions
{
const string DPoPPrefix = OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP + " ";

public static bool IsDPoPAuthorizationScheme(this HttpRequest request)
{
var authz = request.Headers.Authorization.FirstOrDefault();
return authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true;
}

public static bool TryGetDPoPAccessToken(this HttpRequest request, out string token)
{
token = null;

var authz = request.Headers.Authorization.FirstOrDefault();
if (authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true)
{
token = authz[DPoPPrefix.Length..].Trim();
return true;
}
return false;
}

public static string GetAuthorizationScheme(this HttpRequest request)
{
return request.Headers.Authorization.FirstOrDefault()?.Split(' ', System.StringSplitOptions.RemoveEmptyEntries)[0];
}

public static string GetDPoPProofToken(this HttpRequest request)
{
return request.Headers[OidcConstants.HttpHeaders.DPoP].FirstOrDefault();
}

public static string GetDPoPNonce(this AuthenticationProperties props)
{
if (props.Items.ContainsKey("DPoP-Nonce"))
{
return props.Items["DPoP-Nonce"] as string;
}
return null;
}
public static void SetDPoPNonce(this AuthenticationProperties props, string nonce)
{
props.Items["DPoP-Nonce"] = nonce;
}

/// <summary>
/// Create the value of a thumbprint-based cnf claim
/// </summary>
public static string CreateThumbprintCnf(this JsonWebKey jwk)
{
var jkt = jwk.CreateThumbprint();
var values = new Dictionary<string, string>
{
{ JwtClaimTypes.ConfirmationMethods.JwkThumbprint, jkt }
};
return JsonSerializer.Serialize(values);
}

/// <summary>
/// Create the value of a thumbprint
/// </summary>
public static string CreateThumbprint(this JsonWebKey jwk)
{
var jkt = Base64Url.Encode(jwk.ComputeJwkThumbprint());
return jkt;
}
}
152 changes: 152 additions & 0 deletions IdentityServer/v6/BFF/DPoP/DPoP.Api/DPoP/DPoPJwtBearerEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using static IdentityModel.OidcConstants;

namespace DPoP.Api;

public class DPoPJwtBearerEvents : JwtBearerEvents
{
private readonly IOptionsMonitor<DPoPOptions> _optionsMonitor;
private readonly DPoPProofValidator _validator;

public DPoPJwtBearerEvents(IOptionsMonitor<DPoPOptions> optionsMonitor, DPoPProofValidator validator)
{
_optionsMonitor = optionsMonitor;
_validator = validator;
}

public override Task MessageReceived(MessageReceivedContext context)
{
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);

if (context.HttpContext.Request.TryGetDPoPAccessToken(out var token))
{
context.Token = token;
}
else if (dpopOptions.Mode == DPoPMode.DPoPOnly)
{
// this rejects the attempt for this handler,
// since we don't want to attempt Bearer given the Mode
context.NoResult();
}

return Task.CompletedTask;
}

public override async Task TokenValidated(TokenValidatedContext context)
{
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);

if (context.HttpContext.Request.TryGetDPoPAccessToken(out var at))
{
var proofToken = context.HttpContext.Request.GetDPoPProofToken();
var result = await _validator.ValidateAsync(new DPoPProofValidatonContext
{
Scheme = context.Scheme.Name,
ProofToken = proofToken,
AccessToken = at,
Method = context.HttpContext.Request.Method,
Url = context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + context.HttpContext.Request.PathBase + context.HttpContext.Request.Path
});

if (result.IsError)
{
// fails the result
context.Fail(result.ErrorDescription ?? result.Error);

// we need to stash these values away so they are available later when the Challenge method is called later
context.HttpContext.Items["DPoP-Error"] = result.Error;
if (!string.IsNullOrWhiteSpace(result.ErrorDescription))
{
context.HttpContext.Items["DPoP-ErrorDescription"] = result.ErrorDescription;
}
if (!string.IsNullOrWhiteSpace(result.ServerIssuedNonce))
{
context.HttpContext.Items["DPoP-Nonce"] = result.ServerIssuedNonce;
}
}
}
else if (dpopOptions.Mode == DPoPMode.DPoPAndBearer)
{
// if the scheme used was not DPoP, then it was Bearer
// and if a access token was presented with a cnf, then the
// client should have sent it as DPoP, so we fail the request
if (context.Principal.HasClaim(x => x.Type == JwtClaimTypes.Confirmation))
{
context.HttpContext.Items["Bearer-ErrorDescription"] = "Must use DPoP when using an access token with a 'cnf' claim";
context.Fail("Must use DPoP when using an access token with a 'cnf' claim");
}
}
}

public override Task Challenge(JwtBearerChallengeContext context)
{
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);

if (dpopOptions.Mode == DPoPMode.DPoPOnly)
{
// if we are using DPoP only, then we don't need/want the default
// JwtBearerHandler to add its WWW-Authenticate response header
// so we have to set the status code ourselves
context.Response.StatusCode = 401;
context.HandleResponse();
}
else if (context.HttpContext.Items.ContainsKey("Bearer-ErrorDescription"))
{
var description = context.HttpContext.Items["Bearer-ErrorDescription"] as string;
context.ErrorDescription = description;
}

if (context.HttpContext.Request.IsDPoPAuthorizationScheme())
{
// if we are challening due to dpop, then don't allow bearer www-auth to emit an error
context.Error = null;
}

// now we always want to add our WWW-Authenticate for DPoP
// For example:
// WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="Invalid 'iat' value."
var sb = new StringBuilder();
sb.Append(OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP);

if (context.HttpContext.Items.ContainsKey("DPoP-Error"))
{
var error = context.HttpContext.Items["DPoP-Error"] as string;
sb.Append(" error=\"");
sb.Append(error);
sb.Append('\"');

if (context.HttpContext.Items.ContainsKey("DPoP-ErrorDescription"))
{
var description = context.HttpContext.Items["DPoP-ErrorDescription"] as string;

sb.Append(", error_description=\"");
sb.Append(description);
sb.Append('\"');
}
}

context.Response.Headers.Add(HeaderNames.WWWAuthenticate, sb.ToString());


if (context.HttpContext.Items.ContainsKey("DPoP-Nonce"))
{
var nonce = context.HttpContext.Items["DPoP-Nonce"] as string;
context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
}
else
{
var nonce = context.Properties.GetDPoPNonce();
if (nonce != null)
{
context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
}
}

return Task.CompletedTask;
}
}
13 changes: 13 additions & 0 deletions IdentityServer/v6/BFF/DPoP/DPoP.Api/DPoP/DPoPMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace DPoP.Api;

public enum DPoPMode
{
/// <summary>
/// Only DPoP tokens will be accepted
/// </summary>
DPoPOnly,
/// <summary>
/// Both DPoP and Bearer tokens will be accepted
/// </summary>
DPoPAndBearer
}
Loading

0 comments on commit 984d0d7

Please sign in to comment.