Skip to content

Commit b08f3cc

Browse files
committed
Add issuer and expiration validation for API keys
1 parent 8e3a19e commit b08f3cc

File tree

22 files changed

+498
-205
lines changed

22 files changed

+498
-205
lines changed

documentation/api-key-format.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@ The header (decoded from the token above) must contain at least 2 elements: `alg
3131
> 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.
3232
3333
### Payload
34-
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.
34+
The payload (also decoded from the token above) must contain at least 4 elements: `aud` , `exp` `iss`, and `sub`.
35+
- 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.
36+
- 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.
37+
- 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`.
38+
- 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.
39+
40+
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.
3541

36-
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.
3742
```json
3843
{
3944
"aud": "https://github.com/dotnet/dotnet-monitor",
45+
"exp": "1713799523",
4046
"iss": "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey",
4147
"sub": "ae5473b6-8dad-498d-b915-ffffffffffff"
4248
}

documentation/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,13 @@
449449
"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.",
450450
"minLength": 1,
451451
"pattern": "[0-9a-zA-Z_-]+"
452+
},
453+
"Issuer": {
454+
"type": [
455+
"null",
456+
"string"
457+
],
458+
"description": "The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token)."
452459
}
453460
}
454461
},

src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,10 @@ internal sealed class MonitorApiKeyOptions
2020
[RegularExpression("[0-9a-zA-Z_-]+")]
2121
[Required]
2222
public string PublicKey { get; set; }
23+
24+
[Display(
25+
ResourceType = typeof(OptionsDisplayStrings),
26+
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_MonitorApiKeyOptions_Issuer))]
27+
public string Issuer { get; set; }
2328
}
2429
}

src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,4 +769,8 @@
769769
<data name="DisplayAttributeDescription_CollectExceptionsOptions_Filters" xml:space="preserve">
770770
<value>The filters that determine which exceptions should be included/excluded when collecting exceptions.</value>
771771
</data>
772+
<data name="DisplayAttributeDescription_MonitorApiKeyOptions_Issuer" xml:space="preserve">
773+
<value>The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token).</value>
774+
<comment>The description provided for the Issuer parameter on MonitorApiKeyOptions.</comment>
775+
</data>
772776
</root>

src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45

56
namespace Microsoft.Diagnostics.Monitoring.WebApi
67
{
@@ -16,7 +17,10 @@ public static class AuthConstants
1617
public const string ApiKeyJwtInternalIssuer = "https://github.com/dotnet/dotnet-monitor/generatekey+MonitorApiKey";
1718
public const string ApiKeyJwtAudience = "https://github.com/dotnet/dotnet-monitor";
1819
public const string ClaimAudienceStr = "aud";
20+
public const string ClaimExpirationStr = "exp";
1921
public const string ClaimIssuerStr = "iss";
2022
public const string ClaimSubjectStr = "sub";
23+
24+
public static readonly TimeSpan ApiKeyJwtDefaultExpiration = TimeSpan.FromDays(7);
2125
}
2226
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Diagnostics.Monitoring.TestCommon;
5+
using Microsoft.IdentityModel.Tokens;
6+
using System;
7+
using System.IdentityModel.Tokens.Jwt;
8+
using System.Security.Cryptography;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization;
11+
12+
namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests
13+
{
14+
internal sealed class ApiKeySignInfo
15+
{
16+
public readonly JwtHeader Header;
17+
public readonly string PublicKeyEncoded;
18+
public readonly string PrivateKeyEncoded;
19+
20+
private ApiKeySignInfo(JwtHeader header, string publicKeyEncoded, string privateKeyEncoded)
21+
{
22+
Header = header;
23+
PublicKeyEncoded = publicKeyEncoded;
24+
PrivateKeyEncoded = privateKeyEncoded;
25+
}
26+
27+
public static ApiKeySignInfo Create(string algorithmName)
28+
{
29+
SigningCredentials signingCreds;
30+
JsonWebKey exportableJwk;
31+
JsonWebKey privateJwk;
32+
switch (algorithmName)
33+
{
34+
case SecurityAlgorithms.EcdsaSha256:
35+
case SecurityAlgorithms.EcdsaSha256Signature:
36+
case SecurityAlgorithms.EcdsaSha384:
37+
case SecurityAlgorithms.EcdsaSha384Signature:
38+
case SecurityAlgorithms.EcdsaSha512:
39+
case SecurityAlgorithms.EcdsaSha512Signature:
40+
ECDsa ecDsa = ECDsa.Create(GetEcCurveFromName(algorithmName));
41+
ECDsaSecurityKey ecSecKey = new ECDsaSecurityKey(ecDsa);
42+
signingCreds = new SigningCredentials(ecSecKey, algorithmName);
43+
ECDsa pubEcDsa = ECDsa.Create(ecDsa.ExportParameters(false));
44+
ECDsaSecurityKey pubEcSecKey = new ECDsaSecurityKey(pubEcDsa);
45+
exportableJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(pubEcSecKey);
46+
privateJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(ecSecKey);
47+
break;
48+
49+
case SecurityAlgorithms.RsaSha256:
50+
case SecurityAlgorithms.RsaSha256Signature:
51+
case SecurityAlgorithms.RsaSha384:
52+
case SecurityAlgorithms.RsaSha384Signature:
53+
case SecurityAlgorithms.RsaSha512:
54+
case SecurityAlgorithms.RsaSha512Signature:
55+
RSA rsa = RSA.Create(GetRsaKeyLengthFromName(algorithmName));
56+
RsaSecurityKey rsaSecKey = new RsaSecurityKey(rsa);
57+
signingCreds = new SigningCredentials(rsaSecKey, algorithmName);
58+
RSA pubRsa = RSA.Create(rsa.ExportParameters(false)); // lgtm[cs/weak-asymmetric-algorithm] Intentional testing rejection of weak algorithm
59+
RsaSecurityKey pubRsaSecKey = new RsaSecurityKey(pubRsa);
60+
exportableJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(pubRsaSecKey);
61+
privateJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(rsaSecKey);
62+
break;
63+
64+
case SecurityAlgorithms.HmacSha256:
65+
case SecurityAlgorithms.HmacSha384:
66+
case SecurityAlgorithms.HmacSha512:
67+
HMAC hmac = GetHmacAlgorithmFromName(algorithmName);
68+
SymmetricSecurityKey hmacSecKey = new SymmetricSecurityKey(hmac.Key);
69+
signingCreds = new SigningCredentials(hmacSecKey, algorithmName);
70+
exportableJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey);
71+
privateJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey);
72+
break;
73+
74+
default:
75+
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
76+
}
77+
78+
JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonIgnoreCondition.WhenWritingNull);
79+
80+
string publicKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions);
81+
string publicKeyEncoded = Base64UrlEncoder.Encode(publicKeyJson);
82+
83+
string privateKeyJson = JsonSerializer.Serialize(privateJwk, serializerOptions);
84+
string privateKeyEncoded = Base64UrlEncoder.Encode(privateKeyJson);
85+
86+
JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType);
87+
88+
return new ApiKeySignInfo(newHeader, publicKeyEncoded, privateKeyEncoded);
89+
}
90+
91+
private static HMAC GetHmacAlgorithmFromName(string algorithmName)
92+
{
93+
switch (algorithmName)
94+
{
95+
case SecurityAlgorithms.HmacSha256:
96+
return new HMACSHA256();
97+
case SecurityAlgorithms.HmacSha384:
98+
return new HMACSHA384();
99+
case SecurityAlgorithms.HmacSha512:
100+
return new HMACSHA512();
101+
default:
102+
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
103+
}
104+
}
105+
106+
private static int GetRsaKeyLengthFromName(string algorithmName)
107+
{
108+
switch (algorithmName)
109+
{
110+
case SecurityAlgorithms.RsaSha256:
111+
case SecurityAlgorithms.RsaSha256Signature:
112+
return 2048;
113+
case SecurityAlgorithms.RsaSha384:
114+
case SecurityAlgorithms.RsaSha384Signature:
115+
return 3072;
116+
case SecurityAlgorithms.RsaSha512:
117+
case SecurityAlgorithms.RsaSha512Signature:
118+
return 4096;
119+
default:
120+
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
121+
}
122+
}
123+
124+
private static ECCurve GetEcCurveFromName(string algorithmName)
125+
{
126+
switch (algorithmName)
127+
{
128+
case SecurityAlgorithms.EcdsaSha256:
129+
case SecurityAlgorithms.EcdsaSha256Signature:
130+
return ECCurve.NamedCurves.nistP256;
131+
case SecurityAlgorithms.EcdsaSha384:
132+
case SecurityAlgorithms.EcdsaSha384Signature:
133+
return ECCurve.NamedCurves.nistP384;
134+
case SecurityAlgorithms.EcdsaSha512:
135+
case SecurityAlgorithms.EcdsaSha512Signature:
136+
return ECCurve.NamedCurves.nistP521;
137+
default:
138+
throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
139+
}
140+
}
141+
}
142+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IdentityModel.Tokens.Jwt;
5+
6+
namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests
7+
{
8+
internal sealed class ApiKeyToken
9+
{
10+
public static string Create(ApiKeySignInfo signInfo, JwtPayload customPayload)
11+
{
12+
JwtSecurityToken newToken = new JwtSecurityToken(signInfo.Header, customPayload);
13+
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
14+
return tokenHandler.WriteToken(newToken);
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)