Skip to content

Instantly share code, notes, and snippets.

@deeja
Last active October 4, 2024 09:41
Show Gist options
  • Save deeja/c67e6027ca37a8d6a367b8b8bf86d5c6 to your computer and use it in GitHub Desktop.
Save deeja/c67e6027ca37a8d6a367b8b8bf86d5c6 to your computer and use it in GitHub Desktop.
Validating a Firebase JWT with .Net Core and SignalR (without .Net FirebaseAdmin)
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();
}
}
}
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();
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