From 0e279a05d1995ef1d8feeecf64d8a96f0cf25c22 Mon Sep 17 00:00:00 2001 From: Neil South Date: Mon, 12 Dec 2022 18:13:35 +0000 Subject: [PATCH 1/2] adding basic auth Signed-off-by: Neil South --- .../Configurations/AuthenticationOptions.cs | 17 ++++ .../Configurations/BasicAuthOptions.cs | 15 ++++ src/Authentication/Extensions/AuthKeys.cs | 1 + .../IApplicationBuilderExtensions.cs | 4 +- .../MonaiAuthenticationExtensions.cs | 55 +++++++------ .../BasicAuthorizationMiddleware.cs | 81 +++++++++++++++++++ .../EndpointAuthorizationMiddleware.cs | 5 ++ .../EndpointAuthorizationMiddlewareTest.cs | 31 +++++++ .../Tests/test.auth-noclientid.json | 3 + src/Authentication/Tests/test.basic.json | 9 +++ .../Tests/test.bypassedbybasic.json | 16 ++++ 11 files changed, 212 insertions(+), 25 deletions(-) mode change 100644 => 100755 src/Authentication/Configurations/AuthenticationOptions.cs create mode 100755 src/Authentication/Configurations/BasicAuthOptions.cs mode change 100644 => 100755 src/Authentication/Extensions/AuthKeys.cs mode change 100644 => 100755 src/Authentication/Extensions/IApplicationBuilderExtensions.cs mode change 100644 => 100755 src/Authentication/Extensions/MonaiAuthenticationExtensions.cs create mode 100755 src/Authentication/Middleware/BasicAuthorizationMiddleware.cs mode change 100644 => 100755 src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs mode change 100644 => 100755 src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs mode change 100644 => 100755 src/Authentication/Tests/test.auth-noclientid.json create mode 100755 src/Authentication/Tests/test.basic.json create mode 100755 src/Authentication/Tests/test.bypassedbybasic.json diff --git a/src/Authentication/Configurations/AuthenticationOptions.cs b/src/Authentication/Configurations/AuthenticationOptions.cs old mode 100644 new mode 100755 index 32fba93..c34df0e --- a/src/Authentication/Configurations/AuthenticationOptions.cs +++ b/src/Authentication/Configurations/AuthenticationOptions.cs @@ -30,6 +30,9 @@ public class AuthenticationOptions [ConfigurationKeyName("openId")] public OpenIdOptions? OpenId { get; set; } + [ConfigurationKeyName("basicAuth")] + public BasicAuthOptions? BasicAuth { get; set; } + public bool BypassAuth(ILogger logger) { Guard.Against.Null(logger); @@ -40,6 +43,11 @@ public bool BypassAuth(ILogger logger) return true; } + if (BasicAuthEnabled(logger)) + { + return false; + } + if (OpenId is null) { throw new InvalidOperationException("openId configuration is invalid."); @@ -67,6 +75,15 @@ public bool BypassAuth(ILogger logger) return false; } + public bool BasicAuthEnabled(ILogger logger) + { + if (BasicAuth is not null && BasicAuth.Id is not null && BasicAuth.Password is not null) + { + return true; + } + return false; + } + private void ValidateClaims(List claims, bool validateEndpoints) { foreach (var claim in claims) diff --git a/src/Authentication/Configurations/BasicAuthOptions.cs b/src/Authentication/Configurations/BasicAuthOptions.cs new file mode 100755 index 0000000..81846a1 --- /dev/null +++ b/src/Authentication/Configurations/BasicAuthOptions.cs @@ -0,0 +1,15 @@ + + +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.Security.Authentication.Configurations +{ + public class BasicAuthOptions + { + [ConfigurationKeyName("userName")] + public string? Id { get; set; } + + [ConfigurationKeyName("password")] + public string? Password { get; set; } + } +} diff --git a/src/Authentication/Extensions/AuthKeys.cs b/src/Authentication/Extensions/AuthKeys.cs old mode 100644 new mode 100755 index 65af5df..20d029a --- a/src/Authentication/Extensions/AuthKeys.cs +++ b/src/Authentication/Extensions/AuthKeys.cs @@ -25,5 +25,6 @@ public static class AuthKeys // Configuration Keys public const string OpenId = "OpenId"; + public const string BasicAuth = "BasicAuth"; } } diff --git a/src/Authentication/Extensions/IApplicationBuilderExtensions.cs b/src/Authentication/Extensions/IApplicationBuilderExtensions.cs old mode 100644 new mode 100755 index 966ce69..42a2aeb --- a/src/Authentication/Extensions/IApplicationBuilderExtensions.cs +++ b/src/Authentication/Extensions/IApplicationBuilderExtensions.cs @@ -27,7 +27,9 @@ public static class IApplicationBuilderExtensions public static IApplicationBuilder UseEndpointAuthorizationMiddleware( this IApplicationBuilder builder) { - return builder.UseMiddleware(); + builder.UseMiddleware(); + builder.UseMiddleware(); + return builder; } } } diff --git a/src/Authentication/Extensions/MonaiAuthenticationExtensions.cs b/src/Authentication/Extensions/MonaiAuthenticationExtensions.cs old mode 100644 new mode 100755 index 7a9d25e..f7a884e --- a/src/Authentication/Extensions/MonaiAuthenticationExtensions.cs +++ b/src/Authentication/Extensions/MonaiAuthenticationExtensions.cs @@ -54,31 +54,38 @@ public static IServiceCollection AddMonaiAuthentication( JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role"); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("roles"); } - - services - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, AuthKeys.OpenId, options => - { - options.Authority = configurations.Value.OpenId!.Realm; - options.Audience = configurations.Value.OpenId!.Realm; - options.RequireHttpsMetadata = false; - - options.TokenValidationParameters = new TokenValidationParameters + if (configurations.Value.BasicAuthEnabled(logger)) + { + services.AddAuthentication(options => options.DefaultAuthenticateScheme = AuthKeys.BasicAuth) + .AddScheme(AuthKeys.BasicAuth, null); + } + else + { + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, AuthKeys.OpenId, options => { - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurations.Value.OpenId!.RealmKey!)), - RoleClaimType = configurations.Value.OpenId.RoleClaimType, - ValidIssuer = configurations.Value.OpenId.Realm, - ValidAudiences = configurations.Value.OpenId.Audiences, - ValidateIssuerSigningKey = true, - ValidateIssuer = true, - ValidateLifetime = true, - ValidateAudience = true, - }; - }); + options.Authority = configurations.Value.OpenId!.Realm; + options.Audience = configurations.Value.OpenId!.Realm; + options.RequireHttpsMetadata = false; + + options.TokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurations.Value.OpenId!.RealmKey!)), + RoleClaimType = configurations.Value.OpenId.RoleClaimType, + ValidIssuer = configurations.Value.OpenId.Realm, + ValidAudiences = configurations.Value.OpenId.Audiences, + ValidateIssuerSigningKey = true, + ValidateIssuer = true, + ValidateLifetime = true, + ValidateAudience = true, + }; + }); + } services.AddAuthorization(); return services; diff --git a/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs b/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs new file mode 100755 index 0000000..bf9b100 --- /dev/null +++ b/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs @@ -0,0 +1,81 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Net; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.Security.Authentication.Configurations; + +namespace Monai.Deploy.Security.Authentication.Middleware +{ + /// + /// EndpointAuthorizationMiddleware for checking endpoint configuration. + /// + public class BasicAuthorizationMiddleware + { + private readonly RequestDelegate _next; + private readonly IOptions _options; + private readonly ILogger _logger; + + public BasicAuthorizationMiddleware( + RequestDelegate next, + IOptions options, + ILogger logger) + { + _next = next; + _options = options; + _logger = logger; + } + + + public async Task InvokeAsync(HttpContext httpContext) + { + + if (_options.Value.BasicAuthEnabled(_logger) is false) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + try + { + var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); + var credentialBytes = Convert.FromBase64String(authHeader.Parameter); + var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); + var username = credentials[0]; + var password = credentials[1]; + if (string.Compare(username, _options.Value.BasicAuth.Id, false) is 0 && + string.Compare(password, _options.Value.BasicAuth.Password, false) is 0) + { + var claims = new[] { new Claim("name", credentials[0]) }; + var identity = new ClaimsIdentity(claims, "Basic"); + var claimsPrincipal = new ClaimsPrincipal(identity); + httpContext.User = claimsPrincipal; + return; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception "); + } + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + + } + } +} diff --git a/src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs b/src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs old mode 100644 new mode 100755 index 9b2fc1e..a04e74f --- a/src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs +++ b/src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs @@ -48,6 +48,11 @@ public async Task InvokeAsync(HttpContext httpContext) await _next(httpContext).ConfigureAwait(false); return; } + if (_options.Value.BasicAuthEnabled(_logger)) + { + await _next(httpContext).ConfigureAwait(false); + return; + } if (httpContext.User is not null && httpContext.User.Identity is not null diff --git a/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs b/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs old mode 100644 new mode 100755 index 9668f20..272c3ca --- a/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs +++ b/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs @@ -15,6 +15,7 @@ */ using System.Net; +using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.TestHost; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -135,6 +136,36 @@ public async Task GivenConfigurationFileWithOpenIdConfigured_WhenUserProvidesAnE Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); } + + [Fact] + public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsNotAuthenticated_ExpectToDenyRequest() + { + using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://example.com/"); + + var client = server.CreateClient(); + var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); + } + + [Fact] + public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsAuthenticated_ExpectToDenyRequest() + { + using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://example.com/"); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("user:pass"))}"); + var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode); + } + private static Action SetupWebServer(string configFile) => webBuilder => { webBuilder diff --git a/src/Authentication/Tests/test.auth-noclientid.json b/src/Authentication/Tests/test.auth-noclientid.json old mode 100644 new mode 100755 index 72903b0..621229e --- a/src/Authentication/Tests/test.auth-noclientid.json +++ b/src/Authentication/Tests/test.auth-noclientid.json @@ -1,6 +1,9 @@ { "MonaiDeployAuthentication": { "bypassAuthentication": false, + "basicAuth": { + "userName": "nopassword" + }, "openId": { "realm": "TEST-REALM", "realmKey": "l9ZRlbMQBt9k1klUUrlWFuke8WbqnEde", diff --git a/src/Authentication/Tests/test.basic.json b/src/Authentication/Tests/test.basic.json new file mode 100755 index 0000000..ad24dd6 --- /dev/null +++ b/src/Authentication/Tests/test.basic.json @@ -0,0 +1,9 @@ +{ + "MonaiDeployAuthentication": { + "BypassAuthentication": false, + "basicAuth": { + "userName": "user", + "password": "pass" + } + } +} \ No newline at end of file diff --git a/src/Authentication/Tests/test.bypassedbybasic.json b/src/Authentication/Tests/test.bypassedbybasic.json new file mode 100755 index 0000000..4f4511a --- /dev/null +++ b/src/Authentication/Tests/test.bypassedbybasic.json @@ -0,0 +1,16 @@ +{ + "MonaiDeployAuthentication": { + "BypassAuthentication": false, + "basicAuth": { + "userName": "nopassword", + "password": "sded" + }, + "openId": { + "realm": "TEST-REALM", + "realmKey": "l9ZRlbMQBt9k1klUUrlWFuke8WbqnEde", + "audiences": [ "monai-app" ], + "roleClaimType": "roles", + "clientId": "monai-app-test" + } + } +} \ No newline at end of file From d1936afdeac84ff2e9cd82abd05b237e11277db7 Mon Sep 17 00:00:00 2001 From: Neil South Date: Tue, 13 Dec 2022 12:41:06 +0000 Subject: [PATCH 2/2] added missing header and test Signed-off-by: Neil South --- .../Configurations/BasicAuthOptions.cs | 16 +++++++++++- .../BasicAuthorizationMiddleware.cs | 26 +++++++++++-------- .../EndpointAuthorizationMiddlewareTest.cs | 16 +++++++++++- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Authentication/Configurations/BasicAuthOptions.cs b/src/Authentication/Configurations/BasicAuthOptions.cs index 81846a1..31fd535 100755 --- a/src/Authentication/Configurations/BasicAuthOptions.cs +++ b/src/Authentication/Configurations/BasicAuthOptions.cs @@ -1,4 +1,18 @@ - +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ using Microsoft.Extensions.Configuration; diff --git a/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs b/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs index bf9b100..19965a6 100755 --- a/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs +++ b/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.Security.Authentication.Configurations; +using Monai.Deploy.Security.Authentication.Extensions; namespace Monai.Deploy.Security.Authentication.Middleware { @@ -56,18 +57,21 @@ public async Task InvokeAsync(HttpContext httpContext) try { var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); - var credentialBytes = Convert.FromBase64String(authHeader.Parameter); - var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); - var username = credentials[0]; - var password = credentials[1]; - if (string.Compare(username, _options.Value.BasicAuth.Id, false) is 0 && - string.Compare(password, _options.Value.BasicAuth.Password, false) is 0) + if (authHeader.Scheme == "Basic") { - var claims = new[] { new Claim("name", credentials[0]) }; - var identity = new ClaimsIdentity(claims, "Basic"); - var claimsPrincipal = new ClaimsPrincipal(identity); - httpContext.User = claimsPrincipal; - return; + var credentialBytes = Convert.FromBase64String(authHeader.Parameter); + var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); + var username = credentials[0]; + var password = credentials[1]; + if (string.Compare(username, _options.Value.BasicAuth.Id, false) is 0 && + string.Compare(password, _options.Value.BasicAuth.Password, false) is 0) + { + var claims = new[] { new Claim("name", credentials[0]) }; + var identity = new ClaimsIdentity(claims, "Basic"); + var claimsPrincipal = new ClaimsPrincipal(identity); + httpContext.User = claimsPrincipal; + return; + } } } catch (Exception ex) diff --git a/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs b/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs index 272c3ca..4ad2c08 100755 --- a/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs +++ b/src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs @@ -152,7 +152,7 @@ public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsNotAuthent } [Fact] - public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsAuthenticated_ExpectToDenyRequest() + public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsAuthenticated_ExpectToAllowRequest() { using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false); @@ -165,6 +165,20 @@ public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsAuthentica Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode); } + [Fact] + public async Task GivenConfigurationFileWithBasicConfigured_WhenHeaderIsInvalid_ExpectToDenyRequest() + { + using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false); + + var server = host.GetTestServer(); + server.BaseAddress = new Uri("https://example.com/"); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("Authorization", $"BasicBad {Convert.ToBase64String(Encoding.UTF8.GetBytes("user:pass"))}"); + var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode); + } private static Action SetupWebServer(string configFile) => webBuilder => {