Skip to content

Commit 4795ebf

Browse files
authored
Add Bitwarden.Server.Sdk.Authentication Package (#154)
1 parent 01a92cb commit 4795ebf

File tree

9 files changed

+653
-0
lines changed

9 files changed

+653
-0
lines changed

.github/workflows/start-release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
options:
1212
- Bitwarden.Server.Sdk
1313
- Bitwarden.Server.Sdk.Features
14+
- Bitwarden.Server.Sdk.Authentication
1415

1516
permissions:
1617
pull-requests: write

bitwarden-dotnet.sln

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4949B721
4545
EndProject
4646
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Features.Tests", "extensions\Bitwarden.Server.Sdk.Features\tests\Bitwarden.Server.Sdk.Features.Tests\Bitwarden.Server.Sdk.Features.Tests.csproj", "{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}"
4747
EndProject
48+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.Server.Sdk.Authentication", "Bitwarden.Server.Sdk.Authentication", "{79CC5F16-EEB8-4539-AA15-FDC1E9AB03D1}"
49+
EndProject
50+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Authentication", "extensions\Bitwarden.Server.Sdk.Authentication\src\Bitwarden.Server.Sdk.Authentication.csproj", "{0E96965A-4397-46A8-8D84-330ACFF2F8CF}"
51+
EndProject
52+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D623B9E7-6555-45AB-AAEC-EA388FACCE11}"
53+
EndProject
54+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Authentication.Tests", "extensions\Bitwarden.Server.Sdk.Authentication\tests\Bitwarden.Server.Sdk.Authentication.Tests\Bitwarden.Server.Sdk.Authentication.Tests.csproj", "{5AA835F4-F265-403F-897C-8989E354C052}"
55+
EndProject
4856
Global
4957
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5058
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +110,14 @@ Global
102110
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
103111
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
104112
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.Build.0 = Release|Any CPU
113+
{0E96965A-4397-46A8-8D84-330ACFF2F8CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
114+
{0E96965A-4397-46A8-8D84-330ACFF2F8CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
115+
{0E96965A-4397-46A8-8D84-330ACFF2F8CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
116+
{0E96965A-4397-46A8-8D84-330ACFF2F8CF}.Release|Any CPU.Build.0 = Release|Any CPU
117+
{5AA835F4-F265-403F-897C-8989E354C052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
118+
{5AA835F4-F265-403F-897C-8989E354C052}.Debug|Any CPU.Build.0 = Debug|Any CPU
119+
{5AA835F4-F265-403F-897C-8989E354C052}.Release|Any CPU.ActiveCfg = Release|Any CPU
120+
{5AA835F4-F265-403F-897C-8989E354C052}.Release|Any CPU.Build.0 = Release|Any CPU
105121
EndGlobalSection
106122
GlobalSection(NestedProjects) = preSolution
107123
{5EC8B943-2E9E-437D-9FFC-D18B5DB4D7D0} = {695C76EF-1102-4805-970F-7C995EE54930}
@@ -124,5 +140,9 @@ Global
124140
{DF914CD1-F916-4A58-B749-625DB67FAAA7} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC}
125141
{4949B721-5C7F-4D85-AB35-F57B54D7A6E6} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC}
126142
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E} = {4949B721-5C7F-4D85-AB35-F57B54D7A6E6}
143+
{79CC5F16-EEB8-4539-AA15-FDC1E9AB03D1} = {695C76EF-1102-4805-970F-7C995EE54930}
144+
{0E96965A-4397-46A8-8D84-330ACFF2F8CF} = {79CC5F16-EEB8-4539-AA15-FDC1E9AB03D1}
145+
{D623B9E7-6555-45AB-AAEC-EA388FACCE11} = {79CC5F16-EEB8-4539-AA15-FDC1E9AB03D1}
146+
{5AA835F4-F265-403F-897C-8989E354C052} = {D623B9E7-6555-45AB-AAEC-EA388FACCE11}
127147
EndGlobalSection
128148
EndGlobal
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Microsoft.AspNetCore.Builder;
2+
3+
namespace Bitwarden.Server.Sdk.Authentication;
4+
5+
/// <summary>
6+
/// Extension methods to add Bitwarden-style authentication to the HTTP application pipeline.
7+
/// </summary>
8+
public static class AuthenticationApplicationBuilderExtensions
9+
{
10+
/// <summary>
11+
/// Uses Bitwarden-style Authentication middleware.
12+
/// This will always call <see cref="AuthAppBuilderExtensions.UseAuthentication"/> and all rules for that function
13+
/// apply to this one. It should be called after <c>UseRouting</c> and before <c>UseAuthorization</c>.
14+
/// </summary>
15+
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
16+
/// <returns>A reference to this instance after the operation has completed.</returns>
17+
public static IApplicationBuilder UseBitwardenAuthentication(this IApplicationBuilder app)
18+
{
19+
ArgumentNullException.ThrowIfNull(app);
20+
21+
app.UseAuthentication();
22+
23+
app.UseMiddleware<PostAuthenticationLoggingMiddleware>();
24+
25+
return app;
26+
}
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace Bitwarden.Server.Sdk.Authentication;
7+
8+
/// <summary>
9+
/// Extension methods for setting up Bitwarden-style authentication in a <see cref="IServiceCollection"/>.
10+
/// </summary>
11+
public static class AuthenticationServiceCollectionExtensions
12+
{
13+
/// <summary>
14+
/// Adds Bitwarden compatible authentication, can be configured through the `Authentication:Schemes:Bearer` config
15+
/// section.
16+
/// </summary>
17+
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
18+
/// <returns>The <see cref="IServiceCollection"/> for additional chaining.</returns>
19+
public static IServiceCollection AddBitwardenAuthentication(this IServiceCollection services)
20+
{
21+
ArgumentNullException.ThrowIfNull(services);
22+
23+
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
24+
.AddJwtBearer();
25+
26+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JwtBearerOptions>, BitwardenConfigureJwtBearerOptions>());
27+
28+
return services;
29+
}
30+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<PropertyGroup>
10+
<VersionPrefix>0.1.0</VersionPrefix>
11+
<PreReleaseVersionLabel>beta</PreReleaseVersionLabel>
12+
<PreReleaseVersionIteration>1</PreReleaseVersionIteration>
13+
<VersionSuffix Condition="'$(VersionSuffix)' == '' AND '$(IsPreRelease)' == 'true'">$(PreReleaseVersionLabel).$(PreReleaseVersionIteration)</VersionSuffix>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.IdentityModel.Tokens.Jwt;
2+
using Microsoft.AspNetCore.Authentication.JwtBearer;
3+
using Microsoft.Extensions.Hosting;
4+
using Microsoft.Extensions.Options;
5+
using Microsoft.IdentityModel.Logging;
6+
7+
namespace Bitwarden.Server.Sdk.Authentication;
8+
9+
internal class BitwardenConfigureJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
10+
{
11+
private readonly IHostEnvironment _hostEnvironment;
12+
13+
public BitwardenConfigureJwtBearerOptions(IHostEnvironment hostEnvironment)
14+
{
15+
_hostEnvironment = hostEnvironment;
16+
17+
if (_hostEnvironment.IsDevelopment())
18+
{
19+
IdentityModelEventSource.ShowPII = true;
20+
}
21+
}
22+
23+
public void Configure(string? schemeName, JwtBearerOptions options)
24+
{
25+
if (schemeName != JwtBearerDefaults.AuthenticationScheme)
26+
{
27+
return;
28+
}
29+
30+
// The MapInBoundClaims maps the Jwt claim names into the more microsoft standard of the same claims
31+
// For example, the claim `"amr"` might exist in the JWT but with MapInboundClaims = true then once
32+
// you get your hand on the ClaimsPrincipal you'd have to find the claim value using
33+
// `ClaimTypes.AuthenticationMethod` or `"http://schemas.microsoft.com/claims/authnmethodsreferences"`
34+
// Keeping the exact same name on the issuing side and the consumption side is less error prone in our
35+
// opinion.
36+
options.MapInboundClaims = false;
37+
38+
// Our identity service does not issue an audience claim for us to validate
39+
options.TokenValidationParameters.ValidateAudience = false;
40+
41+
// This is the default AccessTokenJwtType that IdentityServer uses
42+
options.TokenValidationParameters.ValidTypes = ["at+jwt"];
43+
44+
// Since we don't map inbound claims our if an email is going to exist
45+
// it will be as a `"email"` claim.
46+
options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Email;
47+
}
48+
49+
public void Configure(JwtBearerOptions options)
50+
{
51+
// Do nothing for unnamed options
52+
}
53+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections;
2+
using System.IdentityModel.Tokens.Jwt;
3+
using System.Security.Claims;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Bitwarden.Server.Sdk.Authentication;
8+
9+
internal sealed class PostAuthenticationLoggingMiddleware
10+
{
11+
private readonly RequestDelegate _next;
12+
private readonly ILogger<PostAuthenticationLoggingMiddleware> _logger;
13+
14+
public PostAuthenticationLoggingMiddleware(RequestDelegate next, ILogger<PostAuthenticationLoggingMiddleware> logger)
15+
{
16+
_next = next;
17+
_logger = logger;
18+
}
19+
20+
public Task Invoke(HttpContext context)
21+
{
22+
if (context.User.Identity == null || !context.User.Identity.IsAuthenticated)
23+
{
24+
return _next(context);
25+
}
26+
27+
var subject = context.User.FindFirstValue(JwtRegisteredClaimNames.Sub);
28+
29+
if (string.IsNullOrEmpty(subject))
30+
{
31+
return _next(context);
32+
}
33+
34+
using (_logger.BeginScope(new AuthenticatedUserLogScope(subject)))
35+
{
36+
return _next(context);
37+
}
38+
}
39+
40+
private sealed class AuthenticatedUserLogScope : IReadOnlyList<KeyValuePair<string, object>>
41+
{
42+
public string Subject { get; }
43+
44+
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
45+
46+
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
47+
{
48+
get
49+
{
50+
if (index == 0)
51+
{
52+
return new KeyValuePair<string, object>(nameof(Subject), Subject);
53+
}
54+
55+
throw new ArgumentOutOfRangeException(nameof(index));
56+
}
57+
}
58+
59+
public AuthenticatedUserLogScope(string subject)
60+
{
61+
Subject = subject;
62+
}
63+
64+
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
65+
{
66+
yield return new KeyValuePair<string, object>(nameof(Subject), Subject);
67+
}
68+
69+
IEnumerator IEnumerable.GetEnumerator()
70+
{
71+
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)