Previously we covered Authentication in Angular and why it was hard to wrap our heads around. To pick up from there, in this series of articles we are going to adopt Firebase Auth as a third party, and investigate different options: managing users remotely, and using Firebase for Authentication only.
This series is not a tutorial for AngularFire per se. We are interesting in knowing how to connect the dots in a true Angular application.
I recommend this YouTube tutorial to integrate AngularFire. In addition to the official Firebase documentation.
Setup and standalone
First, we need to setup AngularFire as documented, or run the following:
npm install @angular/fire firebase
Follow the documentation to add it to module-based
app. Here is how we should add it to a standalone
application.
// main.ts
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';
const firebaseProviders: EnvironmentProviders = importProvidersFrom([
// firebaseConfig is the json extracted for client-side web app
provideFirebaseApp(() => initializeApp(firebbaseConfigHere)),
provideAuth(() => getAuth()),
]);
bootstrapApplication(AppComponent, {
providers: [
// provide few things like interceptors and routes
...CoreProviders,
...AppRouteProviders,
firebaseProviders
],
});
To customize it (Firebase customize dependencies), we can do the following:
// main.ts custom depdencies
// ...
const fbApp = () => initializeApp(firebaeConfigHere);
const authApp = () => initializeAuth(fbApp(), {
persistence: browserSessionPersistence,
popupRedirectResolver: browserPopupRedirectResolver
});
const firebaseProviders: EnvironmentProviders = importProvidersFrom([
provideFirebaseApp(fbApp),
provideAuth(authApp),
]);
// ...
A side note about SSR
Although SSR via Angular Universal is quite capable, but the reasoning behind implementing one keeps escaping me! For content-based websites, normal HTML pages proved to be superior, and in apps behind authentication walls, SSR does not matter much. In addition to the fact that search bots are getting better. That being said, if you have an SSR with Auth, in standalone mode, here is how to do it:
// server.ts
const fbApp = () => initializeApp(Config.Auth.firebase);
const authApp = () => initializeAuth(fbApp(), {
persistence: browserSessionPersistence,
popupRedirectResolver: browserPopupRedirectResolver
});
const firebaseProviders: EnvironmentProviders = importProvidersFrom([
provideFirebaseApp(fbApp),
provideAuth(authApp),
]);
const _app = () => bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(ServerModule),
...CoreProviders,
...AppRouteProviders,
firebaseProviders
],
});
// export the bare minimum, let nodejs take care of everything else
export const AppEngine = ngExpressEngine({
bootstrap: _app
});
Note: you can find all functions in the modular tree-shakable version documented on Firebase Auth Modular.
The use case
The main use case we want to try to implement involves the following:
- Login with Password authentication, and Google authentication
- Create user with access (example:
admin
) - Add attribute to user: (example:
bloodType
) - Connect to our API (not Firebase apps, nor Google API), and properly maintain a healthy token for all Http requests
The bare minimum: AuthService
The difference between password and social login is that we need to create the user in Firebase manually. Thus Firebase provides two methods: signInWithEmailAndPassword
and createUserWithEmailAndPassword
. We will attempt to continue building up on our original AuthService
. So the login component should not change. The register sequence is almost identical, we just need to pass extra custom attributes to deal with later.
Here is a link to the original StackBlitz project that we will attempt to recreate with Firebase, I am not revealing the new project just yet, because it's too premature.
// app/services/auth.service
import { Auth, signInWithEmailAndPassword } from '@angular/fire/auth';
// ...
export class AuthService {
constructor(private auth: Auth) { }
Login(email: string, password: string): Observable<any> {
const res = () => signInWithEmailAndPassword(this.auth, email, password);
// build up a cold observable
return defer(res);
}
// the sign up uses createUserWithEmailAndPassword
Signup(email: string, password: string, custom: any): Observable<any> {
const res = () => createUserWithEmailAndPassword(this.auth, email, password);
// it also accepts an extra attributes, we will handle later
return defer(res)
}
LoginGoogle(): Observable<any> {
const provider = new GoogleAuthProvider(); // from @angular/fire/auth
const res = () => signInWithPopup(this.auth, provider);
return defer(res);
}
}
The public login page we'll keep to the bare minimum:
// components/public/login.component
@Component({ ... })
export class PublicLoginComponent {
constructor(private authService: AuthService, private router: Router) {}
login() {
this.authService
.Login('email@address.com', 'valid_firebase_password')
.pipe(catchError...)
.subscribe({
next: (user) => {
// redirect to dashbaord
this.router.navigateByUrl('/private/dashboard');
}
});
}
// example register with email
signUp() {
const rnd = Math.floor(Math.random() * 1000);
this.authService
.Signup(`user${ rnd }@email.com`, 'valid_firebase_password', { bloodType: 'B+' })
.pipe(catchError...)
.subscribe({
next: (user) => {
this.router.navigateByUrl('/private/dashboard');
}
});
}
// example login with google, later we need to figure out the new user
loginGoogle() {
this.authService
.LoginGoogle()
.pipe(catchError...)
.subscribe({
next: (user) => {
this.router.navigateByUrl('/private/dashboard');
}
});
}
}
The returned object from Firebase looks like this
// User model from firebase containst those basic information
// <userCredentials.User>
{
refreshToken
,displayName
,email
,uid
,providerData
}
Note that we will use .then()
syntax to be able to catch errors in UI, but you can choose to use asycn await
with try catch
block instead. We also returned the Promise
as an Observable
using RxJS defer()
operator, to keep our solution as smooth as it can be.
You can also use RxJS
from()
but remember it is a hotobservable
, and will be emitting a value whether subscribed to or not. A subscription will not make it run again though.
AngularFire provides three observables
to watch changing tokens and auth state:
// AngularFire observables
const auth = inject(Auth); // from @angular/fire/auth
user(auth);
authState(auth);
idToken(auth);
The difference is that authState
observable is not triggered during refresh token. However, the idToken
is what we will be using most of the time.
Now what?
If you are using the application with other Firebase apps, like Firestore, or Realtime Database, there is nothing you aught to do besides catching errors, and creating users. The authenticated user is sent via request.auth
, and you can manage access directly in Firebase console, using rules. This article is not about that. If you, like the rest of us, connect to your own API, then stick around.
The way forward
In Firebase Documentation:
..., you can retrieve an ID token from a client application signed in with Firebase Authentication and include the token in a request to your server. Your server then verifies the ID token and extracts the claims that identify the user (including their
uid
, the identity provider they logged in with, etc.). This identity information can then be used by your server to carry out actions on behalf of the user.
We only need proper authorization for our API, no third-party provider communication is needed.
Speaking directly to a Google API, or a Twitter API, would request knowing the provider, and passing the correct access token given by the provider, not by Firebase Auth.
We have three ways to build our application:
- Keep it foreign: let all authentication and user management be on Firebase
- Bring it home: recreate the users on our server, only use Firebase Auth for social login
- Middle ground: Use Firebase for authentication, and map to a local user (recommended)
Here is a general outline for each:
I. Keep it foreign
We can continue to use the same user returned from Firebase, and completely rely on Firebase to manage users. we can add extra attributes using Admin SDK: custom claims.
Pros: one place to manage users, Cons: one place to manage users, that isn't our place. It's not an easy decision to let a third party take control of all user management, it is a pattern to have the authentication on one server, and user management on another. This also involves having to create our own in house admin platform to manage users.
The sequence of events is as follows:
- Sign in with username and password, or third party through Firebase.
- For a new user, request
bloodType
from user - Send result to API with Admin SDK, to
verify
user. UsesetCustomUserClaims
for custom attributes.
II. Bring it home
The opposite extreme is to verify the token to recreate another access token, then take it from there. This involves asking the user to choose a new password after signing in by social accounts. There is also the option to consult the Firebase Admin SDK to verify the token and use it to identify the user, instead of a password.
Pros: it shuts off Firebase User management completely, Cons: it involves token management on the API level. It might be a good solution if we have other providers or older users. It involves JWT creation and validation.
This is by far the least attractive solution, and it is not much about Firebase, so I will not dig deep into this one.
III. A middle ground
The middle ground is to use the Firebase Auth to create users on Firebase, then map them to local users, and depend on our state management instead of Firebase state management. This is my most favorite solution. It decouples authentication from user management without ever having to create new tokens. I am going with this option.
Here is a note on Firebase docs worthy of mentioning: Custom claims are added to the user's ID token which is transmitted on every authenticated request. For profile non-access related user attributes, use database or other separate storage systems.
So they too recommend this approach.
Here are the basic ingredients for all three.
Laying the foundation
All three solutions involve Admin SDK verification. Since that is sever side matter, we are going to build a simple NodeJs and ExpressJS server, to mimic our API. We need a middleware to capture all API calls, with the token set in the header, to verify the user.
Firebase Admin SDK
First, go through the documentation on how to add Firebase Admin SDK to the server.
Express middleware
Then we'll place the SDK application in its function, and pass it to the middleware, or any other route to use it.
// find this in /server/firebase.sdk.js
// API server Admin SDK app
// use firebase to verify
const admin = require("firebase-admin");
// get this json from the project settings in Firebase console
const serviceAccount = require("./path-to-service-account.json");
// initialize firebase
exports.sdk = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
Then we can pass it to the middleware
// server/auth.middleware.js
// expressJs middleware on API server
module.exports = function (sdk) {
return function (req, res, next) {
// check header for token then verify, find user, return info
var authheader = req.headers['authorization'];
// if not move on with none
if (authheader) {
// call auth then veirfyIdToken
sdk
.auth()
.verifyIdToken(authheader)
.then(function (decodedToken) {
// save in res locals, or fetch profile from db first...
// depends on choice of solution
res.locals.user = decodedToken;
// next
next();
})
.catch(function (error) {
res.locals.user = null;
next();
});
} else {
next();
}
};
};
// in the main server (server/server.js), get the sdk
const sdk = require('./firebase.sdk').sdk;
// pass it to middlewear
const verify = require('./auth.middleware')(sdk);
app.use(verify); // app is the express app created
// we can also pass it to any route that needs it
app.use('/api', require('./api/account')(sdk));
// ... other routes and and app.listen
An example Express API route that makes use of it looks like this
// example routes file for getting account information after firebase validation
const express = require('express');
module.exports = function (sdk) {
var router = express.Router();
router.get('/account', function (req, res) {
// get auth from req local
const user = res.locals.user;
if (user) {
res.json({
data: user,
});
} else {
res.status(401).json({
message: 'Access denied',
code: 'ACCESS_DENIED',
});
}
});
router.post('/user', function (req, res) {
// use the sdk passed when needed
sdk.auth().setCustomUserClaims(...)
// ...
});
// export and use this router in the main server
return router;
};
We can also use getAuth()
directly instead of passing round the sdk
application. I have no preference.
// alternatively getAuth directly in any Express file
const { getAuth } = require('firebase-admin/auth');
// in a route:
getAuth().setUserCustomClaims...
Diving in
The first option is to rely completely on Firebase Auth, to dive into it, tune in next Tuesday inshalla. Let's get some sleep. 🔻
Top comments (0)