Skip to content

Commit

Permalink
Merge pull request #1 from damienbod/dev-net8
Browse files Browse the repository at this point in the history
.NET 8
  • Loading branch information
damienbod authored Jan 28, 2024
2 parents bfec56d + ef2a397 commit 55c438f
Show file tree
Hide file tree
Showing 24 changed files with 120 additions and 81 deletions.
4 changes: 2 additions & 2 deletions GenerateCertiticate/GenerateCertiticate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CertificateManager" Version="1.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 damienbod
Copyright (c) 2024 damienbod

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

## History

- 2024-01-28 Updated packages
- 2024-01-05 Updated packages
- 2023-11-17 Updated .NET 8
- 2023-11-03 Updated packages, fixed security headers
- 2023-10-10 Updated packages
- 2023-08-28 Updated packages
Expand Down
16 changes: 8 additions & 8 deletions api/Api.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="IdentityModel" Version="6.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="0.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.2.3" />
<PackageReference Include="Serilog.Sinks.Seq" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions api/DPoP/ConfigureJwtBearerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using System;

namespace Api;

Expand Down
6 changes: 5 additions & 1 deletion api/DPoP/DPoPExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;

namespace Api;
Expand All @@ -18,7 +22,7 @@ public static bool IsDPoPAuthorizationScheme(this HttpRequest request)
return authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true;
}

public static bool TryGetDPoPAccessToken(this HttpRequest request, out string? token)
public static bool TryGetDPoPAccessToken(this HttpRequest request, [NotNullWhen(true)]out string? token)
{
token = null;

Expand Down
12 changes: 9 additions & 3 deletions api/DPoP/DPoPJwtBearerEvents.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using static IdentityModel.OidcConstants;

namespace Api;
Expand Down Expand Up @@ -43,6 +45,10 @@ public override async Task TokenValidated(TokenValidatedContext context)
if (context.HttpContext.Request.TryGetDPoPAccessToken(out var at))
{
var proofToken = context.HttpContext.Request.GetDPoPProofToken();
if (proofToken == null)
{
throw new InvalidOperationException("Missing DPoP (proof token) HTTP header");
}
var result = await _validator.ValidateAsync(new DPoPProofValidatonContext
{
Scheme = context.Scheme.Name,
Expand All @@ -55,7 +61,7 @@ public override async Task TokenValidated(TokenValidatedContext context)
if (result.IsError)
{
// fails the result
context.Fail(result.ErrorDescription ?? result.Error);
context.Fail(result.ErrorDescription ?? result.Error ?? throw new Exception("No ErrorDescription or Error set."));

// 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;
Expand All @@ -74,7 +80,7 @@ public override async Task TokenValidated(TokenValidatedContext context)
// 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))
if (context.Principal?.HasClaim(x => x.Type == JwtClaimTypes.Confirmation) ?? false)
{
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");
Expand Down Expand Up @@ -129,7 +135,7 @@ public override Task Challenge(JwtBearerChallengeContext context)
}
}

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


if (context.HttpContext.Items.ContainsKey("DPoP-Nonce"))
Expand Down
4 changes: 3 additions & 1 deletion api/DPoP/DPoPOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Api;
using System;

namespace Api;

public class DPoPOptions
{
Expand Down
13 changes: 8 additions & 5 deletions api/DPoP/DPoPProofValidatonContext.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
using System.Collections.Generic;
using System.Security.Claims;

namespace Api;

public class DPoPProofValidatonContext
{
/// <summary>
/// The ASP.NET Core authentication scheme triggering the validation
/// </summary>
public string Scheme { get; set; } = string.Empty;
public required string Scheme { get; set; }

/// <summary>
/// The HTTP URL to validate
/// </summary>
public string Url { get; set; } = string.Empty;
public required string Url { get; set; }

/// <summary>
/// The HTTP method to validate
/// </summary>
public string Method { get; set; } = string.Empty;
public required string Method { get; set; }

/// <summary>
/// The DPoP proof token to validate
/// </summary>
public string? ProofToken { get; set; }
public required string ProofToken { get; set; }

/// <summary>
/// The access token
/// </summary>
public string? AccessToken { get; set; }
public required string AccessToken { get; set; }
}
18 changes: 9 additions & 9 deletions api/DPoP/DPoPProofValidatonResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Api;

public class DPoPProofValidatonResult
{
public static DPoPProofValidatonResult Success { get; set; } = new() { IsError = false };
public static DPoPProofValidatonResult Success = new DPoPProofValidatonResult { IsError = false };

/// <summary>
/// Indicates if the result was successful or not
Expand All @@ -12,38 +12,38 @@ public class DPoPProofValidatonResult
/// <summary>
/// The error code for the validation result
/// </summary>
public string Error { get; set; } = string.Empty;
public string? Error { get; set; }

/// <summary>
/// The error description code for the validation result
/// </summary>
public string ErrorDescription { get; set; } = string.Empty;
public string? ErrorDescription { get; set; }

/// <summary>
/// The serialized JWK from the validated DPoP proof token.
/// </summary>
public string JsonWebKey { get; set; } = string.Empty;
public string? JsonWebKey { get; set; }

/// <summary>
/// The JWK thumbprint from the validated DPoP proof token.
/// </summary>
public string JsonWebKeyThumbprint { get; set; } = string.Empty;
public string? JsonWebKeyThumbprint { get; set; }

/// <summary>
/// The cnf value for the DPoP proof token
/// </summary>
public string Confirmation { get; set; } = string.Empty;
public string? Confirmation { get; set; }

/// <summary>
/// The payload value of the DPoP proof token.
/// </summary>
public IDictionary<string, object>? Payload { get; set; }
public IDictionary<string, object>? Payload { get; internal set; }

/// <summary>
/// The jti value read from the payload.
/// </summary>
public string? TokenId { get; set; }

/// <summary>
/// The ath value read from the payload.
/// </summary>
Expand All @@ -62,5 +62,5 @@ public class DPoPProofValidatonResult
/// <summary>
/// The nonce value issued by the server.
/// </summary>
public string ServerIssuedNonce { get; set; } = string.Empty;
public string? ServerIssuedNonce { get; set; }
}
51 changes: 31 additions & 20 deletions api/DPoP/DPoPProofValidator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using IdentityModel;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Api;

Expand Down Expand Up @@ -125,7 +130,7 @@ protected virtual Task ValidateHeaderAsync(DPoPProofValidatonContext context, DP
return Task.CompletedTask;
}

if (!token.TryGetHeaderValue<IDictionary<string, object>>(JwtClaimTypes.JsonWebKey, out var jwkValues))
if (!token.TryGetHeaderValue<JsonElement>(JwtClaimTypes.JsonWebKey, out var jwkValues))
{
result.IsError = true;
result.ErrorDescription = "Invalid 'jwk' value.";
Expand Down Expand Up @@ -164,9 +169,9 @@ protected virtual Task ValidateHeaderAsync(DPoPProofValidatonContext context, DP
/// <summary>
/// Validates the signature.
/// </summary>
protected virtual Task ValidateSignatureAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
protected virtual async Task ValidateSignatureAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
{
TokenValidationResult tokenValidationResult;
TokenValidationResult? tokenValidationResult = null;

try
{
Expand All @@ -180,35 +185,41 @@ protected virtual Task ValidateSignatureAsync(DPoPProofValidatonContext context,
};

var handler = new JsonWebTokenHandler();
tokenValidationResult = handler.ValidateToken(context.ProofToken, tvp);
tokenValidationResult = await handler.ValidateTokenAsync(context.ProofToken, tvp);
}
catch (Exception ex)
{
Logger.LogDebug("Error parsing DPoP token: {error}", ex.Message);
result.IsError = true;
result.ErrorDescription = "Invalid signature on DPoP token.";
return Task.CompletedTask;
}

if (tokenValidationResult.Exception != null)
if (tokenValidationResult?.Exception != null)
{
Logger.LogDebug("Error parsing DPoP token: {error}", tokenValidationResult.Exception.Message);
result.IsError = true;
result.ErrorDescription = "Invalid signature on DPoP token.";
return Task.CompletedTask;
}

result.Payload = tokenValidationResult.Claims;

return Task.CompletedTask;
if (tokenValidationResult != null)
{
result.Payload = tokenValidationResult.Claims;
}
}

/// <summary>
/// Validates the payload.
/// </summary>
protected virtual async Task ValidatePayloadAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
{
if (result.Payload!.TryGetValue(JwtClaimTypes.DPoPAccessTokenHash, out var ath))
if(result.Payload is null )
{
result.IsError = true;
result.ErrorDescription = "Missing payload";
return;
}

if (result.Payload.TryGetValue(JwtClaimTypes.DPoPAccessTokenHash, out var ath))
{
result.AccessTokenHash = ath as string;
}
Expand All @@ -222,7 +233,7 @@ protected virtual async Task ValidatePayloadAsync(DPoPProofValidatonContext cont

using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(context.AccessToken!);
var bytes = Encoding.UTF8.GetBytes(context.AccessToken);
var hash = sha.ComputeHash(bytes);

var accessTokenHash = Base64Url.Encode(hash);
Expand Down Expand Up @@ -260,15 +271,15 @@ protected virtual async Task ValidatePayloadAsync(DPoPProofValidatonContext cont
return;
}

if (result.Payload.TryGetValue(JwtClaimTypes.IssuedAt, out object? iat))
if (result.Payload.TryGetValue(JwtClaimTypes.IssuedAt, out var iat))
{
if (iat is int iatint)
if (iat is int)
{
result.IssuedAt = iatint;
result.IssuedAt = (int) iat;
}
if (iat is long iatlong)
if (iat is long)
{
result.IssuedAt = iatlong;
result.IssuedAt = (long) iat;
}
}

Expand Down Expand Up @@ -307,7 +318,7 @@ protected virtual async Task ValidateReplayAsync(DPoPProofValidatonContext conte
{
var dpopOptions = OptionsMonitor.Get(context.Scheme);

if (await ReplayCache.ExistsAsync(ReplayCachePurpose, result.TokenId!))
if (await ReplayCache.ExistsAsync(ReplayCachePurpose, result.TokenId!)) // jti is required by an earlier validation
{
result.IsError = true;
result.ErrorDescription = "Detected DPoP proof token replay.";
Expand Down Expand Up @@ -369,7 +380,7 @@ protected virtual Task ValidateIatAsync(DPoPProofValidatonContext context, DPoPP
{
var dpopOptions = OptionsMonitor.Get(context.Scheme);

if (IsExpired(context, result, dpopOptions.ClientClockSkew, result.IssuedAt!.Value))
if (IsExpired(context, result, dpopOptions.ClientClockSkew, result.IssuedAt!.Value)) // iat is required by an earlier validation
{
result.IsError = true;
result.ErrorDescription = "Invalid 'iat' value.";
Expand Down Expand Up @@ -435,7 +446,7 @@ protected virtual ValueTask<long> GetUnixTimeFromNonceAsync(DPoPProofValidatonCo
{
try
{
var value = DataProtector.Unprotect(result.Nonce!);
var value = DataProtector.Unprotect(result.Nonce!); // nonce is required by an earlier validation
if (Int64.TryParse(value, out long iat))
{
return ValueTask.FromResult(iat);
Expand Down
Loading

0 comments on commit 55c438f

Please sign in to comment.