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

Joe/dpop #172

Merged
merged 28 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
58321e1
Update Duende.AccessTokenManagement dependency
josephdecock Jun 8, 2023
eda53b7
First cut at DPoP access token support
josephdecock Jun 8, 2023
4f83a18
Add new types for result of access token retrieval
josephdecock Jun 9, 2023
f80ebc2
Cleaned up access token retrieval types
josephdecock Jun 9, 2023
bd60fee
Unify code that adds access tokens to requests
josephdecock Jun 13, 2023
31bea7c
Add client credentials token call to sample
josephdecock Jun 13, 2023
0cfe70d
Consistent sample cookie names
josephdecock Jun 13, 2023
308a5ac
Add dpop jwk option
josephdecock Jun 13, 2023
7805d4e
Add dpop sample
josephdecock Jun 13, 2023
256aa11
small changes from pairing session
brockallen Jun 13, 2023
b3bcf16
Add DPoP support to sample API
josephdecock Jun 13, 2023
f1f8786
access token transform improvements
josephdecock Jun 13, 2023
384f40b
Disable forwarded headers in the sample api
josephdecock Jun 13, 2023
5ca12c9
polish for the token transformer
josephdecock Jun 14, 2023
a3bc951
Rework configuration and fix tests
josephdecock Jun 14, 2023
c674ab3
Remove duplicate short circuiting logic
josephdecock Jun 14, 2023
ffc1ed2
Log reason for failure to obtain access tokens
josephdecock Jun 14, 2023
662ce47
Add missing xmldoc
josephdecock Jun 14, 2023
5cce920
Remove copied impersonation code from DPoP sample
josephdecock Jun 14, 2023
3ec0fde
Additional sample API - one uses DPoP, one doesn't
josephdecock Jun 14, 2023
b65499d
Fix missing access token parameter ordering
josephdecock Jun 14, 2023
c975b79
Add more sample endpoint configs to dpop sample
josephdecock Jun 15, 2023
b68df36
Add more js6 sample endpoints
josephdecock Jun 15, 2023
a742877
Add yarp remote apis to DPOP sample
josephdecock Jun 15, 2023
ebde44b
Add local api invoking remote api to dpop sample
josephdecock Jun 15, 2023
937261b
Copy applicable changes from DPoP to JS6 sample
josephdecock Jun 15, 2023
5891019
Add sample of audience constrained tokens
josephdecock Jun 15, 2023
9d5b3d1
Fix case sensitivity issue in sln file
josephdecock Jun 15, 2023
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
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