Skip to content

Commit 0f62381

Browse files
authored
Merge pull request #13 from Project-MONAI/nds_addbasiceauth
adding basic auth
2 parents 7008cde + d1936af commit 0f62381

11 files changed

+244
-25
lines changed

src/Authentication/Configurations/AuthenticationOptions.cs

100644100755
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public class AuthenticationOptions
3030
[ConfigurationKeyName("openId")]
3131
public OpenIdOptions? OpenId { get; set; }
3232

33+
[ConfigurationKeyName("basicAuth")]
34+
public BasicAuthOptions? BasicAuth { get; set; }
35+
3336
public bool BypassAuth(ILogger logger)
3437
{
3538
Guard.Against.Null(logger);
@@ -40,6 +43,11 @@ public bool BypassAuth(ILogger logger)
4043
return true;
4144
}
4245

46+
if (BasicAuthEnabled(logger))
47+
{
48+
return false;
49+
}
50+
4351
if (OpenId is null)
4452
{
4553
throw new InvalidOperationException("openId configuration is invalid.");
@@ -67,6 +75,15 @@ public bool BypassAuth(ILogger logger)
6775
return false;
6876
}
6977

78+
public bool BasicAuthEnabled(ILogger logger)
79+
{
80+
if (BasicAuth is not null && BasicAuth.Id is not null && BasicAuth.Password is not null)
81+
{
82+
return true;
83+
}
84+
return false;
85+
}
86+
7087
private void ValidateClaims(List<ClaimMapping> claims, bool validateEndpoints)
7188
{
7289
foreach (var claim in claims)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.Extensions.Configuration;
18+
19+
namespace Monai.Deploy.Security.Authentication.Configurations
20+
{
21+
public class BasicAuthOptions
22+
{
23+
[ConfigurationKeyName("userName")]
24+
public string? Id { get; set; }
25+
26+
[ConfigurationKeyName("password")]
27+
public string? Password { get; set; }
28+
}
29+
}

src/Authentication/Extensions/AuthKeys.cs

100644100755
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ public static class AuthKeys
2525

2626
// Configuration Keys
2727
public const string OpenId = "OpenId";
28+
public const string BasicAuth = "BasicAuth";
2829
}
2930
}

src/Authentication/Extensions/IApplicationBuilderExtensions.cs

100644100755
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ public static class IApplicationBuilderExtensions
2727
public static IApplicationBuilder UseEndpointAuthorizationMiddleware(
2828
this IApplicationBuilder builder)
2929
{
30-
return builder.UseMiddleware<EndpointAuthorizationMiddleware>();
30+
builder.UseMiddleware<BasicAuthorizationMiddleware>();
31+
builder.UseMiddleware<EndpointAuthorizationMiddleware>();
32+
return builder;
3133
}
3234
}
3335
}

src/Authentication/Extensions/MonaiAuthenticationExtensions.cs

100644100755
Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,38 @@ public static IServiceCollection AddMonaiAuthentication(
5454
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
5555
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("roles");
5656
}
57-
58-
services
59-
.AddAuthentication(options =>
60-
{
61-
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
62-
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
63-
})
64-
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, AuthKeys.OpenId, options =>
65-
{
66-
options.Authority = configurations.Value.OpenId!.Realm;
67-
options.Audience = configurations.Value.OpenId!.Realm;
68-
options.RequireHttpsMetadata = false;
69-
70-
options.TokenValidationParameters = new TokenValidationParameters
57+
if (configurations.Value.BasicAuthEnabled(logger))
58+
{
59+
services.AddAuthentication(options => options.DefaultAuthenticateScheme = AuthKeys.BasicAuth)
60+
.AddScheme<AuthenticationSchemeOptions, BypassAuthenticationHandler>(AuthKeys.BasicAuth, null);
61+
}
62+
else
63+
{
64+
services
65+
.AddAuthentication(options =>
66+
{
67+
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
68+
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
69+
})
70+
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, AuthKeys.OpenId, options =>
7171
{
72-
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurations.Value.OpenId!.RealmKey!)),
73-
RoleClaimType = configurations.Value.OpenId.RoleClaimType,
74-
ValidIssuer = configurations.Value.OpenId.Realm,
75-
ValidAudiences = configurations.Value.OpenId.Audiences,
76-
ValidateIssuerSigningKey = true,
77-
ValidateIssuer = true,
78-
ValidateLifetime = true,
79-
ValidateAudience = true,
80-
};
81-
});
72+
options.Authority = configurations.Value.OpenId!.Realm;
73+
options.Audience = configurations.Value.OpenId!.Realm;
74+
options.RequireHttpsMetadata = false;
75+
76+
options.TokenValidationParameters = new TokenValidationParameters
77+
{
78+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurations.Value.OpenId!.RealmKey!)),
79+
RoleClaimType = configurations.Value.OpenId.RoleClaimType,
80+
ValidIssuer = configurations.Value.OpenId.Realm,
81+
ValidAudiences = configurations.Value.OpenId.Audiences,
82+
ValidateIssuerSigningKey = true,
83+
ValidateIssuer = true,
84+
ValidateLifetime = true,
85+
ValidateAudience = true,
86+
};
87+
});
88+
}
8289

8390
services.AddAuthorization();
8491
return services;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Net;
18+
using System.Net.Http.Headers;
19+
using System.Security.Claims;
20+
using System.Text;
21+
using Microsoft.AspNetCore.Http;
22+
using Microsoft.Extensions.Logging;
23+
using Microsoft.Extensions.Options;
24+
using Monai.Deploy.Security.Authentication.Configurations;
25+
using Monai.Deploy.Security.Authentication.Extensions;
26+
27+
namespace Monai.Deploy.Security.Authentication.Middleware
28+
{
29+
/// <summary>
30+
/// EndpointAuthorizationMiddleware for checking endpoint configuration.
31+
/// </summary>
32+
public class BasicAuthorizationMiddleware
33+
{
34+
private readonly RequestDelegate _next;
35+
private readonly IOptions<AuthenticationOptions> _options;
36+
private readonly ILogger<BasicAuthorizationMiddleware> _logger;
37+
38+
public BasicAuthorizationMiddleware(
39+
RequestDelegate next,
40+
IOptions<AuthenticationOptions> options,
41+
ILogger<BasicAuthorizationMiddleware> logger)
42+
{
43+
_next = next;
44+
_options = options;
45+
_logger = logger;
46+
}
47+
48+
49+
public async Task InvokeAsync(HttpContext httpContext)
50+
{
51+
52+
if (_options.Value.BasicAuthEnabled(_logger) is false)
53+
{
54+
await _next(httpContext).ConfigureAwait(false);
55+
return;
56+
}
57+
try
58+
{
59+
var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]);
60+
if (authHeader.Scheme == "Basic")
61+
{
62+
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
63+
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);
64+
var username = credentials[0];
65+
var password = credentials[1];
66+
if (string.Compare(username, _options.Value.BasicAuth.Id, false) is 0 &&
67+
string.Compare(password, _options.Value.BasicAuth.Password, false) is 0)
68+
{
69+
var claims = new[] { new Claim("name", credentials[0]) };
70+
var identity = new ClaimsIdentity(claims, "Basic");
71+
var claimsPrincipal = new ClaimsPrincipal(identity);
72+
httpContext.User = claimsPrincipal;
73+
return;
74+
}
75+
}
76+
}
77+
catch (Exception ex)
78+
{
79+
_logger.LogError(ex, "Exception ");
80+
}
81+
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
82+
83+
}
84+
}
85+
}

src/Authentication/Middleware/EndpointAuthorizationMiddleware.cs

100644100755
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public async Task InvokeAsync(HttpContext httpContext)
4848
await _next(httpContext).ConfigureAwait(false);
4949
return;
5050
}
51+
if (_options.Value.BasicAuthEnabled(_logger))
52+
{
53+
await _next(httpContext).ConfigureAwait(false);
54+
return;
55+
}
5156

5257
if (httpContext.User is not null
5358
&& httpContext.User.Identity is not null

src/Authentication/Tests/EndpointAuthorizationMiddlewareTest.cs

100644100755
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
using System.Net;
18+
using System.Text;
1819
using Microsoft.AspNetCore.Authentication.JwtBearer;
1920
using Microsoft.AspNetCore.TestHost;
2021
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
@@ -135,6 +136,50 @@ public async Task GivenConfigurationFileWithOpenIdConfigured_WhenUserProvidesAnE
135136
Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode);
136137
}
137138

139+
140+
[Fact]
141+
public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsNotAuthenticated_ExpectToDenyRequest()
142+
{
143+
using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false);
144+
145+
var server = host.GetTestServer();
146+
server.BaseAddress = new Uri("https://example.com/");
147+
148+
var client = server.CreateClient();
149+
var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false);
150+
151+
Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode);
152+
}
153+
154+
[Fact]
155+
public async Task GivenConfigurationFileWithBasicConfigured_WhenUserIsAuthenticated_ExpectToAllowRequest()
156+
{
157+
using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false);
158+
159+
var server = host.GetTestServer();
160+
server.BaseAddress = new Uri("https://example.com/");
161+
162+
var client = server.CreateClient();
163+
client.DefaultRequestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("user:pass"))}");
164+
var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false);
165+
166+
Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
167+
}
168+
[Fact]
169+
public async Task GivenConfigurationFileWithBasicConfigured_WhenHeaderIsInvalid_ExpectToDenyRequest()
170+
{
171+
using var host = await new HostBuilder().ConfigureWebHost(SetupWebServer("test.basic.json")).StartAsync().ConfigureAwait(false);
172+
173+
var server = host.GetTestServer();
174+
server.BaseAddress = new Uri("https://example.com/");
175+
176+
var client = server.CreateClient();
177+
client.DefaultRequestHeaders.Add("Authorization", $"BasicBad {Convert.ToBase64String(Encoding.UTF8.GetBytes("user:pass"))}");
178+
var responseMessage = await client.GetAsync("api/Test").ConfigureAwait(false);
179+
180+
Assert.Equal(HttpStatusCode.Unauthorized, responseMessage.StatusCode);
181+
}
182+
138183
private static Action<IWebHostBuilder> SetupWebServer(string configFile) => webBuilder =>
139184
{
140185
webBuilder

src/Authentication/Tests/test.auth-noclientid.json

100644100755
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"MonaiDeployAuthentication": {
33
"bypassAuthentication": false,
4+
"basicAuth": {
5+
"userName": "nopassword"
6+
},
47
"openId": {
58
"realm": "TEST-REALM",
69
"realmKey": "l9ZRlbMQBt9k1klUUrlWFuke8WbqnEde",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"MonaiDeployAuthentication": {
3+
"BypassAuthentication": false,
4+
"basicAuth": {
5+
"userName": "user",
6+
"password": "pass"
7+
}
8+
}
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"MonaiDeployAuthentication": {
3+
"BypassAuthentication": false,
4+
"basicAuth": {
5+
"userName": "nopassword",
6+
"password": "sded"
7+
},
8+
"openId": {
9+
"realm": "TEST-REALM",
10+
"realmKey": "l9ZRlbMQBt9k1klUUrlWFuke8WbqnEde",
11+
"audiences": [ "monai-app" ],
12+
"roleClaimType": "roles",
13+
"clientId": "monai-app-test"
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)