Passthrough JWT Authentication using Firebase & SignalR (.Net Core)
 
Tags: javascript, dotnet, jwt, firebase
Published: 2020-07-13 12:57:41

# 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](https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com).

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

- JS API ["Firebase Auth: getIdTokenResult()"]("https://firebase.google.com/docs/reference/js/firebase.User#getidtokenresult")
- REST ["Exchange a refresh token for an ID token"](https://firebase.google.com/docs/reference/rest/auth#section-refresh-token)

If you are using Nuxt.js, [here is a Firebase plugin](https://gist.github.com/deeja/0af182c789449a9074166e8acc5c325b) 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
1. Payload
1. 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

```js
{
  "alg": "RS256",
  "kid": "218459bba164bb7b91c328f891f0b5653e3b38bf",
  "typ": "JWT"
}
```
### Payload:
General info and claims
```js
{
  "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`

```js
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]"
1. `Audience` - Set to the PROJECT ID
1. `AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(...)` - Allow use of JWT
1. `Events.OnMessageReceived` - Get the query string `access_token` and reassign to context.Token for handling. 
1. `OnChallenge`, `OnAuthenticationFailed`, `OnForbidden`, `OnTokenValidated` - Use these for debugging
1. `TokenValidationParameters` - Validate everything
1. `IssuerSigningKeyResolver = manager.GetCertificate` - Set the Certificate manager to be the delegated supplier of Security keys
1. `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`

```csharp
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
```csharp
 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.
```csharp
 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;
        }
```