Skip to content

Commit f6c2e28

Browse files
authored
Merge pull request #97 from traien/master
Add Basic Authentication Support to serilog-ui
2 parents 4ed105a + 6b5508b commit f6c2e28

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
8686
}
8787
```
8888

89+
## Basic Authentication
90+
91+
If you need to add basic authentication to your serilog-ui instance, you can use the `BasicAuthenticationFilter`. Here's how to configure it in your `Startup.Configure` method:
92+
93+
```csharp
94+
app.UseSerilogUi(options =>
95+
{
96+
options.Authorization.Filters = new IUiAuthorizationFilter[]
97+
{
98+
new BasicAuthenticationFilter { User = "User", Pass = "P@ss" }
99+
};
100+
options.Authorization.RunAuthorizationFilterOnAppRoutes = true;
101+
});
102+
```
103+
89104
### For further configuration: [:fast_forward:](https://github.com/serilog-contrib/serilog-ui/wiki/Install:-Configuration-Options)
90105

91106
## Running the Tests: [:test_tube:](https://github.com/serilog-contrib/serilog-ui/wiki/Development:-Testing)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Net.Http.Headers;
3+
using System.Security.Cryptography;
4+
using System.Text;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Serilog.Ui.Web.Authorization;
8+
9+
public class BasicAuthenticationFilter : IUiAuthorizationFilter
10+
{
11+
public string User { get; set; }
12+
public string Pass { get; set; }
13+
14+
private const string AuthenticationScheme = "Basic";
15+
internal const string AuthenticationCookieName = "SerilogAuth";
16+
17+
public bool Authorize(HttpContext httpContext)
18+
{
19+
var header = httpContext.Request.Headers["Authorization"];
20+
var isAuthenticated = false;
21+
22+
if (header == "null" || string.IsNullOrEmpty(header))
23+
{
24+
var authCookie = httpContext.Request.Cookies[AuthenticationCookieName];
25+
if (!string.IsNullOrWhiteSpace(authCookie))
26+
{
27+
var hashedCredentials = EncryptCredentials(User, Pass);
28+
isAuthenticated = authCookie.Equals(hashedCredentials, StringComparison.OrdinalIgnoreCase);
29+
}
30+
}
31+
else
32+
{
33+
var authValues = AuthenticationHeaderValue.Parse(header);
34+
35+
if (IsBasicAuthentication(authValues))
36+
{
37+
var tokens = ExtractAuthenticationTokens(authValues);
38+
39+
if (CredentialsMatch(tokens))
40+
{
41+
isAuthenticated = true;
42+
var hashedCredentials = EncryptCredentials(User, Pass);
43+
httpContext.Response.Cookies.Append(AuthenticationCookieName, hashedCredentials);
44+
}
45+
}
46+
}
47+
48+
if (!isAuthenticated)
49+
{
50+
SetChallengeResponse(httpContext);
51+
}
52+
53+
return isAuthenticated;
54+
}
55+
56+
private string EncryptCredentials(string user, string pass)
57+
{
58+
using var sha256 = SHA256.Create();
59+
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes($"{user}:{pass}"));
60+
var hashedCredentials = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
61+
return hashedCredentials;
62+
}
63+
64+
private static bool IsBasicAuthentication(AuthenticationHeaderValue authValues)
65+
{
66+
return AuthenticationScheme.Equals(authValues.Scheme, StringComparison.InvariantCultureIgnoreCase);
67+
}
68+
69+
private static (string, string) ExtractAuthenticationTokens(AuthenticationHeaderValue authValues)
70+
{
71+
var parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authValues.Parameter));
72+
var parts = parameter.Split(':');
73+
return (parts[0], parts[1]);
74+
}
75+
76+
private bool CredentialsMatch((string Username, string Password) tokens)
77+
{
78+
return tokens.Username == User && tokens.Password == Pass;
79+
}
80+
81+
private void SetChallengeResponse(HttpContext httpContext)
82+
{
83+
httpContext.Response.StatusCode = 401;
84+
httpContext.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Serilog UI\"");
85+
httpContext.Response.WriteAsync("Authentication is required.");
86+
}
87+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Linq;
2+
using FluentAssertions;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.Net.Http.Headers;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace Serilog.Ui.Web.Authorization.Tests;
9+
10+
public class BasicAuthenticationFilterTests
11+
{
12+
[Fact]
13+
public async Task Authorize_WithValidCredentials_ShouldReturnTrue()
14+
{
15+
// Arrange
16+
var filter = new BasicAuthenticationFilter
17+
{
18+
User = "User",
19+
Pass = "P@ss"
20+
};
21+
22+
var httpContext = new DefaultHttpContext();
23+
httpContext.Request.Headers["Authorization"] = "Basic VXNlcjpQQHNz"; // Base64 encoded "User:P@ss"
24+
25+
// Act
26+
var result = filter.Authorize(httpContext);
27+
var authCookie = httpContext.Response.GetTypedHeaders().SetCookie.FirstOrDefault(sc => sc.Name == BasicAuthenticationFilter.AuthenticationCookieName);
28+
29+
// Assert
30+
result.Should().BeTrue();
31+
authCookie.Should().NotBeNull();
32+
}
33+
34+
[Fact]
35+
public async Task Authorize_WithInvalidCredentials_ShouldReturnFalse()
36+
{
37+
// Arrange
38+
var filter = new BasicAuthenticationFilter
39+
{
40+
User = "User",
41+
Pass = "P@ss"
42+
};
43+
44+
var httpContext = new DefaultHttpContext();
45+
httpContext.Request.Headers["Authorization"] = "Basic QWRtaW46dXNlcg=="; // Base64 encoded "Admin:user"
46+
47+
// Act
48+
var result = filter.Authorize(httpContext);
49+
50+
// Assert
51+
result.Should().BeFalse();
52+
}
53+
54+
[Fact]
55+
public async Task Authorize_WithMissingAuthorizationHeader_ShouldSetChallengeResponse()
56+
{
57+
// Arrange
58+
var filter = new BasicAuthenticationFilter
59+
{
60+
User = "User",
61+
Pass = "P@ss"
62+
};
63+
64+
var httpContext = new DefaultHttpContext();
65+
66+
// Act
67+
var result = filter.Authorize(httpContext);
68+
69+
// Assert
70+
result.Should().BeFalse();
71+
httpContext.Response.StatusCode.Should().Be(401);
72+
httpContext.Response.Headers[HeaderNames.WWWAuthenticate].Should().Contain("Basic realm=\"Serilog UI\"");
73+
}
74+
}

0 commit comments

Comments
 (0)