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..31fd535 --- /dev/null +++ b/src/Authentication/Configurations/BasicAuthOptions.cs @@ -0,0 +1,29 @@ +/* + * 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; + +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..19965a6 --- /dev/null +++ b/src/Authentication/Middleware/BasicAuthorizationMiddleware.cs @@ -0,0 +1,85 @@ +/* + * 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; +using Monai.Deploy.Security.Authentication.Extensions; + +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"]); + if (authHeader.Scheme == "Basic") + { + 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..4ad2c08 --- 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,50 @@ 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_ExpectToAllowRequest() + { + 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); + } + [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 => { 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