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

LFT-1285 - Ignore use=enc JWKs in JWKS parsing #364

Merged
merged 12 commits into from
Jul 31, 2024
12 changes: 11 additions & 1 deletion src/D2L.Security.OAuth2/Keys/Default/JsonWebKeySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,19 @@ public JsonWebKeySet( string json, Uri src ) {
#endif

var builder = ImmutableArray.CreateBuilder<JsonWebKey>();

foreach( object keyObject in keyObjects ) {
string keyJson = JsonSerializer.Serialize( keyObject );
JsonWebKey key = JsonWebKey.FromJson( keyJson );

if( !JsonWebKey.TryParseJsonWebKey( keyJson, out var key, out var error, out var exception, out var useEncKey ) ) {
if( useEncKey ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtering could apply more generally but for now I just want to do the minimal thing to get the integrations working. I think the API needs a redesign overall.

// Just filter out keys meant for encryption
continue;
} else {
throw new JsonWebKeyParseException( error, exception );
}
}

builder.Add( key );
}
m_keys = builder.ToImmutable();
Expand Down
113 changes: 94 additions & 19 deletions src/D2L.Security.OAuth2/Keys/JsonWebKey.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using D2L.Security.OAuth2.Keys.Default;

#if NET6_0
Expand Down Expand Up @@ -60,81 +63,153 @@
/// <param name="json">The json JWK</param>
/// <returns>A <see cref="JsonWebKey"/></returns>
public static JsonWebKey FromJson( string json ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintaining this API + behaviour for people that call this directly (> 0 in our code)

if( !TryParseJsonWebKey( json, out var result, out var error, out var e, out _ ) ) {
throw new JsonWebKeyParseException( error, e );
}

return result;
}

public static bool TryParseJsonWebKey(
string json,
[NotNullWhen( true )]
out JsonWebKey? result,
[NotNullWhen( false )]
out string? error,
out Exception? exception,
out bool useEncKey
) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly changed to TryX from throwing, but also added this separate useEncKey signal.

Dictionary<string, object> data;
try {
data = JsonSerializer.Deserialize<Dictionary<string, object>>( json );
} catch ( JsonException e ) {
throw new JsonWebKeyParseException( "error deserializing JSON web key string", e );
result = null;
error = "error deserializing JSON web key string";
exception = e;
useEncKey = false;
return false;
}

if( data.ContainsKey( "use" ) && data[ "use" ] != null && data[ "use" ].ToString() != "sig" ) {
string msg = String.Format( "invalid 'use' value in JSON web key: {0}", data[ "use" ] );
throw new JsonWebKeyParseException( msg );
result = null;
error = "invalid 'use' value in JSON web key: " + data[ "use" ];
exception = null;
useEncKey = data[ "use" ].ToString() == "enc";
return false;
}
Comment on lines 93 to 99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe make this ignored in general (and not just "enc")? Perhaps a ParseResult { Success, Error, Ignore } vs bool?

It would ultimately be nice to be able to say "the key {kid} is not intended for singnatures" rather than "the key {kid} was not found" though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so I mentioned in the PR description basically I think our parser should just parse and deciding if a key was valid or not ought to be up to the user.

All of our users currently only want signing keys so I think it's OK practically to just filter them (vs. exploding) but I only exposed that on JWKS because the API for getting one key implies it will never return null (it throws currently) and it gets used e.g. parsing keys from the database, and I didn't want to add sig=enc logic for right now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words rather than returning ignore from here I'd prefer if we just successfully parsed all valid JWKs and then moved the policy decisions elsewhere


if( !data.ContainsKey( "kty" ) ) {
throw new JsonWebKeyParseException( "missing 'kty' parameter in JSON web key" );
result = null;
error = "missing 'kty' parameter in JSON web key";
exception = null;
useEncKey = false;
return false;
}

if( !data.ContainsKey( "kid" ) ) {
throw new JsonWebKeyParseException( "missing 'kid' parameter in JSON web key" );
result = null;
error = "missing 'kid' parameter in JSON web key";
exception = null;
useEncKey = false;
return false;
}

string id = data[ "kid" ].ToString();

Check warning on line 117 in src/D2L.Security.OAuth2/Keys/JsonWebKey.cs

View workflow job for this annotation

GitHub Actions / Build and test (Windows)

Converting null literal or possible null value to non-nullable type.

Check warning on line 117 in src/D2L.Security.OAuth2/Keys/JsonWebKey.cs

View workflow job for this annotation

GitHub Actions / Build and test (Windows)

Converting null literal or possible null value to non-nullable type.

Check warning on line 117 in src/D2L.Security.OAuth2/Keys/JsonWebKey.cs

View workflow job for this annotation

GitHub Actions / Build and test (Linux)

Converting null literal or possible null value to non-nullable type.
DateTimeOffset? expiresAt = null;
if( data.ContainsKey( "exp" ) ) {
if( !long.TryParse( data[ "exp" ].ToString(), out long ts ) ) {
string msg = String.Format( "invalid 'exp' value in JSON web key: {0}", data[ "exp" ] );
throw new JsonWebKeyParseException( msg );
result = null;
error = "invalid 'exp' value in JSON web key: " + data[ "exp" ];
exception = null;
useEncKey = false;
return false;
}
expiresAt = DateTimeOffset.FromUnixTimeSeconds( ts );
}

switch( data[ "kty" ].ToString() ) {
case "RSA":
if( !data.ContainsKey( "n" ) ) {
throw new JsonWebKeyParseException( "missing 'n' parameter in RSA JSON web key" );
result = null;
error = "missing 'n' parameter in RSA JSON web key";
exception = null;
useEncKey = false;
return false;
}

if( !data.ContainsKey( "e" ) ) {
throw new JsonWebKeyParseException( "missing 'e' parameter in RSA JSON web key" );
result = null;
error = "missing 'e' parameter in RSA JSON web key";
exception = null;
useEncKey = false;
return false;
}

if( HasRsaPrivateKeyMaterial( data ) ) {
throw new JsonWebKeyParseException( "RSA JSON web key has private key material" );
result = null;
error = "RSA JSON web key has private key material";
exception = null;
useEncKey = false;
return false;
}

return new RsaJsonWebKey(
result = new RsaJsonWebKey(
id: id,
expiresAt: expiresAt,
n: data[ "n" ].ToString(),
e: data[ "e" ].ToString()
);

error = null;
exception = null;
useEncKey = false;

return true;

case "EC":
if( !data.ContainsKey( "crv" ) ) {
throw new JsonWebKeyParseException( "missing 'crv' parameter in EC JSON web key" );
result = null;
error = "missing 'crv' parameter in EC JSON web key";
exception = null;
useEncKey = false;
return false;
}

if( !data.ContainsKey( "x" ) ) {
throw new JsonWebKeyParseException( "missing 'x' parameter in EC JSON web key" );
result = null;
error = "missing 'x' parameter in EC JSON web key";
exception = null;
useEncKey = false;
return false;
}

if( !data.ContainsKey( "y" ) ) {
throw new JsonWebKeyParseException( "missing 'y' parameter in EC JSON web key" );
result = null;
error = "missing 'y' parameter in EC JSON web key";
exception = null;
useEncKey = false;
return false;
}

return new EcDsaJsonWebKey(
result = new EcDsaJsonWebKey(
id: id,
expiresAt: expiresAt,
curve: data[ "crv" ].ToString(),
x: data[ "x" ].ToString(),
y: data[ "y" ].ToString()
);

error = null;
exception = null;
useEncKey = false;
return true;

default:
string msg = String.Format( "'{0}' is not a supported JSON eb key type", data[ "kty" ] );
throw new JsonWebKeyParseException( msg );
result = null;
error = $"'{data["kty"]}' is not a supported JSON web key type";
exception = null;
useEncKey = false;
return false;

}
}
Expand Down Expand Up @@ -163,6 +238,6 @@
/// <summary>
/// Constructs a new <see cref="JsonWebKeyParseException"/>
/// </summary>
public JsonWebKeyParseException( string msg, Exception inner ) : base( msg, inner ) { }
public JsonWebKeyParseException( string msg, Exception? inner ) : base( msg, inner ) { }
}
}
10 changes: 10 additions & 0 deletions src/D2L.Security.OAuth2/NotNullWhenAttribute.net4x.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace System.Diagnostics.CodeAnalysis;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Note filepath: .net4x.cs is treated specially via our build.)


[AttributeUsage( AttributeTargets.Parameter )]
internal sealed class NotNullWhenAttribute : Attribute {
public bool ReturnValue { get; }

public NotNullWhenAttribute( bool returnValue ) {
ReturnValue = returnValue;
}
}
Loading