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