Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 2489

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 26, 20242024-11-26T05:53:11+00:00 2024-11-26T05:53:11+00:00

III – Firebase For Authentication only in Angular

  • 61k

The middle ground is to use Firebase Auth, for authentication, and use our own API for user management.

Find the project on Github.

Express API routes

The idea of having two servers, one for authentication and one for profiling is to deal with the profile server (our API) as a normal data source, with authentication required. The authentication server is expected to send in an access token, which is verified first, before it moves down the pipe of creating and editing user profile. So after signing in and signing up with password, or after signing in with Google, we need to send the token to our Express server to create the user, fetch user, or patch user.

// server/routes router.post('/auth/login', function (req, res) {   // if id does not exist create user, else update user   const payload = req.body;   const token = payload.token;   // we need to first verify token   sdk.auth().verifyIdToken(token).then(function (decodedToken) {     // then find user by id     const id = decodedToken.uid;     // find profile in db, example     const profile = {...};      // if user does not exist, create one     if (!profile) {       // save new user to db then return       // we don't have bloodType just yet       const _newuser = {         id: decodedToken.uid,         picture: decodedToken.picture,         // use the decodeToken for now         email: decodedToken.email,         // some hard coded attributes         admin: true,         // this flag is important         newUser: true};       // save _newuser in db here, then return       // ...       res.json(_newuser);     } else {       // return existing user with full profile, including bloodtype       // watch out for client-side gap, bloodType may still not exist       res.json(profile);     }    }).catch(function (error) {     // 401   }); });  // update logged in user router.patch('/user', function(req, res) {   // get from middleware   const user = res.locals.user;    if (user) {     // update user in db with body     const payload = req.body;     let profile = {...user};     profile.bloodType = payload.bloodType;     // switch off newUser     profile.newUser = false;     // save to db     // ...     // update locals     res.locals.user = profile;      // return profile     res.json(profile);   } else {      res.status(401).json({       message: 'Access denied',       code: 'ACCESS_DENIED'     });   } }); 
Enter fullscreen mode Exit fullscreen mode

As you can see I have saved the newUser flag in our server, because it is crucial to close a UI gap, we'll cover later.

Sign in with password in AuthService would call the first route.

// services/auth.service // login changed, we'll map to IAuthInfo later Login(email: string, password: string): Observable<IAuthInfo> {   const res = () => signInWithEmailAndPassword(this.auth, email, password);   return defer(res).pipe(     // get the token to force a fresh one     switchMap(auth => auth.user.getIdToken()),     // login user in our API     switchMap(token => this.LoginUser(token))   ); }  LoginUser(token: string): Observable<IAuthInfo> {   return this.http.post('/auth/login', { token });   // we need to later save into state } 
Enter fullscreen mode Exit fullscreen mode

Sign up with password would call the first route, then request the bloodType from the user, then call the PATCH /user next.

// services/auth.service // sign up changed SingUp(email: string, password: string, custom: any): Observable<IAuthInfo> {   // here send a sign up request, with extra params   const res = () =>     createUserWithEmailAndPassword(this.auth, email, password);    return defer(res).pipe(     // first new token     switchMap(auth => auth.user.getIdToken()),     // log user in and request extra information     switchMap(token => this.LoginUser(token)),     // then patch     switchMap(_ => this.UpdateUser(custom)   ); }  UpdateUser(custom: any): Observable<IAuthInfo> {   return this.http.patch('/user', custom).pipe(     // we need to update state again here   ); } 
Enter fullscreen mode Exit fullscreen mode

In our components so far nothing has changed. But we can do better. In a minute. First, let's see what changed on Sign in with Google.

Remember that we faced an issue with null tokens before, so here we getIdToken before any action to API.

Sign in with Google

In the previous episode we depended on the Firebase service to tell us whether the user is new or not using getAdditonalUserInfo, before we proceeded to show the sign up form. Now we need to log the user in our API server first. Our API server can take on the job and check if uid already exists, and return newUser from our data source. Initially the service call looks like this

// serivces/auth.service  // Login by Google initially: LoginGoogle(): Observable<IAuthInfo> {   const provider = new GoogleAuthProvider();   const res = () => signInWithPopup(this.auth, provider);    return defer(res).pipe(     switchMap(auth => auth.user.getIdToken()),     // call our API and check for uid     switchMap(token => this.LoginUser(token))   ); } 
Enter fullscreen mode Exit fullscreen mode

We can use the IAuthInfo model to check if user is new (remember the /auth/login returns newUser flag).

// components/public/login.component  // login with Google loginGoogle() {   this.authService.LoginGoogle()   .subscribe({     next: (user) => {       // read newUser from our API       if (!user.newUser) {         this.router.navigateByUrl('/private/dashboard');       } else {         // show the sign up field (somehow)         this.showMeForm = true;       }     },   }); } // finish sign up by google update() {   // grab form bloodType, example: B+   this.authService.UpdateUser({ bloodType: 'B+' }).subscribe({     next: (user) => this.router.navigateByUrl('/private/dashboard');   }); } 
Enter fullscreen mode Exit fullscreen mode

If user is new, after submitting and calling update we proceed normally to call UpdateUser({bloodType}) to PATCH user.

The new user flag

The first thing we need to fix is this gap between Login and Sign Up, if user does not finish sign up and comes back later to try with the same Google email, Firebase will return an existing user. But that user never provided their bloodType. To fix that, we need to keep track of the newUser flag. The flag is turned off only when user finishes the sign up and provide bloodType. Which is in the PATCH /user route.

This is one of the two issues I promised we'd fix in the previous episode.

Request Email

The second issue to fix is the email. If you do not need an email in your application this should be fine. But if you, like the rest of us, would like to annoy your customers with endless propaganda emails, then you need the email. First we addScope:

 // serivces/auth.service LoginGoogle(): Observable<IAuthInfo> {   const provider = new GoogleAuthProvider();   provider.addScope('email');   // ... } 
Enter fullscreen mode Exit fullscreen mode

The returned token does not have the email. Nice going Firebase! The extra piece of information is found in two places,

  • userCredential.user.providerData[]. An array of well-formed data (UserInfo[])
  • AdditionalUserInfo.profile. The format depends on the provider, needs to be requested via getAdditionalUserInfo but also has a very important piece of information: isNewUser. But we can do without it now.

For the particular case of Google sign in, we'll rely on the first method. But you might want to investigate further for X (Twitter) provider, and no, not Facebook, we're boycotting Facebook.

The change affects three places: LoginGoogle, LoginUser, and the route /auth/login:

// services/auth.service // catch email in google, and send it to API LoginGoogle(): Observable<IAuthInfo> {  //...    return defer(res).pipe(       switchMap(auth => auth.user.getIdToken()),             // login with provider email       switchMap(token => this.LoginUser(token, this.auth.currentUser.providerData[0].email))   ); } // catch email and send to API LoginUser(token: string, email?: string): Observable<IAuthInfo> {     return this.http.post('/auth/login', { token, email }); } 
Enter fullscreen mode Exit fullscreen mode

On the server:

// in server/routes.js router.post('/auth/login', function (req, res) {    const payload = req.body;   const token = payload.token;    sdk.auth().verifyIdToken(token).then(function (decodedToken) {     //...     if (!profile) {       // ...       // read email from payload instead of decodedToken       const _newuser = {         email: payload.email         // ...       }        // ...       res.json(_newuser);     }     //...   }) }); 
Enter fullscreen mode Exit fullscreen mode

Which means, we need to send the email with Sign in with Password. Let's also do the promised enhancement of combining sign in and sign up with password.

Combine sign in and sign up

For best user experience, we should allow user to sign in first, and if the user is new, request additional information for sign up. This matches the sequence of events handled with Google login, or any third party login.

In our component we need just one login call, that switches to sign up if the user is new.

// components/public/login.component // change to login to allow sign up login() {   this.authService     .Login('email@address.com', 'valid_firebase_password')     .pipe(catchError...)       .subscribe({       next: (user) => {         // read newUser from our API         if (!user.newUser) {           this.router.navigateByUrl('/private/dashboard');         } else {           // show the sign up field           this.showMe = true;         }       },    }); } 
Enter fullscreen mode Exit fullscreen mode

This looks exactly like the loginGoogle method. We are on the right track.

In our AuthService, the Login needs to change a bit, it will catch the invalid credentials error, to switch to SignUp.

// services/auth.service // Login first Login(email: string, password: string): Observable<IAuthInfo> {   const res = () => signInWithEmailAndPassword(this.auth, email, password);   return defer(res).pipe(     switchMap(auth => auth.user.getIdToken()),     // send email     switchMap(token => this.LoginUser(token, email)),     catchError(err => {       // catch invalid credentials to sign up       if (err.code === 'auth/invalid-credential') {         return this.SingUp(email, password);       }       // throw everything else       return throwError(() => err);     })   ); } // remove custom attributes SingUp(email: string, password: string): Observable<IAuthInfo> {   const res = () =>     createUserWithEmailAndPassword(this.auth, email, password);   return defer(res).pipe(     switchMap(auth => auth.user.getIdToken()),     // stop here and return, send email as well     switchMap(token => this.LoginUser(token, email)),   ); } 
Enter fullscreen mode Exit fullscreen mode

Now the update and UpdateUser take care of patching the bloodType, just as in the Google sign in sequence. Great. Now what?

Authentication header

In order for the PATCH /user call to work, we need to pass a fresh token into the header. The following is the authentication middleware, nothing fancy, we just get the user from data source first, match to the uid returned by Firebase token.

// server/auth.middleware.js // update to find profile by uid first sdk.auth().verifyIdToken(authheader).then(function (decodedToken) {   // example:   let profile = profiles.find((profile) => profile.id === decodedToken.uid);   // if found, set, else nullify   if (profile){     res.locals.user = profile;   } else {     res.locals.user = null;   }   // next   next(); }) // ... 
Enter fullscreen mode Exit fullscreen mode

Our http interceptor is the same we created in the last episode. It too will have a 401 error catch, that will request a fresh token from Firebase. Let's dig in the AuthState and user model to see how we can save the token.

Maintaining state

The end result we want for the user state is to be able to display the information based on a state item (like the one we developed in our Angular Authentication series).

// components/some component // template `<div *ngIf="status$ | async as s">   {{ s.email }} {{ s.bloodType }} </div>`  status$: Observable<IAuthInfo>; constructor(private authState: AuthState) {} ngOnInit(): void {   // watch auth state item   this.status$ = this.authState.stateItem$; } 
Enter fullscreen mode Exit fullscreen mode

Here are the ingredients:

  • An IAuthInfo to model our local user model
  • Update state after calling LoginUser and UpdateUser in AuthService
  • An AuthState service to keep track of user state
  • An AuthGuard to protect private route
  • Initiate the token property directly from Firebase

Auth user model

The IAuthInfo has the basic attributes, and a public method to map our user

// services/auth.model export interface IAuthInfo {   id: string;   bloodType?: string;   admin?: boolean;   // some properties from firebase   picture?: string;   email?: string;   // a place for the token   token?: string;   // a boolean for newUser   newUser?: boolean; }  export const MapAuth = (auth: any): IAuthInfo => {   // map incoming from db with our user   // this isn't required, but preferable   // token is not mapped here   return {     id: auth.id,     email: auth.email,     admin: auth.admin,     bloodType: auth.bloodType,     picture: auth.picture,     newUser: auth.newUser   } } 
Enter fullscreen mode Exit fullscreen mode

The token is not mapped directly from API, it will be added from Firebase.

Update service

After every visit to the API, we need to update the state, preferably by mapping to our local model, passing the fresh token coming back.

// services/auth.service // update state after API calls LoginUser(token: string, email?: any): Observable<IAuthInfo> {   return this.http.post('/auth/login', { token, email }).pipe(     map((auth: any) => {       // map and save user in localstorage here, including token       const _user = MapAuth(auth);       this.authState.UpdateState({ ..._user, token });       return _user;     }),   ); } UpdateUser(custom: any): Observable<IAuthInfo> {   return this.http.patch('/user', custom).pipe(     map(auth => {       // now update localstorage again       const _user = MapAuth(auth);       this.authState.UpdateState({..._user});       return _user;       })   ); } 
Enter fullscreen mode Exit fullscreen mode

AuthState service

The main elements of our AuthState besides the constructor is a state item BehaviorSubject and a proper UpdateState method that updates the Subject and saves into local storage. It should inject the AngularFire Auth service as well. We should add the GetToken and RefreshToken to use in our http interceptor. Here it is 🔻

@Injectable({ providedIn: 'root' }) export class AuthState {   // create an internal subject and an observable to keep track   private stateItem: BehaviorSubject<IAuthInfo | null> = new BehaviorSubject(null);   stateItem$: Observable<IAuthInfo | null> = this.stateItem.asObservable();   constructor(     // inject from '@angular/fire/auth'     private auth: Auth   )   {     // TODO: initiate state   }    UpdateState(item: Partial<IAuthInfo>): Observable<IAuthInfo> {     // update existing state     const newItem = { ...this.stateItem.getValue(), ...item };     this.stateItem.next(newItem);     // save into a key in localStorage     localStorage.setItem('user', JSON.stringify(newItem));     // return observable     return this.stateItem$;   }    GetToken() {     // return token as is     const _auth = this.stateItem.getValue();     return _auth?.token || null;   }   RefreshToken() {     // refresh by calling getIdToken with `true`     return defer(() =>        this.auth.currentUser.getIdToken(true)).pipe(       switchMap(token => {         // update state then return an observable to pipe to         return this.UpdateState({ token });       })     );   } } 
Enter fullscreen mode Exit fullscreen mode

Read more about a localStorage Angular wrapper, and RxJS based state management to have a fuller solution.

Auth route guard

The guard now reads directly from our state item.

// services/auth.guard  export const AuthCanActivate: CanActivateFn = (...): Observable<boolean> => {   // inject our auth state   const auth = inject(AuthState);   const router = inject(Router);    const role = route.data.role;    // watch user   return auth.stateItem$.pipe(     map(_user => {      // if user exists let them in, else redirect to login       if (!_user) {         router.navigateByUrl('/public/login');         return false;       }        // user exists, match property to route data       if (!_user.hasOwnProperty(role)) {         router.navigateByUrl('/public/login');         return false;       }       return true;     })   ); }; 
Enter fullscreen mode Exit fullscreen mode

Initiate the token

When the application is launched, we can initiate the state from the local storage, through the same APP_INITIALIZER factory we already have. The constructor looks like this

// services/auth.state  constructor(...) {   // initialize state directly from localStorage     const _localuser: IAuthInfo = JSON.parse(localStorage.getItem('user'));   if (_localuser) {     this.UpdateState(_localuser);   } } 
Enter fullscreen mode Exit fullscreen mode

The most extreme case is when user lands on a protected route, and that user already has a saved state in local storage. The synchronous way of setting the initial state means the Auth Guard will have something to use. But the token used, is not only stale, but expired. What does that mean?

The curious case of an expired token

Should we expel user? Given the fact that Firebase token is designed around the idea of refreshing itself in an hour, we should not worry about it. Also remember, the route guard is cosmetic, the real security is on API. The next API call will throw a 401, which will initiate a refresh sequence.

We can also make sure that 401 doesn't happen as often, if we update the token on initialization at least once:

// services/auth.state  constructor(...) {   // .. update localStorage first   // take just 1, this will only get a new one if it's expired   idToken(this.auth).pipe(     take(1),   ).subscribe({     next: (token) => {       if (token) {         this.UpdateState({ token });       }     }   }); } 
Enter fullscreen mode Exit fullscreen mode

If changes occur off the system, like user changes their password, we simply have to wait till it expires. Or again, we can guard important actions with our own “forced” token. For example, to request a list of codes only after user is logged in, we can do this

// example of a tighter security call GetSafeCodes(): Observable<something> {   // first get token with force flag   return defer(() => this.auth.currentUser.getIdToken(true)).pipe(     switchMap(token => {       // then call API if token exists       return this.GetCodes();     }),     catchError(...)   ); } 
Enter fullscreen mode Exit fullscreen mode

Logout

Last bit to add is the logout, it is exactly like the one we had last episode, with the addition of cleaning localStorage in AuthState

// services/auth.state Logout() {   this.stateItem.next(null);   localStorage.removeItem('user'); } 
Enter fullscreen mode Exit fullscreen mode

Conclusion

So this is it. Exposed and laid down. Here are some extra points to mention

  • Firebase Auth is a front layer that hides a lot of operations that take care of authentication, especially with third party
  • It is mostly asynchronous calls, returns Promises. This is a bit annoying, as you lose information down the pipe of multiple promises.
  • Use the modular SDK, for tree-shaking
  • The documentation will make you lose few pounds, or a few years of your life expectancy, if you are working with web, you have go through only these:
    • Authentication for Web (And reference of auth JS functions)
    • Admin SDK (with NodeJS) (And reference of Nodejs functions)
  • The decoded token of Firebase in Admin SDK has a different model than the one returned after user sign-in on client-side. For example, picture, is for photoUrl. There is no displayName in Admin SDK. There is a name property, but it is not documented!

In summary:

  • We created a NodeJs Express server to handle Firebase verification
  • We created a service that handles sign in, and sign up, with Password and Google, that have the same sequence
  • We saved information in our local storage
  • We hunted down the Firebase token, refreshed it when needed, and relaxed when it was okay, relied on 401 when it made sense
  • We looked into two different implementations, one is more recommended than the other, and it is my personal go for

Happy 2024. Are we there yet?

Thanks for reading this far, if you have comments and questions, let me know. 🔻

RESOURCES

  • Firebase Docs
  • Firebase Admin SDK
  • Project on GitHub

RELATED POSTS

Authentication in Angular, why it is so hard to wrap your head around it – Sekrab Garage

The basic ingredients. Authentication ingredientsA full cycle of authentication and authorization in an SPA may go through the following factors:<ul><li>User login: getting an access token, and possibly a refresh token.</li…. Posted in Angular

favicon garage.sekrab.com

LocalStorage wrapper service in Angular – Sekrab Garage

Storage and Caching. Browser Storage is accessible in browser platform only, and it is just one way to cache in browser. Thus we should avoid accessing it directly in code, wrapping it with internal service allows us to…. Posted in Angular, Design

favicon garage.sekrab.com

RxJS based state management in Angular – Part I – Sekrab Garage

Angular state management simple solution. Google it, Angular State Management, odds are, you will end up on an ngRx solution. Referring to this greate article Choosing the State Management Approach in Angular App , I am here to explore and i…. Posted in Angular, Design

favicon garage.sekrab.com

angularfirebasetutorialwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers
  • Author

    Refactoring for Efficiency: Tackling Performance Issues in Data-Heavy Pages

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.