Sometimes in your project you want to allow your users to log-in through URL. So it can access private features only click in one link (mostly sent by email). What we are doing is use Firebase Custom Auth to do this.

Cloud Functions for Firebase

Steps

  • Create a HTTP Firebase CloudFunction (CF) who receives a user ID, generate a token using the Firebase Admin SDK and returns it.
  • Create a Page to call CF and Generate the Token.
  • Create a Page only accessible if a valid Token is passed on the URL.

Init

Create the project

$ npm install ionic typescript -g
$ ionic start login-flow blank --type=angular
$ cd login-flow

Cloud Function

Requirements

  1. Create a project on https://console.firebase.google.com/
  2. Install firebase tools $ npm install -g firebase-tools
  3. Login $ firebase login
  4. Init the functions and select TypeScript $ firebase init functions
  5. Enable IAM API How To
  6. Set required permissions to Service Account How To

Code

Create functions/src/auth.ts

export class AuthService {
  admin;
  constructor(admin) {
    this.admin = admin;
  }
  public createToken(uid: string) {
    return this.admin.auth().createCustomToken(uid);
  }
}

Edit functions/src/index.ts

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { AuthService } from './auth';
admin.initializeApp();
export const createToken = functions.https.onRequest((request, response) => {
  response.set('Access-Control-Allow-Origin', '*');
  const authService: AuthService = new AuthService(admin);
  authService.createToken(request.query['uid'])
  .then(
    data => {
      response.send(data);
    }
  ).catch(function(error) {
    console.log('Error creating custom token:', error);
  });
});

Deploy

$ firebase deploy --only functions
You will receive the URL to the CF

Test

Just call [URL]?uid=[anyuid]
I.E. https://us-central1-firestore-custom-auth-angular.cloudfunctions.net/createToken?uid=123

Troubleshooting

You should set the engine on functions/package.json

{
  ...
  "engines": {
    "node": "8"
  },
  ...
}

I’ve some errors on deploy, and so far the workaround for me is remove project node_modules before functions, I didn’t figure out why. $ rm -rf node_modules
Deploy CF and after reinstall the modules $ npm i

Ionic Angular Project

Requirements

  • Install AngularFire $ npm install firebase @angular/fire --save
  • Copy Firebase Config of Firebase Console. On project overview page, click Add Firebase to your web app.

    Create FILES

    $ ng g module core
    $ ng g service core/auth/auth
    $ ng g guard core/auth/auth
    $ ng g page login
    

CODE

The structure is based on Implementing Login flow with Ionic 4

UPDATE on /src/environments/environment.ts

export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};

ADD ON src/app/app.module.ts

...
import { CoreModule } from './core/core.module';
import { environment } from '../environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
...
@NgModule({
  ...
  imports: [
    ...
    AngularFireModule.initializeApp(environment.firebase),
    IonicModule.forRoot(),
    AngularFireAuthModule,
    ...
    CoreModule,
  ],
  ...
})
export class AppModule {}

Login

  • Get CF URL and set on Base URL, Set any UID and Generate Token.
  • Generate Token calls the CF using HTTPClient passing the UID and set Token.
  • To test if is valid Token just click Login with Token, it uses AuthService.
  • Login By URL concatenetes the token with the HOME url. And open it in another tab.

src/app/login/login.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>login</ion-title>
  </ion-toolbar>
</ion-header>
<ion-content padding>
  <ion-input type="text" placeholder="Base URL" [(ngModel)]="baseUrl"></ion-input>
  <ion-input type="text" placeholder="User Id" [(ngModel)]="uid"></ion-input>
  <ion-button expand="full" (click)="generateToken()">Generate Token</ion-button>
  <ion-input type="text" placeholder="Token" [(ngModel)]="token"></ion-input>
  <ion-button expand="full" (click)="login()" *ngIf="!(authState$ | async)">Login with Token</ion-button>
  <ion-button expand="full" color="secondary" (click)="logout()" *ngIf="(authState$ | async)">Logout</ion-button>
  <a target="_blank" rel="noopener" href="/home/">Login By URL</a>
</ion-content>

src/app/login/login.page.ts

import { AuthService } from './../core/auth/auth.service';
import { Observable } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
  selector: 'app-login',
  templateUrl: './login.page.html',
  styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
  authState$: Observable<any>;
  token: string;
  uid: string;
  baseUrl;
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient
  ) { }
  ngOnInit() {
    this.authState$ = this.authService.getAuthStateObserver();
  }
  generateToken() {
    const url = `${this.baseUrl}?uid=${this.uid}`;
    this.httpClient.get(url, {responseType: 'text'}).toPromise().then(
      data => {
        this.token = data;
      }
    );
  }
  login() {
   this.authService.login(this.token);
  }
  logout() {
    this.authService.logout();
  }
}

Auth Service

Uses AugularFireAuth, calls signInWithCustomToken and return true if a valid token. src/app/core/auth/auth.service.ts

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(public afAuth: AngularFireAuth) {}
  public login(token: string) {
    return this.afAuth.auth.signInWithCustomToken(token).then(
      data => {
        return true;
      }
    );
  }
  public logout() {
    this.afAuth.auth.signOut();
  }
  public getAuthStateObserver() {
    return this.afAuth.authState;
  }
}

Auth Guard

Gets the token param of route, calls AuthService, and if true, allows the access. src/app/core/auth/auth.guard.ts

import { AuthService } from './auth.service';
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    private authService: AuthService
  ) {}

  async canActivate(route: ActivatedRouteSnapshot) {
    const token = route.params.token;
    return this.authService.login(token);
  }

}

Routes

Home route demands a token param and it only canActivate if AuthGuard (who get the token from route) allows. src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from './core/auth/auth.guard';
const routes: Routes = [
  { path: '', redirectTo: 'login', pathMatch: 'full' },
  {
    path: 'home/:token',
    canActivate: [AuthGuard],
    loadChildren: './home/home.module#HomePageModule' },
  { path: 'login', loadChildren: './login/login.module#LoginPageModule' },
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

So Home page is only accessible if a valid Token is passed on the URL.

:)

  • You can a Demo of this project here
  • The code is shared here

Daniel Antonio Conte

Life, Universe, Everything

Follow me