loading...

Passthrough JWT Authentication using Firebase & SignalR (.Net Core)

deeja profile image Daniel Blackwell ・4 min read

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 .:

  1. Header
  2. Payload
  3. Signature

Header

Algorithm, Key ID and Type.

{
  "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.

  1. ValidIssuer - Set to "https://securetoken.google.com/[PROJECT ID]"
  2. Audience - Set to the PROJECT ID
  3. AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(...) - Allow use of JWT
  4. Events.OnMessageReceived - Get the query string access_token and reassign to context.Token for handling.
  5. OnChallenge, OnAuthenticationFailed, OnForbidden, OnTokenValidated - Use these for debugging
  6. TokenValidationParameters - Validate everything
  7. IssuerSigningKeyResolver = manager.GetCertificate - Set the Certificate manager to be the delegated supplier of Security keys
  8. 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;
        }

Discussion

pic
Editor guide