Skip to content

Commit

Permalink
Add issuer and expiration validation for API keys (#6456)
Browse files Browse the repository at this point in the history
  • Loading branch information
jander-msft authored Apr 22, 2024
1 parent be1b830 commit e22a67d
Show file tree
Hide file tree
Showing 22 changed files with 500 additions and 205 deletions.
10 changes: 8 additions & 2 deletions documentation/api-key-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ The header (decoded from the token above) must contain at least 2 elements: `alg
> The `alg` requirement is designed to enforce `dotnet monitor` to use public/private key signed tokens. This allows the key that is stored in configuration (as `Authentication__MonitorApiKey__PublicKey`) to only contain public key information and thus does not need to be kept secret.
### Payload
The payload (also decoded from the token above) must contain at least 2 elements: `aud` (or [Audience](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3)), and `sub` (or [Subject](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2)). `dotnet monitor` expects the `aud` to always be `https://github.com/dotnet/dotnet-monitor` which signals that the token is intended for dotnet-monitor. The `sub` field is any non-empty string defined in `Authentication__MonitorApiKey__Subject`, this is used to validate that the token provided is for the expected instance and is user-defined in configuration.
The payload (also decoded from the token above) must contain at least 4 elements: `aud` , `exp` `iss`, and `sub`.
- The `aud` field ([Audience](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3)) must to always be `https://github.com/dotnet/dotnet-monitor` which signals that the token is intended for dotnet-monitor.
- The `exp` field ([Expiration](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4)) is the expiration date of the token in the form of an integer that is the number of seconds since Unix epoch.
- The `iss` field ([Issuer](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)) is any non-empty string defined in `Authentication__MonitorApiKey__Issuer`, this is used to validate that the token provided was produced by the expected issuer. If `Authentication__MonitorApiKey__Issuer` is not specified, the value in the token must be `https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey`.
- The `sub` field ([Subject](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2)) is any non-empty string defined in `Authentication__MonitorApiKey__Subject`, this is used to validate that the token provided is for the expected instance and is user-defined in configuration.

When using the `generatekey` command, the `sub` field will be a randomly-generated `Guid` but the `sub` field may be any non-empty string that matches the configuration. The `iss` (or [Issuer](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)) field will be set to `https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey` to specify the source of the token.

When using the `generatekey` command, the `sub` field will be a randomly-generated `Guid` but the `sub` field may be any non-empty string that matches the configuration. The `iss` (or [Issuer](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)) field will be set to `https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey` to specify the source of the token, however `dotnet monitor` will accept any `iss` field value, and does not need to be present.
```json
{
"aud": "https://github.com/dotnet/dotnet-monitor",
"exp": "1713799523",
"iss": "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey",
"sub": "ae5473b6-8dad-498d-b915-ffffffffffff"
}
Expand Down
7 changes: 7 additions & 0 deletions documentation/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@
"description": "The public key used to sign the JWT (JSON Web Token) used for authentication. This field is a JSON Web Key serialized as JSON encoded with base64Url encoding. The JWK must have a kty field of RSA or EC and should not have the private key information.",
"minLength": 1,
"pattern": "[0-9a-zA-Z_-]+"
},
"Issuer": {
"type": [
"null",
"string"
],
"description": "The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token)."
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ internal sealed class MonitorApiKeyOptions
[RegularExpression("[0-9a-zA-Z_-]+")]
[Required]
public string PublicKey { get; set; }

[Display(
ResourceType = typeof(OptionsDisplayStrings),
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_MonitorApiKeyOptions_Issuer))]
public string Issuer { get; set; }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -769,4 +769,8 @@
<data name="DisplayAttributeDescription_CollectExceptionsOptions_Filters" xml:space="preserve">
<value>The filters that determine which exceptions should be included/excluded when collecting exceptions.</value>
</data>
<data name="DisplayAttributeDescription_MonitorApiKeyOptions_Issuer" xml:space="preserve">
<value>The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token).</value>
<comment>The description provided for the Issuer parameter on MonitorApiKeyOptions.</comment>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Diagnostics.Monitoring.WebApi
{
Expand All @@ -16,7 +17,10 @@ public static class AuthConstants
public const string ApiKeyJwtInternalIssuer = "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey";
public const string ApiKeyJwtAudience = "https://github.com/dotnet/dotnet-monitor";
public const string ClaimAudienceStr = "aud";
public const string ClaimExpirationStr = "exp";
public const string ClaimIssuerStr = "iss";
public const string ClaimSubjectStr = "sub";

public static readonly TimeSpan ApiKeyJwtDefaultExpiration = TimeSpan.FromDays(7);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Diagnostics.Monitoring.TestCommon;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests
{
internal sealed class ApiKeySignInfo
{
public readonly JwtHeader Header;
public readonly string PublicKeyEncoded;
public readonly string PrivateKeyEncoded;

private ApiKeySignInfo(JwtHeader header, string publicKeyEncoded, string privateKeyEncoded)
{
Header = header;
PublicKeyEncoded = publicKeyEncoded;
PrivateKeyEncoded = privateKeyEncoded;
}

public static ApiKeySignInfo Create(string algorithmName)
{
SigningCredentials signingCreds;
JsonWebKey exportableJwk;
JsonWebKey privateJwk;
switch (algorithmName)
{
case SecurityAlgorithms.EcdsaSha256:
case SecurityAlgorithms.EcdsaSha256Signature:
case SecurityAlgorithms.EcdsaSha384:
case SecurityAlgorithms.EcdsaSha384Signature:
case SecurityAlgorithms.EcdsaSha512:
case SecurityAlgorithms.EcdsaSha512Signature:
ECDsa ecDsa = ECDsa.Create(GetEcCurveFromName(algorithmName));
ECDsaSecurityKey ecSecKey = new ECDsaSecurityKey(ecDsa);
signingCreds = new SigningCredentials(ecSecKey, algorithmName);
ECDsa pubEcDsa = ECDsa.Create(ecDsa.ExportParameters(false));
ECDsaSecurityKey pubEcSecKey = new ECDsaSecurityKey(pubEcDsa);
exportableJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(pubEcSecKey);
privateJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(ecSecKey);
break;

case SecurityAlgorithms.RsaSha256:
case SecurityAlgorithms.RsaSha256Signature:
case SecurityAlgorithms.RsaSha384:
case SecurityAlgorithms.RsaSha384Signature:
case SecurityAlgorithms.RsaSha512:
case SecurityAlgorithms.RsaSha512Signature:
RSA rsa = RSA.Create(GetRsaKeyLengthFromName(algorithmName));
RsaSecurityKey rsaSecKey = new RsaSecurityKey(rsa);
signingCreds = new SigningCredentials(rsaSecKey, algorithmName);
RSA pubRsa = RSA.Create(rsa.ExportParameters(false)); // lgtm[cs/weak-asymmetric-algorithm] Intentional testing rejection of weak algorithm
RsaSecurityKey pubRsaSecKey = new RsaSecurityKey(pubRsa);
exportableJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(pubRsaSecKey);
privateJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(rsaSecKey);
break;

case SecurityAlgorithms.HmacSha256:
case SecurityAlgorithms.HmacSha384:
case SecurityAlgorithms.HmacSha512:
HMAC hmac = GetHmacAlgorithmFromName(algorithmName);
SymmetricSecurityKey hmacSecKey = new SymmetricSecurityKey(hmac.Key);
signingCreds = new SigningCredentials(hmacSecKey, algorithmName);
exportableJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey);
privateJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey);
break;

default:
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
}

JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonIgnoreCondition.WhenWritingNull);

string publicKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions);
string publicKeyEncoded = Base64UrlEncoder.Encode(publicKeyJson);

string privateKeyJson = JsonSerializer.Serialize(privateJwk, serializerOptions);
string privateKeyEncoded = Base64UrlEncoder.Encode(privateKeyJson);

JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType);

return new ApiKeySignInfo(newHeader, publicKeyEncoded, privateKeyEncoded);
}

private static HMAC GetHmacAlgorithmFromName(string algorithmName)
{
switch (algorithmName)
{
case SecurityAlgorithms.HmacSha256:
return new HMACSHA256();
case SecurityAlgorithms.HmacSha384:
return new HMACSHA384();
case SecurityAlgorithms.HmacSha512:
return new HMACSHA512();
default:
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
}
}

private static int GetRsaKeyLengthFromName(string algorithmName)
{
switch (algorithmName)
{
case SecurityAlgorithms.RsaSha256:
case SecurityAlgorithms.RsaSha256Signature:
return 2048;
case SecurityAlgorithms.RsaSha384:
case SecurityAlgorithms.RsaSha384Signature:
return 3072;
case SecurityAlgorithms.RsaSha512:
case SecurityAlgorithms.RsaSha512Signature:
return 4096;
default:
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
}
}

private static ECCurve GetEcCurveFromName(string algorithmName)
{
switch (algorithmName)
{
case SecurityAlgorithms.EcdsaSha256:
case SecurityAlgorithms.EcdsaSha256Signature:
return ECCurve.NamedCurves.nistP256;
case SecurityAlgorithms.EcdsaSha384:
case SecurityAlgorithms.EcdsaSha384Signature:
return ECCurve.NamedCurves.nistP384;
case SecurityAlgorithms.EcdsaSha512:
case SecurityAlgorithms.EcdsaSha512Signature:
return ECCurve.NamedCurves.nistP521;
default:
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IdentityModel.Tokens.Jwt;

namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests
{
internal sealed class ApiKeyToken
{
public static string Create(ApiKeySignInfo signInfo, JwtPayload customPayload)
{
JwtSecurityToken newToken = new JwtSecurityToken(signInfo.Header, customPayload);
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
return tokenHandler.WriteToken(newToken);
}
}
}
Loading

0 comments on commit e22a67d

Please sign in to comment.