Skip to content

Commit

Permalink
Merge pull request #13 from Project-MONAI/nds_addbasiceauth
Browse files Browse the repository at this point in the history
adding basic auth
  • Loading branch information
neildsouth authored Dec 13, 2022
2 parents 7008cde + d1936af commit 0f62381
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 25 deletions.
17 changes: 17 additions & 0 deletions src/Authentication/Configurations/AuthenticationOptions.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.");
Expand Down Expand Up @@ -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<ClaimMapping> claims, bool validateEndpoints)
{
foreach (var claim in claims)
Expand Down
29 changes: 29 additions & 0 deletions src/Authentication/Configurations/BasicAuthOptions.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
1 change: 1 addition & 0 deletions src/Authentication/Extensions/AuthKeys.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public static class AuthKeys

// Configuration Keys
public const string OpenId = "OpenId";
public const string BasicAuth = "BasicAuth";
}
}
4 changes: 3 additions & 1 deletion src/Authentication/Extensions/IApplicationBuilderExtensions.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public static class IApplicationBuilderExtensions
public static IApplicationBuilder UseEndpointAuthorizationMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<EndpointAuthorizationMiddleware>();
builder.UseMiddleware<BasicAuthorizationMiddleware>();
builder.UseMiddleware<EndpointAuthorizationMiddleware>();
return builder;
}
}
}
55 changes: 31 additions & 24 deletions src/Authentication/Extensions/MonaiAuthenticationExtensions.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthenticationSchemeOptions, BypassAuthenticationHandler>(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;
Expand Down
85 changes: 85 additions & 0 deletions src/Authentication/Middleware/BasicAuthorizationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// EndpointAuthorizationMiddleware for checking endpoint configuration.
/// </summary>
public class BasicAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IOptions<AuthenticationOptions> _options;
private readonly ILogger<BasicAuthorizationMiddleware> _logger;

public BasicAuthorizationMiddleware(
RequestDelegate next,
IOptions<AuthenticationOptions> options,
ILogger<BasicAuthorizationMiddleware> 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;

}
}
}
5 changes: 5 additions & 0 deletions src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

using System.Net;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.TestHost;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
Expand Down Expand Up @@ -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<IWebHostBuilder> SetupWebServer(string configFile) => webBuilder =>
{
webBuilder
Expand Down
3 changes: 3 additions & 0 deletions src/Authentication/Tests/test.auth-noclientid.json
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"MonaiDeployAuthentication": {
"bypassAuthentication": false,
"basicAuth": {
"userName": "nopassword"
},
"openId": {
"realm": "TEST-REALM",
"realmKey": "l9ZRlbMQBt9k1klUUrlWFuke8WbqnEde",
Expand Down
9 changes: 9 additions & 0 deletions src/Authentication/Tests/test.basic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"MonaiDeployAuthentication": {
"BypassAuthentication": false,
"basicAuth": {
"userName": "user",
"password": "pass"
}
}
}
16 changes: 16 additions & 0 deletions src/Authentication/Tests/test.bypassedbybasic.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

0 comments on commit 0f62381

Please sign in to comment.