diff --git a/documentation/api-key-format.md b/documentation/api-key-format.md
index 94fce4fba5e..00c44e2a6e9 100644
--- a/documentation/api-key-format.md
+++ b/documentation/api-key-format.md
@@ -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"
}
diff --git a/documentation/schema.json b/documentation/schema.json
index 42e085c4da9..f11d56d8b41 100644
--- a/documentation/schema.json
+++ b/documentation/schema.json
@@ -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)."
}
}
},
diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs
index 3dd7b2a78ee..8f54cce9111 100644
--- a/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs
+++ b/src/Microsoft.Diagnostics.Monitoring.Options/MonitorApiKeyOptions.cs
@@ -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; }
}
}
diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs
index 07c5a1057c0..9b212eb9f5c 100644
--- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs
+++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs
@@ -1252,6 +1252,15 @@ public static string DisplayAttributeDescription_MetricsOptions_Providers {
}
}
+ ///
+ /// Looks up a localized string similar to The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token)..
+ ///
+ public static string DisplayAttributeDescription_MonitorApiKeyOptions_Issuer {
+ get {
+ return ResourceManager.GetString("DisplayAttributeDescription_MonitorApiKeyOptions_Issuer", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to 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..
///
diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx
index 5e5300bb214..f0abc10a457 100644
--- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx
+++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx
@@ -769,4 +769,8 @@
The filters that determine which exceptions should be included/excluded when collecting exceptions.
+
+ The expected value of the 'iss' or Issuer field in the JWT (JSON Web Token).
+ The description provided for the Issuer parameter on MonitorApiKeyOptions.
+
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs
index ce63a2fdcb0..0fb6a509a8a 100644
--- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs
+++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Auth/AuthConstants.cs
@@ -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
{
@@ -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);
}
}
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeySignInfo.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeySignInfo.cs
new file mode 100644
index 00000000000..d833d68e2fb
--- /dev/null
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeySignInfo.cs
@@ -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));
+ }
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeyToken.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeyToken.cs
new file mode 100644
index 00000000000..f302d9474ca
--- /dev/null
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Auth/ApiKey/ApiKeyToken.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs
index 7e8a0df421f..e13ae742e76 100644
--- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs
@@ -13,6 +13,7 @@
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Net.Http;
@@ -330,9 +331,9 @@ public async Task RejectsBadAudience()
Guid subject = Guid.NewGuid();
string subjectStr = subject.ToString("D");
const string BadApiKeyJwtAudience = "SomeOtherAudience";
- JwtPayload newPayload = GetJwtPayload(BadApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer);
+ JwtPayload newPayload = GetJwtPayload(BadApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
- toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey);
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string token);
// Start dotnet-monitor
await toolRunner.StartAsync();
@@ -340,7 +341,7 @@ public async Task RejectsBadAudience()
// Create HttpClient with default request headers
using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey);
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
ApiClient apiClient = new(_outputHelper, httpClient);
var statusCodeException = await Assert.ThrowsAsync(
@@ -358,9 +359,9 @@ public async Task RejectsMissingAudience()
Guid subject = Guid.NewGuid();
string subjectStr = subject.ToString("D");
- JwtPayload newPayload = GetJwtPayload(null, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer);
+ JwtPayload newPayload = GetJwtPayload(null, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
- toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey);
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string token);
// Start dotnet-monitor
await toolRunner.StartAsync();
@@ -368,7 +369,7 @@ public async Task RejectsMissingAudience()
// Create HttpClient with default request headers
using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey);
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
ApiClient apiClient = new(_outputHelper, httpClient);
var statusCodeException = await Assert.ThrowsAsync(
@@ -377,7 +378,35 @@ public async Task RejectsMissingAudience()
}
[Fact]
- public async Task AllowDifferentIssuer()
+ public async Task RejectsMissingIssuer()
+ {
+ await using MonitorCollectRunner toolRunner = new(_outputHelper);
+ toolRunner.DisableMetricsViaCommandLine = true;
+
+ _outputHelper.WriteLine("Generating API key.");
+
+ Guid subject = Guid.NewGuid();
+ string subjectStr = subject.ToString("D");
+ JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, issuer: null, AuthConstants.ApiKeyJwtDefaultExpiration);
+
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string token);
+
+ // Start dotnet-monitor
+ await toolRunner.StartAsync();
+
+ // Create HttpClient with default request headers
+ using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
+ httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
+ ApiClient apiClient = new(_outputHelper, httpClient);
+
+ var statusCodeException = await Assert.ThrowsAsync(
+ apiClient.GetProcessesAsync);
+ Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode);
+ }
+
+ [Fact]
+ public async Task RejectsDifferentIssuer()
{
await using MonitorCollectRunner toolRunner = new(_outputHelper);
toolRunner.DisableMetricsViaCommandLine = true;
@@ -387,9 +416,9 @@ public async Task AllowDifferentIssuer()
Guid subject = Guid.NewGuid();
string subjectStr = subject.ToString("D");
const string ApiKeyJwtIssuer = "MyOtherServiceMintingTokens";
- JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, ApiKeyJwtIssuer);
+ JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, ApiKeyJwtIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
- toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string apiKey);
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, subjectStr, newPayload, out string token);
// Start dotnet-monitor
await toolRunner.StartAsync();
@@ -397,11 +426,43 @@ public async Task AllowDifferentIssuer()
// Create HttpClient with default request headers
using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey);
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
ApiClient apiClient = new(_outputHelper, httpClient);
- var processes = await apiClient.GetProcessesAsync();
- Assert.NotNull(processes);
+ var statusCodeException = await Assert.ThrowsAsync(
+ apiClient.GetProcessesAsync);
+ Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode);
+ }
+
+ [Fact]
+ public async Task AllowsConfiguredIssuer()
+ {
+ await using MonitorCollectRunner toolRunner = new(_outputHelper);
+ toolRunner.DisableMetricsViaCommandLine = true;
+
+ _outputHelper.WriteLine("Generating API key.");
+
+ ApiKeySignInfo signInfo = ApiKeySignInfo.Create(SecurityAlgorithms.EcdsaSha384);
+
+ Guid subject = Guid.NewGuid();
+ string subjectStr = subject.ToString("D");
+ const string ApiKeyJwtIssuer = "MyOtherServiceMintingTokens";
+ JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, ApiKeyJwtIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
+
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(signInfo, subjectStr, ApiKeyJwtIssuer);
+
+ string token = ApiKeyToken.Create(signInfo, newPayload);
+
+ // Start dotnet-monitor
+ await toolRunner.StartAsync();
+
+ // Create HttpClient with default request headers
+ using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
+ httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
+ ApiClient apiClient = new(_outputHelper, httpClient);
+
+ await apiClient.GetProcessesAsync();
}
///
@@ -422,9 +483,9 @@ public async Task RejectsBadSubject(string jwtSubject, string configSubject, Htt
_outputHelper.WriteLine("Generating API key.");
- JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, jwtSubject, AuthConstants.ApiKeyJwtInternalIssuer);
+ JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, jwtSubject, AuthConstants.ApiKeyJwtInternalIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
- toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, configSubject, newPayload, out string apiKey);
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(SecurityAlgorithms.EcdsaSha384, configSubject, newPayload, out string token);
// Start dotnet-monitor
await toolRunner.StartAsync();
@@ -432,7 +493,7 @@ public async Task RejectsBadSubject(string jwtSubject, string configSubject, Htt
// Create HttpClient with default request headers
using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey);
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
ApiClient apiClient = new(_outputHelper, httpClient);
var statusCodeException = await Assert.ThrowsAsync(
@@ -440,6 +501,80 @@ public async Task RejectsBadSubject(string jwtSubject, string configSubject, Htt
Assert.Equal(expectedError, statusCodeException.StatusCode);
}
+ [Fact]
+ public async Task RejectsMissingExpiration()
+ {
+ await using MonitorCollectRunner toolRunner = new(_outputHelper);
+ toolRunner.DisableMetricsViaCommandLine = true;
+
+ _outputHelper.WriteLine("Generating API key.");
+
+ ApiKeySignInfo signInfo = ApiKeySignInfo.Create(SecurityAlgorithms.EcdsaSha384);
+
+ Guid subject = Guid.NewGuid();
+ string subjectStr = subject.ToString("D");
+
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(signInfo, subjectStr);
+
+ // Create token without expiration
+ JwtPayload newPayload = GetJwtPayload(
+ AuthConstants.ApiKeyJwtAudience,
+ subjectStr,
+ AuthConstants.ApiKeyJwtInternalIssuer,
+ expiration: null);
+ string token = ApiKeyToken.Create(signInfo, newPayload);
+
+ // Start dotnet-monitor
+ await toolRunner.StartAsync();
+
+ // Create HttpClient with default request headers
+ using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
+ httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
+ ApiClient apiClient = new(_outputHelper, httpClient);
+
+ var statusCodeException = await Assert.ThrowsAsync(
+ apiClient.GetProcessesAsync);
+ Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode);
+ }
+
+ [Fact]
+ public async Task RejectsExpiredToken()
+ {
+ await using MonitorCollectRunner toolRunner = new(_outputHelper);
+ toolRunner.DisableMetricsViaCommandLine = true;
+
+ _outputHelper.WriteLine("Generating API key.");
+
+ ApiKeySignInfo signInfo = ApiKeySignInfo.Create(SecurityAlgorithms.EcdsaSha384);
+
+ Guid subject = Guid.NewGuid();
+ string subjectStr = subject.ToString("D");
+
+ toolRunner.ConfigurationFromEnvironment.UseApiKey(signInfo, subjectStr);
+
+ // Create token that expired yesterday
+ JwtPayload newPayload = GetJwtPayload(
+ AuthConstants.ApiKeyJwtAudience,
+ subjectStr,
+ AuthConstants.ApiKeyJwtInternalIssuer,
+ DateTime.UtcNow.AddDays(-1));
+ string token = ApiKeyToken.Create(signInfo, newPayload);
+
+ // Start dotnet-monitor
+ await toolRunner.StartAsync();
+
+ // Create HttpClient with default request headers
+ using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
+ httpClient.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
+ ApiClient apiClient = new(_outputHelper, httpClient);
+
+ var statusCodeException = await Assert.ThrowsAsync(
+ apiClient.GetProcessesAsync);
+ Assert.Equal(HttpStatusCode.Unauthorized, statusCodeException.StatusCode);
+ }
+
///
/// Tests that we get a warning message when a user provides a private key in the public key configuration.
///
@@ -458,35 +593,21 @@ public async Task WarnOnPrivateKey(string signingAlgo)
Guid subject = Guid.NewGuid();
string subjectStr = subject.ToString("D");
- JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer);
+ JwtPayload newPayload = GetJwtPayload(AuthConstants.ApiKeyJwtAudience, subjectStr, AuthConstants.ApiKeyJwtInternalIssuer, AuthConstants.ApiKeyJwtDefaultExpiration);
RootOptions opts = new();
- opts.UseApiKey(signingAlgo, subjectStr, newPayload, out string apiKey, out SecurityKey creds);
- JsonWebKey exportableJwk = null;
- if (signingAlgo.StartsWith("RS"))
- {
- exportableJwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(creds as RsaSecurityKey);
- }
- else if (signingAlgo.StartsWith("ES"))
- {
- exportableJwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(creds as ECDsaSecurityKey);
- }
- else
- {
- Assert.Fail("Unknown algorithm");
- }
+ ApiKeySignInfo signInfo = ApiKeySignInfo.Create(signingAlgo);
+ opts.UseApiKey(signInfo, subjectStr, newPayload, out string token);
JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonIgnoreCondition.WhenWritingNull);
serializerOptions.IgnoreReadOnlyProperties = true;
- string privateKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions);
- string privateKeyEncoded = Base64UrlEncoder.Encode(privateKeyJson);
AuthenticationOptions authOpts = new AuthenticationOptions()
{
MonitorApiKey = new MonitorApiKeyOptions()
{
Subject = opts.Authentication.MonitorApiKey.Subject,
- PublicKey = privateKeyEncoded,
+ PublicKey = signInfo.PrivateKeyEncoded,
},
};
toolRunner.ConfigurationFromEnvironment.Authentication = authOpts;
@@ -497,7 +618,7 @@ public async Task WarnOnPrivateKey(string signingAlgo)
// Create HttpClient with default request headers
using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory);
httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, apiKey);
+ new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, token);
ApiClient apiClient = new(_outputHelper, httpClient);
await apiClient.GetProcessesAsync();
@@ -677,7 +798,12 @@ private void ToolRunner_WarnPrivateKey(string fieldName)
_warnPrivateKeyLog.Add((fieldName, DateTime.Now));
}
- private static JwtPayload GetJwtPayload(string audience, string subject, string issuer)
+ private static JwtPayload GetJwtPayload(string audience, string subject, string issuer, TimeSpan expiration)
+ {
+ return GetJwtPayload(audience, subject, issuer, DateTime.UtcNow + expiration);
+ }
+
+ private static JwtPayload GetJwtPayload(string audience, string subject, string issuer, DateTime? expiration)
{
List claims = new();
@@ -696,6 +822,12 @@ private static JwtPayload GetJwtPayload(string audience, string subject, string
Claim audClaim = new Claim(AuthConstants.ClaimIssuerStr, issuer);
claims.Add(audClaim);
}
+ if (expiration.HasValue)
+ {
+ long expirationSecondsSinceEpoch = EpochTime.GetIntDate(expiration.Value);
+ Claim expClaim = new Claim(AuthConstants.ClaimExpirationStr, expirationSecondsSinceEpoch.ToString(CultureInfo.InvariantCulture));
+ claims.Add(expClaim);
+ }
JwtPayload newPayload = new JwtPayload(claims);
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs
index c6dec5b8fe9..29b05716f09 100644
--- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/GenerateKeyTests.cs
@@ -77,8 +77,10 @@ public async Task GenerateKey(OutputFormat? format)
ValidAlgorithms = JwtAlgorithmChecker.GetAllowedJwsAlgorithmList(),
// Issuer Settings
- ValidateIssuer = true, // This setting differs from actual token validation in the product because we want to make sure we set our Issuer
+ ValidateIssuer = true,
ValidIssuer = AuthConstants.ApiKeyJwtInternalIssuer,
+
+ // Issuer Signing Key Settings
ValidateIssuerSigningKey = true,
IssuerSigningKeys = new SecurityKey[] { validatingKey },
TryAllIssuerSigningKeys = true,
@@ -89,7 +91,7 @@ public async Task GenerateKey(OutputFormat? format)
// Other Settings
ValidateActor = false,
- ValidateLifetime = false,
+ ValidateLifetime = true,
};
// Required for CodeQL.
tokenValidationParams.EnableAadSigningKeyIssuerValidation();
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.ApiKey.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.ApiKey.cs
new file mode 100644
index 00000000000..c3f23fd9216
--- /dev/null
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.ApiKey.cs
@@ -0,0 +1,69 @@
+// 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.Tool.FunctionalTests;
+using Microsoft.Diagnostics.Monitoring.WebApi;
+using Microsoft.Diagnostics.Tools.Monitor;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+
+namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options
+{
+ internal static partial class OptionsExtensions
+ {
+ ///
+ /// Sets API Key authentication. Use this overload for most operations, unless specifically testing Authentication
+ ///
+ public static RootOptions UseApiKey(this RootOptions options, string algorithmName, Guid subject, out string token)
+ {
+ string subjectStr = subject.ToString("D");
+ Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, AuthConstants.ApiKeyJwtAudience);
+ long expirationSecondsSinceEpoch = EpochTime.GetIntDate(DateTime.UtcNow + AuthConstants.ApiKeyJwtDefaultExpiration);
+ Claim expClaim = new Claim(AuthConstants.ClaimExpirationStr, expirationSecondsSinceEpoch.ToString(CultureInfo.InvariantCulture));
+ Claim issClaim = new Claim(AuthConstants.ClaimIssuerStr, AuthConstants.ApiKeyJwtInternalIssuer);
+ Claim subClaim = new Claim(AuthConstants.ClaimSubjectStr, subjectStr);
+ JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, expClaim, issClaim, subClaim });
+
+ return options.UseApiKey(algorithmName, subjectStr, newPayload, out token);
+ }
+
+ public static RootOptions UseApiKey(this RootOptions options, string algorithmName, string subject, JwtPayload customPayload, out string token)
+ {
+ return options.UseApiKey(ApiKeySignInfo.Create(algorithmName), subject, customPayload, out token);
+ }
+
+ public static RootOptions UseApiKey(this RootOptions options, ApiKeySignInfo signInfo, string subject, JwtPayload customPayload, out string token)
+ {
+ options.UseApiKey(signInfo, subject);
+
+ token = ApiKeyToken.Create(signInfo, customPayload);
+
+ return options;
+ }
+
+ public static RootOptions UseApiKey(this RootOptions options, ApiKeySignInfo signInfo, string subject, string issuer = null)
+ {
+ if (null == options.Authentication)
+ {
+ options.Authentication = new AuthenticationOptions();
+ }
+
+ if (null == options.Authentication.MonitorApiKey)
+ {
+ options.Authentication.MonitorApiKey = new MonitorApiKeyOptions();
+ }
+
+ options.Authentication.MonitorApiKey.Subject = subject;
+ options.Authentication.MonitorApiKey.PublicKey = signInfo.PublicKeyEncoded;
+ if (!string.IsNullOrEmpty(issuer))
+ {
+ options.Authentication.MonitorApiKey.Issuer = issuer;
+ }
+
+ return options;
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs
index 0a3cbf7cf05..b0de78cfa4d 100644
--- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs
+++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs
@@ -6,19 +6,13 @@
using Microsoft.Diagnostics.Tools.Monitor;
using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options;
using Microsoft.Diagnostics.Tools.Monitor.Egress.FileSystem;
-using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
-using System.IdentityModel.Tokens.Jwt;
-using System.Security.Claims;
-using System.Security.Cryptography;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using Xunit;
namespace Microsoft.Diagnostics.Monitoring.TestCommon.Options
{
- internal static class OptionsExtensions
+ internal static partial class OptionsExtensions
{
public static RootOptions AddFileSystemEgress(this RootOptions options, string name, string outputPath)
{
@@ -182,21 +176,6 @@ public static RootOptions SetDumpTempFolder(this RootOptions options, string dir
return options;
}
- ///
- /// Sets API Key authentication. Use this overload for most operations, unless specifically testing Authentication or Authorization.
- ///
- public static RootOptions UseApiKey(this RootOptions options, string algorithmName, Guid subject, out string token)
- {
- string subjectStr = subject.ToString("D");
- Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, AuthConstants.ApiKeyJwtAudience);
- Claim issClaim = new Claim(AuthConstants.ClaimIssuerStr, AuthConstants.ApiKeyJwtInternalIssuer);
- Claim subClaim = new Claim(AuthConstants.ClaimSubjectStr, subjectStr);
- JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, issClaim, subClaim });
-
- return options.UseApiKey(algorithmName, subjectStr, newPayload, out token);
- }
-
-
///
/// Sets AzureAd authentication. Use this overload for most operations, unless specifically testing Authentication or Authorization.
///
@@ -237,139 +216,5 @@ public static RootOptions UseAzureAd(this RootOptions options, AzureAdOptions az
return options;
}
-
- public static RootOptions UseApiKey(this RootOptions options, string algorithmName, string subject, JwtPayload customPayload, out string token)
- {
- return options.UseApiKey(algorithmName, subject, customPayload, out token, out SecurityKey _);
- }
-
- public static RootOptions UseApiKey(this RootOptions options, string algorithmName, string subject, JwtPayload customPayload, out string token, out SecurityKey privateKey)
- {
- if (null == options.Authentication)
- {
- options.Authentication = new AuthenticationOptions();
- }
-
- if (null == options.Authentication.MonitorApiKey)
- {
- options.Authentication.MonitorApiKey = new MonitorApiKeyOptions();
- }
-
- SigningCredentials signingCreds;
- JsonWebKey exportableJwk;
- 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);
- privateKey = 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);
- privateKey = 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);
- privateKey = hmacSecKey;
- break;
-
- default:
- throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName));
- }
-
- JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType);
- JwtSecurityToken newToken = new JwtSecurityToken(newHeader, customPayload);
- JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
- string resultToken = tokenHandler.WriteToken(newToken);
-
- JsonSerializerOptions serializerOptions = JsonSerializerOptionsFactory.Create(JsonIgnoreCondition.WhenWritingNull);
- string publicKeyJson = JsonSerializer.Serialize(exportableJwk, serializerOptions);
-
- string publicKeyEncoded = Base64UrlEncoder.Encode(publicKeyJson);
-
- options.Authentication.MonitorApiKey.Subject = subject;
- options.Authentication.MonitorApiKey.PublicKey = publicKeyEncoded;
-
- token = resultToken;
-
- return options;
- }
-
- 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));
- }
- }
}
}
diff --git a/src/Tools/dotnet-monitor/Auth/ApiKey/GeneratedJwtKey.cs b/src/Tools/dotnet-monitor/Auth/ApiKey/GeneratedJwtKey.cs
index 764a61c9bf0..3b78f8d8e21 100644
--- a/src/Tools/dotnet-monitor/Auth/ApiKey/GeneratedJwtKey.cs
+++ b/src/Tools/dotnet-monitor/Auth/ApiKey/GeneratedJwtKey.cs
@@ -25,8 +25,13 @@ private GeneratedJwtKey(string token, string subject, string publicKey)
PublicKey = publicKey;
}
- public static GeneratedJwtKey Create()
+ public static GeneratedJwtKey Create(TimeSpan expirationOffset)
{
+ if (expirationOffset.TotalSeconds <= 0)
+ {
+ throw new ArgumentException(Strings.ErrorMessage_ExpirationMustBePositive, nameof(expirationOffset));
+ }
+
Guid subjectId = Guid.NewGuid();
string subjectStr = subjectId.ToString("D");
@@ -36,10 +41,13 @@ public static GeneratedJwtKey Create()
SigningCredentials signingCreds = new SigningCredentials(secKey, SecurityAlgorithms.EcdsaSha384);
JwtHeader newHeader = new JwtHeader(signingCreds, null, JwtConstants.HeaderType);
+ long expirationSecondsSinceEpoch = EpochTime.GetIntDate(DateTime.UtcNow + expirationOffset);
+
Claim audClaim = new Claim(AuthConstants.ClaimAudienceStr, AuthConstants.ApiKeyJwtAudience);
+ Claim expClaim = new Claim(AuthConstants.ClaimExpirationStr, expirationSecondsSinceEpoch.ToString());
Claim issClaim = new Claim(AuthConstants.ClaimIssuerStr, AuthConstants.ApiKeyJwtInternalIssuer);
Claim subClaim = new Claim(AuthConstants.ClaimSubjectStr, subjectStr);
- JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, issClaim, subClaim });
+ JwtPayload newPayload = new JwtPayload(new Claim[] { audClaim, expClaim, issClaim, subClaim });
JwtSecurityToken newToken = new JwtSecurityToken(newHeader, newPayload);
diff --git a/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerOptionsExtensions.cs b/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerOptionsExtensions.cs
index 2eeea6cd475..3b3f637f0cd 100644
--- a/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerOptionsExtensions.cs
+++ b/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerOptionsExtensions.cs
@@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.Tools.Monitor.Auth.ApiKey
{
internal static class JwtBearerOptionsExtensions
{
- public static void ConfigureApiKeyTokenValidation(this JwtBearerOptions options, SecurityKey publicKey)
+ public static void ConfigureApiKeyTokenValidation(this JwtBearerOptions options, SecurityKey publicKey, string issuer)
{
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
@@ -19,7 +19,10 @@ public static void ConfigureApiKeyTokenValidation(this JwtBearerOptions options,
ValidAlgorithms = JwtAlgorithmChecker.GetAllowedJwsAlgorithmList(),
// Issuer Settings
- ValidateIssuer = false,
+ ValidateIssuer = true,
+ ValidIssuer = issuer,
+
+ // Issuer Signing Key Settings
ValidateIssuerSigningKey = true,
IssuerSigningKeys = new SecurityKey[] { publicKey },
TryAllIssuerSigningKeys = true,
@@ -30,7 +33,7 @@ public static void ConfigureApiKeyTokenValidation(this JwtBearerOptions options,
// Other Settings
ValidateActor = false,
- ValidateLifetime = false,
+ ValidateLifetime = true,
};
// Required for CodeQL.
diff --git a/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerPostConfigure.cs b/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerPostConfigure.cs
index f1c507e20bd..d9eab1a6c30 100644
--- a/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerPostConfigure.cs
+++ b/src/Tools/dotnet-monitor/Auth/ApiKey/JwtBearerPostConfigure.cs
@@ -37,7 +37,7 @@ public void PostConfigure(string name, JwtBearerOptions options)
return;
}
- options.ConfigureApiKeyTokenValidation(configSnapshot.PublicKey);
+ options.ConfigureApiKeyTokenValidation(configSnapshot.PublicKey, configSnapshot.Issuer);
}
}
}
diff --git a/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyConfiguration.cs b/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyConfiguration.cs
index 69e984fe256..3a4dec22b22 100644
--- a/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyConfiguration.cs
+++ b/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyConfiguration.cs
@@ -19,5 +19,6 @@ internal class MonitorApiKeyConfiguration : AuthenticationSchemeOptions
public string Subject { get; set; }
public SecurityKey PublicKey { get; set; }
public IEnumerable ValidationErrors { get; set; }
+ public string Issuer { get; set; }
}
}
diff --git a/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyPostConfigure.cs b/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyPostConfigure.cs
index d2c19d5d9ae..82918259862 100644
--- a/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyPostConfigure.cs
+++ b/src/Tools/dotnet-monitor/Auth/ApiKey/MonitorApiKeyPostConfigure.cs
@@ -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 Microsoft.Diagnostics.Monitoring.WebApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -113,11 +114,15 @@ public void PostConfigure(string name, MonitorApiKeyConfiguration options)
{
options.Subject = string.Empty;
options.PublicKey = null;
+ options.Issuer = string.Empty;
}
else
{
options.Subject = sourceOptions.Subject;
options.PublicKey = jwk;
+ options.Issuer = string.IsNullOrEmpty(sourceOptions.Issuer) ?
+ AuthConstants.ApiKeyJwtInternalIssuer :
+ sourceOptions.Issuer;
}
}
}
diff --git a/src/Tools/dotnet-monitor/Commands/GenerateApiKeyCommandHandler.cs b/src/Tools/dotnet-monitor/Commands/GenerateApiKeyCommandHandler.cs
index 351959a69d0..63da008340a 100644
--- a/src/Tools/dotnet-monitor/Commands/GenerateApiKeyCommandHandler.cs
+++ b/src/Tools/dotnet-monitor/Commands/GenerateApiKeyCommandHandler.cs
@@ -22,9 +22,9 @@ namespace Microsoft.Diagnostics.Tools.Monitor.Commands
///
internal static class GenerateApiKeyCommandHandler
{
- public static void Invoke(OutputFormat output, TextWriter outputWriter)
+ public static void Invoke(OutputFormat output, TimeSpan expiration, TextWriter outputWriter)
{
- GeneratedJwtKey newJwt = GeneratedJwtKey.Create();
+ GeneratedJwtKey newJwt = GeneratedJwtKey.Create(expiration);
RootOptions opts = new()
{
diff --git a/src/Tools/dotnet-monitor/HostBuilder/HostBuilderHelper.cs b/src/Tools/dotnet-monitor/HostBuilder/HostBuilderHelper.cs
index d5741485dd0..42291c2c35b 100644
--- a/src/Tools/dotnet-monitor/HostBuilder/HostBuilderHelper.cs
+++ b/src/Tools/dotnet-monitor/HostBuilder/HostBuilderHelper.cs
@@ -98,7 +98,7 @@ public static IHostBuilder CreateHostBuilder(HostBuilderSettings settings)
if (settings.AuthenticationMode == StartupAuthenticationMode.TemporaryKey)
{
- GeneratedJwtKey jwtKey = GeneratedJwtKey.Create();
+ GeneratedJwtKey jwtKey = GeneratedJwtKey.Create(AuthConstants.ApiKeyJwtDefaultExpiration);
context.Properties.Add(typeof(GeneratedJwtKey), jwtKey);
// These are configured via the command line configuration source so that
diff --git a/src/Tools/dotnet-monitor/Program.cs b/src/Tools/dotnet-monitor/Program.cs
index 991b2380d29..02f32f18e6a 100644
--- a/src/Tools/dotnet-monitor/Program.cs
+++ b/src/Tools/dotnet-monitor/Program.cs
@@ -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 Microsoft.Diagnostics.Monitoring.WebApi;
using Microsoft.Diagnostics.Tools.Monitor.Commands;
using System;
using System.CommandLine;
@@ -17,13 +18,15 @@ private static CliCommand GenerateApiKeyCommand()
name: "generatekey",
description: Strings.HelpDescription_CommandGenerateKey)
{
- OutputOption
+ OutputOption,
+ ExpirationOption
};
command.SetAction((result) =>
{
GenerateApiKeyCommandHandler.Invoke(
result.GetValue(OutputOption),
+ result.GetValue(ExpirationOption),
result.Configuration.Output);
});
@@ -178,6 +181,14 @@ private static CliCommand ConfigCommand()
HelpName = "output"
};
+ private static CliOption ExpirationOption =
+ new CliOption("--expiration", "-e")
+ {
+ DefaultValueFactory = (_) => AuthConstants.ApiKeyJwtDefaultExpiration,
+ Description = Strings.HelpDescription_Expiration,
+ HelpName = "expiration"
+ };
+
private static CliOption ConfigLevelOption =
new CliOption("--level")
{
diff --git a/src/Tools/dotnet-monitor/Strings.Designer.cs b/src/Tools/dotnet-monitor/Strings.Designer.cs
index 441f56f5328..e6468279b8f 100644
--- a/src/Tools/dotnet-monitor/Strings.Designer.cs
+++ b/src/Tools/dotnet-monitor/Strings.Designer.cs
@@ -231,6 +231,15 @@ internal static string ErrorMessage_ExpectedToFindSharedLibrariesAtPath {
}
}
+ ///
+ /// Looks up a localized string similar to The expiration offset must be a positive time span value..
+ ///
+ internal static string ErrorMessage_ExpirationMustBePositive {
+ get {
+ return ResourceManager.GetString("ErrorMessage_ExpirationMustBePositive", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The extension file '{0}' for extension '{1}' could not be found..
///
@@ -628,6 +637,15 @@ internal static string HelpDescription_CommandShow {
}
}
+ ///
+ /// Looks up a localized string similar to The expiration time on or after the generated key will no longer be accepted. This is a time span offset (e.g. "7.00:00:00" for 7 days) that will be added to the current date time to create the expiration date time..
+ ///
+ internal static string HelpDescription_Expiration {
+ get {
+ return ResourceManager.GetString("HelpDescription_Expiration", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The fully qualified path and filename of the json configuration file you'd like to add to the list of configuration sources..
///
diff --git a/src/Tools/dotnet-monitor/Strings.resx b/src/Tools/dotnet-monitor/Strings.resx
index 7cd1f5b2bec..8616e168143 100644
--- a/src/Tools/dotnet-monitor/Strings.resx
+++ b/src/Tools/dotnet-monitor/Strings.resx
@@ -942,4 +942,11 @@
The process has Hot Reload enabled.
+
+ The expiration offset must be a positive time span value.
+
+
+ The expiration time on or after the generated key will no longer be accepted. This is a time span offset (e.g. "7.00:00:00" for 7 days) that will be added to the current date time to create the expiration date time.
+ Gets the string to display in help that explains what the '--expiration' option does.
+
\ No newline at end of file