-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #172 from DuendeSoftware/joe/dpop
Joe/dpop
- Loading branch information
Showing
77 changed files
with
3,102 additions
and
196 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.