Last active
October 4, 2024 09:41
-
-
Save deeja/c67e6027ca37a8d6a367b8b8bf86d5c6 to your computer and use it in GitHub Desktop.
Validating a Firebase JWT with .Net Core and SignalR (without .Net FirebaseAdmin)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Net; | |
using System.Security.Cryptography.X509Certificates; | |
using System.Text; | |
using System.Text.Json; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.IdentityModel.Tokens; | |
// Used for the supply, and periodic retrieval, of Firebase public certificates | |
public class CertificateManager : IDisposable | |
{ | |
private readonly Task _backgroundRefresher; | |
private readonly Uri _googleCertUrl = new Uri("https://www.googleapis.com/robot/v1/metadata/x509/[email protected]"); | |
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); | |
private int _certificateFetchIntervalMinutes = 10; | |
private Dictionary<string, X509SecurityKey> _certificates = new Dictionary<string, X509SecurityKey>(); | |
public CertificateManager() | |
{ | |
_backgroundRefresher = Task.Run(async () => | |
{ | |
while (true) | |
{ | |
await RefreshTokens(); | |
await Task.Delay(1000 * 60 * CertificateFetchIntervalMinutes); | |
} | |
}); | |
} | |
public int CertificateFetchIntervalMinutes | |
{ | |
get => _certificateFetchIntervalMinutes; | |
set => _certificateFetchIntervalMinutes = Math.Max(value, 1); | |
} | |
public void Dispose() | |
{ | |
_backgroundRefresher?.Dispose(); | |
_lock?.Dispose(); | |
} | |
public async Task RefreshTokens() | |
{ | |
Console.WriteLine($"{nameof(CertificateManager)}.{nameof(RefreshTokens)}: Refreshing Tokens"); | |
_lock.EnterWriteLock(); | |
try | |
{ | |
var wc = new WebClient(); | |
var jsonString = await wc.DownloadDataTaskAsync(_googleCertUrl); | |
var keyDictionary = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(new MemoryStream(jsonString)); | |
_certificates = keyDictionary.ToDictionary(pair => pair.Key, pair => new X509SecurityKey(new X509Certificate2(Encoding.ASCII.GetBytes(pair.Value)), pair.Key)); | |
Console.WriteLine($"{nameof(CertificateManager)}.{nameof(RefreshTokens)}: Certificates: {_certificates.Count}"); | |
} | |
catch (Exception e) | |
{ | |
Console.WriteLine(e); | |
throw; | |
} | |
finally | |
{ | |
_lock.ExitWriteLock(); | |
} | |
} | |
public IEnumerable<SecurityKey> GetCertificate(string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) | |
{ | |
_lock.EnterReadLock(); | |
try | |
{ | |
var x509SecurityKeys = _certificates.Where((pair, i) => pair.Key == kid).Select(pair => pair.Value).ToArray(); // toArray() should be called collapse expression tree | |
return x509SecurityKeys; | |
} | |
finally | |
{ | |
_lock.ExitReadLock(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr"; | |
// using a delegate function as the factory | |
const getMyJwtToken = () => { /* return the token from somewhere */}; | |
const connection = new HubConnectionBuilder() | |
.withUrl(connectionUrl, {accessTokenFactory: getMyJwtToken }) | |
.withAutomaticReconnect() | |
.configureLogging(LogLevel.Information) | |
.build(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Authentication.JwtBearer; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.IdentityModel.Tokens; | |
using Workboard.Web.Hubs; | |
namespace Workboard.Web | |
{ | |
public class Startup | |
{ | |
public const string PROJECT_ID = "my-project-121234"; | |
public const string AUDIENCE = PROJECT_ID; | |
public const string ISSUER = "https://securetoken.google.com/" + PROJECT_ID; | |
public static string[] CORS_ORIGINS = { "http://localhost:3000" }; // CORS Origin is required for SignalR | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
CertificateManager manager = new CertificateManager(); | |
services.AddCors(); | |
// Nuget Package: Microsoft.AspNetCore.Authentication.JwtBearer | |
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => | |
{ | |
options.Events = new JwtBearerEvents | |
{ | |
OnMessageReceived = context => | |
{ | |
// SignalR doesn't appear to have the bearer header | |
// The JWT is added as a query string when using the JS token factory on the SignalR JS Api | |
// JS API: new HubConnectionBuilder().withUrl(connectionUrl, {accessTokenFactory: () => getMyJwtToken()}) | |
var accessToken = context.Request.Query["access_token"]; | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
context.Token = accessToken; | |
} | |
return Task.CompletedTask; | |
}, | |
/** The following hooks are very handy for debugging */ | |
//OnChallenge = context => Task.CompletedTask, | |
//OnAuthenticationFailed = context => Task.CompletedTask, | |
//OnForbidden = context => Task.CompletedTask, | |
//OnTokenValidated = context => Task.CompletedTask | |
}; | |
options.Audience = AUDIENCE; | |
options.TokenValidationParameters = new TokenValidationParameters | |
{ | |
ValidIssuer = ISSUER, | |
ValidateIssuer = true, | |
ValidateAudience = true, | |
ValidateLifetime = true, | |
ValidateIssuerSigningKey= true, | |
ValidateActor = true, | |
ValidateTokenReplay = true, | |
IssuerSigningKeyResolver = manager.GetCertificate // Delegate for resolving certificates | |
}; | |
}); | |
services.AddSignalR(options => | |
{ | |
#if DEBUG | |
options.EnableDetailedErrors = true; | |
#endif | |
}); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseAuthentication(); | |
app.UseRouting(); | |
app.UseCors(builder => builder.WithOrigins(CORS_ORIGINS).AllowCredentials().AllowAnyHeader()); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapHub<ChatHub>("/myhub"); | |
// A root page is not required, but it's nice to have a health page to poll | |
endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); | |
}); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment