Skip to content

Commit

Permalink
Merge pull request #172 from DuendeSoftware/joe/dpop
Browse files Browse the repository at this point in the history
Joe/dpop
  • Loading branch information
brockallen authored Jun 15, 2023
2 parents 15e4f8c + 9d5b3d1 commit 270c135
Show file tree
Hide file tree
Showing 77 changed files with 3,102 additions and 196 deletions.
5 changes: 3 additions & 2 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />

<!-- runtime -->
<PackageReference Update="Duende.AccessTokenManagement.OpenIdConnect" Version="2.0.0" />
<PackageReference Update="IdentityModel" Version="6.1.0" />
<PackageReference Update="Duende.AccessTokenManagement.OpenIdConnect" Version="2.0.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="$(FrameworkVersionRuntime)" />
<PackageReference Update="Yarp.ReverseProxy" Version="$(YarpVersion)" />

Expand All @@ -27,7 +28,7 @@
<PackageReference Update="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(FrameworkVersionTesting)" />
<PackageReference Update="Microsoft.AspNetCore.TestHost" Version="$(FrameworkVersionTesting)" />

<PackageReference Update="Duende.IdentityServer" Version="6.1.5" />
<PackageReference Update="Duende.IdentityServer" Version="6.3.2" />

<PackageReference Update="CsQuery.NETStandard" Version="1.3.6.1" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.3.1" />
Expand Down
45 changes: 45 additions & 0 deletions Duende.Bff.sln
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazor6.Client", "samples\B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazor6.Shared", "samples\Blazor6\Shared\Blazor6.Shared.csproj", "{E383523A-461D-4058-AF5A-BD67A9B723D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JS6.DPoP", "samples\JS6.DPoP\JS6.DPoP.csproj", "{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.DPoP", "samples\Api.DPoP\Api.DPoP.csproj", "{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Isolated", "samples\Api.Isolated\Api.Isolated.csproj", "{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -219,6 +225,42 @@ Global
{E383523A-461D-4058-AF5A-BD67A9B723D1}.Release|x64.Build.0 = Release|Any CPU
{E383523A-461D-4058-AF5A-BD67A9B723D1}.Release|x86.ActiveCfg = Release|Any CPU
{E383523A-461D-4058-AF5A-BD67A9B723D1}.Release|x86.Build.0 = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|x64.Build.0 = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Debug|x86.Build.0 = Debug|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|Any CPU.Build.0 = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|x64.ActiveCfg = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|x64.Build.0 = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|x86.ActiveCfg = Release|Any CPU
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41}.Release|x86.Build.0 = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|x64.Build.0 = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Debug|x86.Build.0 = Debug|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|Any CPU.Build.0 = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|x64.ActiveCfg = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|x64.Build.0 = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|x86.ActiveCfg = Release|Any CPU
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2}.Release|x86.Build.0 = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|x64.ActiveCfg = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|x64.Build.0 = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|x86.ActiveCfg = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Debug|x86.Build.0 = Debug|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|Any CPU.Build.0 = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|x64.ActiveCfg = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|x64.Build.0 = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|x86.ActiveCfg = Release|Any CPU
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -239,6 +281,9 @@ Global
{0AD5FDF4-0927-42D2-9D50-31A3370EDCA4} = {DD4052E0-5BA6-46DF-B13E-887EC8B45A77}
{5BADAC68-DD73-443D-98C1-D7C2FF033DA3} = {DD4052E0-5BA6-46DF-B13E-887EC8B45A77}
{E383523A-461D-4058-AF5A-BD67A9B723D1} = {DD4052E0-5BA6-46DF-B13E-887EC8B45A77}
{D2C4B3D3-A802-4E5A-84F1-7C99E716FB41} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
{E1305546-C5C3-4D10-A0AD-F81DF057B2E2} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
{399929C6-47FF-4A45-9BEA-B70FFC1EDE69} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3DAD5980-4688-4794-9CF0-6F3CB67194E7}
Expand Down
12 changes: 12 additions & 0 deletions samples/Api.DPoP/Api.DPoP.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" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.9" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
</ItemGroup>
</Project>
35 changes: 35 additions & 0 deletions samples/Api.DPoP/DPoP/ConfigureJwtBearerOptions.cs
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 Api.DPoP;

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 samples/Api.DPoP/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 Api.DPoP;

/// <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 samples/Api.DPoP/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 Api.DPoP;

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 samples/Api.DPoP/DPoP/DPoPMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Api.DPoP;

public enum DPoPMode
{
/// <summary>
/// Only DPoP tokens will be accepted
/// </summary>
DPoPOnly,
/// <summary>
/// Both DPoP and Bearer tokens will be accepted
/// </summary>
DPoPAndBearer
}
15 changes: 15 additions & 0 deletions samples/Api.DPoP/DPoP/DPoPOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace Api.DPoP;

public class DPoPOptions
{
public DPoPMode Mode { get; set; } = DPoPMode.DPoPOnly;

public TimeSpan ProofTokenValidityDuration { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan ClientClockSkew { get; set; } = TimeSpan.FromMinutes(0);
public TimeSpan ServerClockSkew { get; set; } = TimeSpan.FromMinutes(5);

public bool ValidateIat { get; set; } = true;
public bool ValidateNonce { get; set; } = false;
}
Loading

0 comments on commit 270c135

Please sign in to comment.