Why? No backchannel validation
We can validate JWT tokens without any communication with the issuer using public keys.
This means we can know that everything that is provided in the JWT is valid without a callout to somewhere else.
Pros
- Many times faster than calling out to a backchannel service
- As there are no backchannel requests, no API limits can be hit.
Neutral
- Payload can be any size up to 7KB
Cons
- Token cannot be revoked once created; token can only expire.
My code for the .Net Validation Setup is available here: https://gist.github.com/deeja/c67e6027ca37a8d6a367b8b8bf86d5c6
It should be a guide only! There needs to be work put in to make it production ready.
Firebase Authentication
When logged on with Firebase Authentication, the client is provided a Json Web Token (JWT). In the case of Firebase, these can be validated using publically shared x509 certificates.
Gettting the token
There isn't much in this post around setting up and using Firebase Auth client-side as that's not what this is supposed to be.
If you are wanting to use Firebase, I recommend following a tutorial or two, then come back to this post.
Post Login
After login, you will need to exchange your ID token for a JWT token.
If you are using Nuxt.js, here is a Firebase plugin that uses the @nuxt/firebase
module
The Token
The Firebase JWT looks a bit like this:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjIxODQ1OWJiYTE2NGJiN2I5MWMzMjhmODkxZjBiNTY1M2UzYjM4YmYiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiU3RldmUgTWNRdWVlbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vYXBpY3VybCIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9bUFJPSkVDVC1JRF0iLCJhdWQiOiJbUFJPSkVDVC1JRF0iLCJhdXRoX3RpbWUiOjE1OTU1NjM2NzAsInVzZXJfaWQiOiJbVVNFUiBJRF0iLCJzdWIiOiJbVVNFUiBJRCBBR0FJTl0iLCJpYXQiOjE1OTQ2Mzc2NTksImV4cCI6MTU5NDY0MTI1OSwiZW1haWwiOiJbRU1BSUxdIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyI5ODI3MzQ1OTc4MzQ1MDIzNDU5OCJdLCJlbWFpbCI6WyJbRU1BSUxdIl19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.Q8p3zArOtkqcnNlNhBfdU7Bo8vtW5ML-D019lsRJTFe_hj65bNqbLyVU1BRhTsTS87DyQlA-acXmY22i5mS-vzhZcRXzoV-gkAn8Zy1xUprp7kh6he8uiIK5EoO4045e-gGFR8z3AqgpW-ZetCRT0gejq_q9mSg6cyz0UP7RCVXXyFns-RhU4gk_r7HzIclFGfPIEqabYuufJQZ_-Hv_do3gUt5BljfqAwAsSB6V8oxTfSxfqI_IBMiyU-Lxa-nCwt_S0kLWueIUUhsdkkHy2NSp4Y2EqLPtIUeWEq8EMbVfCoMKLD_TVGEk3NRPMcPQNC6CTpLUuQgpxFCaIcPXVw
Which splits into three parts, delimited by .
:
- Header
- Payload
- Signature
Header
Algorithm, Key ID and Type.
- Firebase uses the RS256 encryption method.
- The Key ID references public/shared keys at https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com
{
"alg": "RS256",
"kid": "218459bba164bb7b91c328f891f0b5653e3b38bf",
"typ": "JWT"
}
Payload:
General info and claims
{
"name": "Steve McQueen",
"picture": "https://lh3.googleusercontent.com/a-/apicurl",
"iss": "https://securetoken.google.com/[PROJECT-ID]",
"aud": "[PROJECT-ID]",
"auth_time": 1595563670,
"user_id": "[USER ID]",
"sub": "[USER ID AGAIN]",
"iat": 1594637659,
"exp": 1594641259,
"email": "[EMAIL]",
"email_verified": true,
"firebase": {
"identities": {
"google.com": [
"98273459783450234598"
],
"email": [
"[EMAIL]"
]
},
"sign_in_provider": "google.com"
}
}
Validation Signature
The signature is a verification token generated using Google's private keys, which can be verified using the public / shared keys.
For more info on how this is done, check out https://jwt.io/
SignalR
https://dotnet.microsoft.com/apps/aspnet/signalr
SignalR is a websockets framework that works "natively" with .Net.
The connections are made to "Hubs", and those "Hubs" co-ordinate responses based on messages and events.
SignalR JS Client
The SignalR JS client gets the JWT via a factory method on the HubConnectionBuilder.
An interesting thing is that SignalR doesn't appear to support the Bearer [My Token]
Authorization header.
Instead, the token is added as a query sting with the name access_token
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();
SignalR .Net Host / Server
The Host is a bit more complicated. The code for this is available on my gist https://gist.github.com/deeja/c67e6027ca37a8d6a367b8b8bf86d5c6
I will go over some of the details here.
-
ValidIssuer
- Set to "https://securetoken.google.com/[PROJECT ID]" -
Audience
- Set to the PROJECT ID -
AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(...)
- Allow use of JWT -
Events.OnMessageReceived
- Get the query stringaccess_token
and reassign to context.Token for handling. -
OnChallenge
,OnAuthenticationFailed
,OnForbidden
,OnTokenValidated
- Use these for debugging -
TokenValidationParameters
- Validate everything -
IssuerSigningKeyResolver = manager.GetCertificate
- Set the Certificate manager to be the delegated supplier of Security keys -
AddCors
UseCors
- Required for SignalR
CertificateManager.cs
As the Google public certificates can change, these need to periodically refreshed. For this I have added a CertificateManager
to the gist which holds a task called _backgroundRefresher
private readonly Task _backgroundRefresher;
public CertificateManager()
{
_backgroundRefresher = Task.Run(async () =>
{
while (true)
{
await RefreshTokens();
await Task.Delay(1000 * 60 * CertificateFetchIntervalMinutes);
}
});
}
Certificates are hydrated from the provided JSON
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));
GetCertificate
is the member that was delegated to handle the request for Certificates in the JwtBearer options.
public IEnumerable<SecurityKey> GetCertificate(string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters)
{
_lock.EnterReadLock();
var x509SecurityKeys = _certificates.Where((pair, i) => pair.Key == kid).Select(pair => pair.Value).ToArray(); // toArray() should be called collapse expression tree
_lock.ExitReadLock();
return x509SecurityKeys;
}
Top comments (1)
S2