Firebase Authentication, Cloud Endpoints and gRPC (1of2)
- 7 minutes read - 1327 wordsI’m building a service that requires user authentication. The primary endpoint is a gRPC-based service. I would like to consider using certificate-based auth but this feels… challenging. Instead, I have been aware of, but never used, Firebase Authentication and was interested to see that Cloud Endpoints includes Firebase Authentication as one of its supported auth mechanisms. Curiosity piqued, I confirmed that gRPC supports Google token-based authentication.
The following is a summary of what I did but I’ll leave the extensive documentation to Google, (Google’s) Firebase and gRPC, all of which, in this case, provide really good explanations.
Now I can:
- Authenticate with one of several authentication providers (Google, Microsoft etc.)
- Use the JWT (token) from Firebase to authenticate gRPC calls through Cloud Endpoints
Firebase Authentication
The Firebase documentation is good but it took me a while to grok the overview. I (unintentionally) stumbled upon the appropriate path of using FirebaseUI and a web workflow to enable the authentication that I need.
Firebase was acquired by Google several years ago and enabled Google to capitalize on the then somewhat nascent model of mobile apps tightly-coupled with a database. The Firebase database has been combined with Google’s then Datastore and spawned a child called Firestore. I’m using Firestore throughout my service for its simplicity (JSON document-oriented storage), flexibility (Firestore triggers) and power.
Firebase appears as something of a smorgasbord of functionality that is both distint and overlapping with Google Cloud Platform. The Firebase console remains a distinct Google Console but, the aforementioned Firestore, Cloud Functions etc. are being unified.
Coming from a Google Cloud Platform, Golang, Datastore background, it’s slightly difficult to grok Firebase. But Firebase Authentication is unique.
Essentially it enables authentication against multiple providers. In my case, I want to reduce my customers’ impedance on the platform by enabling them to use their existing (preferred) credentials. Be those Google, Microsoft, GitHub, Facebook or Twitter. None of us wants to have to create and manage usernames|passwords for every site we use. And, as a developer, I don’t want to have to do any more authentication than the bare minimum. Firebase Authentication is an excellent solution for my customers and for me.
FirebaseUI makes Firebase Authentication even more straightforward by providing canned UI elements.
I now need only:
- Configure multiple providers in Firebase Authentication
- Build a simple web page that hosts FirebaseUI and some straightforward JavaScript
And I get:
NOTE I will add more (e.g. Facebook, Twitter) but this is my prototype.
The only complexity in this process is in creating OAuth apps on each of the authentication providers that are used by Firebase Authentication as conduits for the authentication flow.
However, this process is somewhat standardized. Each OAuth app will have some form of unique identifier (Client ID, App ID etc.) and a secret (Client Secret, App Secret etc) that Firebase uses to authenticate its request for authentication. Firebase provides a redirect URI of the form: https://${FIREBASE_PROJECT_ID}.firebaseapp.com/__/auth/handler
that the provider’s apps use to call back to Firebase (with the JWT).
This step is something of a one-and-done unless you change the list of OAuth providers or your change an app as defined at the provider.
The FirebaseUI content provides a good overview of the JavaScript you need to implement the authentication flow:
// Initialize Firebase
var firebaseConfig = {{.FirebaseConfig }};
firebase.initializeApp(firebaseConfig);
// Initialize the FirebaseUI Widget using Firebase.
var ui = new firebaseui.auth.AuthUI(firebase.auth());
var uiConfig = {
callbacks: {
signInSuccessWithAuthResult: (authResult, redirectUrl) => {
// User successfully signed in.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
return true;
},
uiShown: () => {
// The widget is rendered.
// Hide the loader.
document.getElementById('loader').style.display = 'none';
}
},
// Will use popup for IDP Providers sign-in flow instead of the default, redirect.
signInFlow: 'popup',
signInSuccessUrl: '/app',
signInOptions: [
// List of enabled OAuth providers
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
"microsoft.com",
],
tosUrl: "/tos",
privatePolicyUrl: "/privacy",
};
ui.start('#firebaseui-auth-container', uiConfig);
NOTE The configuration for Microsoft’s OAuth provider was less well explained. There is no
firebase.auth.MicrosoftAuthProvider.PROVIDER_ID
and this threw me for a while. Microsoft is documented by Firebase and there’s a blog announcing support for Microsoft OAuth but Microsoft does not appear in the examples. I discovered that using"microsoft.com"
works.
A successful signin redirects to /app
which simply outputs the returned token details:
// Initialize Firebase
// Use Golang Template value FirebaseConfig
var firebaseConfig = {{.FirebaseConfig }};
firebase.initializeApp(firebaseConfig);
// Function that updates an HTML id with value
const display = (id, value) => {
document.getElementById(id).innerText = value;
};
// const user = firebase.auth().currentUser;
firebase.auth().onAuthStateChanged((user) => {
if (user) {
const name = user.displayName;
const email = user.email;
const photoURL = user.photoURL;
const emailVerified = user.emailVerified;
const uid = user.uid;
display("name", name);
display("email", email);
display("photo", photo);
display("uid", uid);
// Function returns a Promise
user.getIdToken()
.then(idToken => {
// Can't use template literal because Golang templates require back-ticks
// Unable to escape back-tickets in Golang
console.log("JWT: " + idToken);
display("id_token", idToken);
})
.catch(exception => {
console.log(exception);
});
user.getIdTokenResult()
.then(idTokenResult => {
console.log("idTokenResult: " + idTokenResult);
// Stringify idTokenResult
let result = [
"authtime: " + idTokenResult.authTime,
"claims: " + idTokenResult.claims,
"expirationTime: " + idTokenResult.expirationTime,
"issuedAtTime: " + idTokenResult.issuedAtTime,
"Provider:" + idTokenResult.signInProvider,
"Token: " + idTokenResult.token,
].reduce((result, item) => result + "\n" + item);
display("id_tokenresult", result);
})
}
})
NOTE The documentation suggests using
firebase.auth().currentUser
but I was unable to get this to work. Insteadfirebase.auth().onAuthStateChange(...)
works great.
Apologies for my JavaScript, it’s not my first language.
One very useful feature with Firebase is that localhost
is configured as one of the OAuth authorised domains (along with the Firebase project’s endpoints). This means that you can test OAuth locally without needing to deploy to Firebase (Cloud Functions).
Additionally, because it’s all JavaScript, we have the awesomeness of e.g. Chrome|Firefox web developer tools to debug code, delete state etc.
getIdToken
and getIdTokenResult
are similar. getIdToken
just returns the JWT (token) as an encoded string whereas getIdTokenResult
provides the JWT payload (name
,iss
,aud
,sub
,iat
,exp
) which comprise the issuer’s details, the authentication time, when the token was issued, the provider etc. etc.
You may use Auth0’s jwt.io
to decode JWTs. You can verify for yourself that the site isn’t sucking up your JWTs but, as always, be very careful. These are bearer tokens as we shall soon see.
For proof-of-concept purposes, I grab the JWT from the web page to authenticate my gRPC call.
Cloud Endpoints
I’ve written about [Cloud Endpoints] with gRPC (Cloud Endpoints, OpenAPI and gRPC… or not!, gRPC, Cloud Run & Endpoints) and won’t redo those discussions. Suffice to say, that it’s straightforward but involved and I know it works.
One perhaps obvious recommendation is that you likely want to gcloud run deploy ...
using --no-allow-unauthenticated
to ensure that only authenticated users navigate your proxy and that no-one goes around the proxy to the service directly.
As I’ve shown previously, you can then:
# Get a JWT from Google
TOKEN=$(gcloud auth print-identity-token)
# Authentication using it
# Call the backend service
grpcurl \
-H "Authorization: Bearer ${TOKEN}" \
-d "{ ... }" \
-proto something.proto \
${SRV_HOST}:${PORT} \
v1alpha1.SomeService/SomeMethod
# Authenticate using it
# Go via the ESP proxy
grpcurl \
-H "Authorization: Bearer ${TOKEN}" \
-d "{ ... }" \
-proto something.proto \
${ESP_HOST}:${PORT} \
v1alpha1.SomeService/SomeMethod
But, what this shows is that an authorised user can get credentials and call the service.
It’s cool but it’s not new bananas.
What is interesting is that, using a JWT acquired through Firebase Authentication using say Microsoft OAuth, I can also authenticate (but only) through the ESP proxy:
FIREBASE_TOKEN="..." # JWT received from my the above Firebase Auth implementation
grpcurl \
-H "Authorization: Bearer ${FIREBASE_TOKEN}" \
-d "{ ... }" \
-proto something.proto \
${ESP_HOST}:${PORT} \
v1alpha1.SomeService/SomeMethod
Unfortunately, Cloud Endpoints has no UI for its authentication. Rather the proxy forwards the authentication (the JWT) as a X-Endpoint-API-UserInfo
header. The receiving service can then unwrap the authentication and decide how to proceed. I’ll explore that in a subsequent post.