Skip to content

Commit

Permalink
Copy and fork IdentityModel DPoP extensions to fix IdentityModel/Iden…
Browse files Browse the repository at this point in the history
  • Loading branch information
blowdart committed Jan 6, 2025
1 parent ce3be7c commit 4b2dc52
Show file tree
Hide file tree
Showing 14 changed files with 545 additions and 22 deletions.
28 changes: 28 additions & 0 deletions samples/Samples.OAuth/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using idunno.Bluesky;

using Samples.Common;
using IdentityModel;
using Microsoft.IdentityModel.JsonWebTokens;

namespace Samples.OAuth
{
Expand Down Expand Up @@ -96,6 +98,32 @@ static async Task PerformOperations(string? handle, string? password, string? au
LoginResult loginResult = await loginClient.ProcessOAuth2Response(queryString, cancellationToken: cancellationToken);

Console.WriteLine($"Succeeded : {!loginResult.IsError}");

if (!loginResult.IsError)
{
JsonWebToken accessToken = new (loginResult.AccessToken);

Uri issuer = new (accessToken.Issuer);

if (!issuer.Equals(authorizationServer))
{
ConsoleColor currentColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Invalid issuer, expected {authorizationServer}, received {accessToken.Issuer}");
Console.ForegroundColor = currentColor;
return;
}

if (accessToken.Subject is null || accessToken.Subject != did)
{
ConsoleColor currentColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Invalid subject, expected {did}, received {accessToken.Subject}");
Console.ForegroundColor = currentColor;
return;
}
}

Debugger.Break();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Linq;
using System.Net.Http;

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Extensions for HTTP request/response messages
/// </summary>
public static class DPoPExtensions
{
/// <summary>
/// Sets the DPoP nonce request header if nonce is not null.
/// </summary>
public static void SetDPoPProofToken(this HttpRequestMessage request, string? proofToken)
{
ArgumentNullException.ThrowIfNull(request);

// remove any old headers
request.Headers.Remove(OidcConstants.HttpHeaders.DPoP);
// set new header
request.Headers.Add(OidcConstants.HttpHeaders.DPoP, proofToken);
}

/// <summary>
/// Reads the DPoP nonce header from the response
/// </summary>
public static string? GetDPoPNonce(this HttpResponseMessage response)
{
ArgumentNullException.ThrowIfNull(response);


var nonce = response.Headers
.FirstOrDefault(x => x.Key == OidcConstants.HttpHeaders.DPoPNonce)
.Value?.FirstOrDefault();
return nonce;
}

/// <summary>
/// Returns the URL without any query params
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "Forked code")]
public static string GetDPoPUrl(this HttpRequestMessage request)
{
ArgumentNullException.ThrowIfNull(request);

return request.RequestUri!.Scheme + "://" + request.RequestUri!.Authority + request.RequestUri!.LocalPath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Models a DPoP proof token
/// </summary>
public class DPoPProof
{
/// <summary>
/// The proof token
/// </summary>
public string ProofToken { get; set; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Text.Json.Serialization;

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Internal class to aid serialization of DPoP proof token payloads. Giving
/// each claim a property allows us to add this type to the source generated
/// serialization
/// </summary>
internal class DPoPProofPayload
{
[JsonPropertyName(JwtClaimTypes.JwtId)]
public string JwtId { get; set; } = default!;
[JsonPropertyName(JwtClaimTypes.DPoPHttpMethod)]
public string DPoPHttpMethod { get; set; } = default!;
[JsonPropertyName(JwtClaimTypes.DPoPHttpUrl)]
public string DPoPHttpUrl { get; set; } = default!;
[JsonPropertyName(JwtClaimTypes.IssuedAt)]
public long IssuedAt { get; set; }
[JsonPropertyName(JwtClaimTypes. DPoPAccessTokenHash)]
public string? DPoPAccessTokenHash { get; set; }
[JsonPropertyName(JwtClaimTypes. Nonce)]
public string? Nonce { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Models the request information to create a DPoP proof token
/// </summary>
public class DPoPProofRequest
{
/// <summary>
/// The HTTP URL of the request
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "Forked Code")]
public string Url { get; set; } = default!;

/// <summary>
/// The HTTP method of the request
/// </summary>
public string Method { get; set; } = default!;

/// <summary>
/// The nonce value for the DPoP proof token.
/// </summary>
public string? DPoPNonce { get; set; }

/// <summary>
/// The access token
/// </summary>
public string? AccessToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Used to create DPoP proof tokens.
/// </summary>
public class DPoPProofTokenFactory
{
private readonly JsonWebKey _jwk;

/// <summary>
/// Constructor
/// </summary>
public DPoPProofTokenFactory(string proofKey)
{
_jwk = new JsonWebKey(proofKey);

if (string.IsNullOrEmpty(_jwk.Alg))
{
throw new ArgumentException("alg must be set on proof key");
}
}

/// <summary>
/// Creates a DPoP proof token.
/// </summary>
public DPoPProof CreateProofToken(DPoPProofRequest request)
{
ArgumentNullException.ThrowIfNull(request);

var jsonWebKey = _jwk;

// jwk: representing the public key chosen by the client, in JSON Web Key (JWK) [RFC7517] format,
// as defined in Section 4.1.3 of [RFC7515]. MUST NOT contain a private key.
Dictionary<string, object> jwk;
if (string.Equals(jsonWebKey.Kty, JsonWebAlgorithmsKeyTypes.EllipticCurve, StringComparison.Ordinal))
{
jwk = new Dictionary<string, object>
{
{ "kty", jsonWebKey.Kty },
{ "x", jsonWebKey.X },
{ "y", jsonWebKey.Y },
{ "crv", jsonWebKey.Crv }
};
}
else if (string.Equals(jsonWebKey.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal))
{
jwk = new Dictionary<string, object>
{
{ "kty", jsonWebKey.Kty },
{ "e", jsonWebKey.E },
{ "n", jsonWebKey.N }
};
}
else
{
throw new InvalidOperationException("invalid key type.");
}

var header = new Dictionary<string, object>()
{
{ "typ", JwtClaimTypes.JwtTypes.DPoPProofToken },
{ JwtClaimTypes.JsonWebKey, jwk },
};

var payload = new DPoPProofPayload
{
JwtId = CryptoRandom.CreateUniqueId(),
DPoPHttpMethod = request.Method,
DPoPHttpUrl = request.Url,
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};

if (!string.IsNullOrWhiteSpace(request.AccessToken))
{
// ath: hash of the access token. The value MUST be the result of a base64url encoding
// the SHA-256 hash of the ASCII encoding of the associated access token's value.
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(request.AccessToken));
var ath = Base64Url.Encode(hash);

payload.DPoPAccessTokenHash = ath;
}

if (!string.IsNullOrEmpty(request.DPoPNonce))
{
payload.Nonce = request.DPoPNonce!;
}

var handler = new JsonWebTokenHandler() { SetDefaultTimesOnTokenCreation = false };
var key = new SigningCredentials(jsonWebKey, jsonWebKey.Alg);
var proofToken = handler.CreateToken(JsonSerializer.Serialize(payload, SourceGenerationContext.Default.DPoPProofPayload), key, header);

return new DPoPProof { ProofToken = proofToken! };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;

namespace IdentityModel.OidcClient.DPoP;

/// <summary>
/// Helper to create JSON web keys.
/// </summary>
public static class JsonWebKeys
{
/// <summary>
/// Creates a new RSA JWK.
/// </summary>
public static JsonWebKey CreateRsa(string algorithm = OidcConstants.Algorithms.Asymmetric.PS256)
{
using (RSA rsa = RSA.Create())
{
var rsaKey = new RsaSecurityKey(rsa);

var jwk = JsonWebKeyConverter.ConvertFromSecurityKey(rsaKey);
jwk.Alg = algorithm;

return jwk;
}
}

/// <summary>
/// Creates a new RSA JWK string.
/// </summary>
public static string CreateRsaJson(string algorithm = OidcConstants.Algorithms.Asymmetric.PS256)
{
return JsonSerializer.Serialize(CreateRsa(algorithm), SourceGenerationContext.Default.JsonWebKey);
}

/// <summary>
/// Creates a new ECDSA JWK.
/// </summary>
public static JsonWebKey CreateECDsa(string algorithm = OidcConstants.Algorithms.Asymmetric.ES256)
{
using (ECDsa ecdsa = ECDsa.Create(GetCurveFromCrvValue(GetCurveNameFromSigningAlgorithm(algorithm))))
{

var ecKey = new ECDsaSecurityKey(ecdsa);
JsonWebKey jwk = JsonWebKeyConverter.ConvertFromSecurityKey(ecKey);
jwk.Alg = algorithm;

return jwk;
}
}

/// <summary>
/// Creates a new ECDSA JWK string.
/// </summary>
public static string CreateECDsaJson(string algorithm = OidcConstants.Algorithms.Asymmetric.ES256)
{
return JsonSerializer.Serialize(CreateECDsa(algorithm), SourceGenerationContext.Default.JsonWebKey);
}

internal static string GetCurveNameFromSigningAlgorithm(string alg)
{
return alg switch
{
"ES256" => "P-256",
"ES384" => "P-384",
"ES512" => "P-521",
_ => throw new InvalidOperationException($"Unsupported alg type of {alg}"),
};
}

/// <summary>
/// Returns the matching named curve for RFC 7518 crv value
/// </summary>
internal static ECCurve GetCurveFromCrvValue(string crv)
{
return crv switch
{
JsonWebKeyECTypes.P256 => ECCurve.NamedCurves.nistP256,
JsonWebKeyECTypes.P384 => ECCurve.NamedCurves.nistP384,
JsonWebKeyECTypes.P521 => ECCurve.NamedCurves.nistP521,
_ => throw new InvalidOperationException($"Unsupported curve type of {crv}"),
};
}
}
Loading

0 comments on commit 4b2dc52

Please sign in to comment.