Using firebase for authentication in an R plumber application: an example with Angular
A few weeks back I started to move an Express.js application to R plumber with the idea of avoiding the ugliness of interacting with a bunch of R scripts from this JavaScript backend.
One missing component for this migration was the authentication piece for which the app currently uses firebase. I’ve been using firebase for a while now as I really enjoy how easy it is to enable authentication with different providers, including Google, Facebook, GitHub, etc, (because lets face it, we are all tired of having to remember so many passwords, so it is great to be able to use common providers). It also offers a generous free plan that is perfect for most of my use cases (e.g. 10K/month authentications).
In this post, I show a minimal example of how I went about implementing authentication in an R plumber application and an Angular frontend using firebase.
- Creating a firebase project and enabling authentication with Google
- Adding an authentication filter in R plumber
- Interacting with the R plumber application from an Angular frontend
Creating a firebase project and enabling authentication with Google
Although the firebase UI has changed across the years, creating a project and enabling authentication with different providers is straightforward. After navigating to https://firebase.google.com/ sign in with your Google account -> Get started -> Create a project and follow the instructions.
If all went ok, now you should have your project listed in the console. For example:
Click on the project and click on “Authentication” in the left panel follow by “Get started”. A list of providers should then appear. For this example, select Google -> enable (note that a support e-mail should be provided).
Assuming all went ok, now Google should be listed as a “Sign-in method”.
And that’s it for this first part. Now let’s work in the R plumber backend.
Adding an authentication filter in R plumber
In a nutshell, when users authenticate, they will obtain an “Authorization” token that gets stored directly in the browser. This token is then sent with each request to the API (in our case R plumber API) where it can be verified.
In order to verify the token that will be received with each request to the API, we can use the `jwt_decode_sig()` function from the “jose” package. A snippet looks like:
library(jose)
jwt_decode_sig(token,"./firebase_certs.pem")
The firebase_cets.pem file include the public keys that we can use to verify authentication tokens from firebase and can be obtained from this link (or the git repo at the end of this post). More information about these, can be obtained here.
Assuming that we get a valid token. The above snippet will return an R object with the following fields:
$name #User name
$picture #Avatar URL
$iss #Must be "https://securetoken.google.com/<projectId>",
$aud #Must be your Firebase project ID
$auth_time #Authentication time
$user_id #User id
$sub #Non-empty string and must be the uid of the user or device.
$iat #Issued-at-time
$exp #Expiration time
$email #User's email address
$email_verified #Whether the email has been verified
$firebase$sign_in_provider #Google in this case
If the token is not valid, it will throw an error.
With all the above fields we could play around to create different flavors of an authentication logic for the API. For this example, however, I opted for the simplest logic — a user is correctly authenticated as far as jwt_decode_sig does not throw an error).
The full R plumber API code for this example looks like:
library(plumber)
library(jose)#' @filter cors
cors <- function(req, res) {
res$setHeader("Access-Control-Allow-Origin", "*")
if (req$REQUEST_METHOD == "OPTIONS") {
res$setHeader("Access-Control-Allow-Methods","*")
res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
res$status <- 200
return(list())
} else {
plumber::forward()
}
}#* @filter checkAuthentication
function(req, res){
auth <- try(jwt_decode_sig(req$HEADERS['authorization'],"./firebase_certs.pem"), silent=TRUE)
if (inherits(auth,"try-error")){
res$status <- 401
return()
}
print(auth) #For inspection purposes only
plumber::forward() #User authenticated, continue
}#* @post /probability
#* @param successes
#* @param size
#* @param p
function(successes, size, p) {
return(dbinom(successes, size, p))
}
In the code above, I am using two filters. More information about filters is described in a previous post. Briefly, each HTTP request will go through each filter until reaching the solicited endpoint. The first filter is the CORS (Cross-Origin Resource Sharing) filter and enables an application running in a different origin to make requests to the API. The latter is pretty useful during development.
The “checkAuthentication” filter is the one that will block the request if the user is not authenticated and return a 401 (Unauthorized) status, otherwise will allow the request to go through (plumber::forward()).
Finally, this application has a single endpoint that returns the value of the probability density function of the binomial distribution given certain number of successes (e.g. number of heads), number of trials (e.g. number of tosses) and probability of success on each trial (e.g. probability of heads).
In order to now start the R plumber application, the following command can be used (note that other ports can be used):
library(plumber)
r<-plumb("plumber.R")$run(port=8080)
#Running plumber API at http://127.0.0.1:8080
#Running swagger Docs at http://127.0.0.1:8080/__docs__/
Interacting with the R plumber application from an Angular frontend
The Angular frontend is considerably more convoluted, so I will only describe the relevant parts of the code.
In order to interact with firebase, we can use the @angular/fire package. Once this is installed, it can be added and configured in (e.g.) the AppModule by adding the following lines to app.module.ts:
//app.module.ts
....
import { getAuth, provideAuth } from '@angular/fire/auth';
import { AngularFireModule } from '@angular/fire/compat';
import { environment } from 'src/environments/environment';
....@NgModule({
....
imports: [
....
AngularFireModule.initializeApp(environment.firebase),
provideAuth(() => getAuth()),
....
],
....
})
export class AppModule { }
The environment.firebase object contains the firebase project configuration information:
//environment.ts
export const environment = {
production: false,
firebase: {
apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
authDomain: "plumber-auth.firebaseapp.com",
projectId: "plumber-auth",
storageBucket: "plumber-auth.appspot.com",
messagingSenderId: "XXXXXXXXXXXXXXXX",
appId: "XXXXXXXXXXXXXXXXXXXXXXX"
},
API_ENDPOINT: 'http://127.0.0.1:8080' //plumber API URL
};
The configuration information can be obtained from firebase by clicking on the </> (web) button:
Now that we have configured firebase in the Angular application, we can start authenticate users. A minimal authentication service could look something like:
//auth.service.ts
import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/compat/auth';
import firebase from 'firebase/compat/app';@Injectable({
providedIn: 'root'
})
export class AuthService { public currentUser: firebase.User | null = null;
constructor(private auth: AngularFireAuth) {} initAuthListener() {
this.auth.authState.subscribe(user => {
if (user) {
this.currentUser = user;
} else {
this.currentUser = null;
}
});
} loginGoogle() {
this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
}
logout() {
this.auth.signOut();
}}
The initAuthListener()
will be used to detect when the user logs in and logs out and will be triggered when the application starts. loginGoogle()
will open the traditional popup so that users can authenticate with their Google credentials, and logout()
will of course log the user out.
Now, the service that interact with the R plumber application looks like:
Here, the getProbability()
function will first check if this.authService.currentUser exists and if so will get the “authorization” token with getIdToken()
and add it to the request headers. Note that even if the user is not authenticated, we will carry out the HTTP request just to demonstrate that our plumber application would return an error if the user is not authenticated.
If the the request is successful, then a probability based on some parameters will be returned, otherwise, an error will be shown in a snackbar with showMessage()
.
Finally, the component and template that interact with this service look like:
The DbinomFormComponent class has the probability observable that will be used to listen to changes in the probability estimates that are obtained after triggering getProbability()
(assuming the user is authenticated). In the template, I subscribe directly to the observable with the async pipe the show the output of the request stored in p.
The final product of this example looks like this:
All the code for this can be found here.