Secure Ionic 4 API with oAuth

DAY 4 - July 2019

In our previous tutorial we learned how to secure an API in our Django project. In this tutorial, we will see how to call our secure API from our Ionic 4 project.

Get an oAuth2 at Ionic 4 application launches

Each time our ionic application is launched, first thing we need to do is to check if we have an oAuth2 token and if not create one.

import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import {ApiDjangoService} from '../app/services/api-django.service'
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public apiService : ApiDjangoService
  ) {
    this.initializeApp();
  }
  accessAuthorizedWithUrl(){
  }
  getToken(){
  } 
  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      // check if we have a token to use API
      this.apiService.checkOauthToken().then((result) => {
        if (result){
          console.log("Deja token "+result);
          this.accessAuthorizedWithUrl()
        }
        else { 
          console.log("PAS DE TOKEN TOKEN")
          this.getToken()
        }
      });
    });
  }
} 

  • First we import our ApiDjangoService and inject it in our AppComponent constructor
  • Then once the platform is ready, we are going to call a apiService.checkOauthToken() to check if we have a token or not
  • If we have a token, it's perfect we can continue and go to accessAuthorizedWithUrl function.
  • If we don't have a token, we are going to ask one
Since we want to save our token in our application storage, we will need the Ionic Storage library, so to install it:

ionic cordova plugin add cordova-sqlite-storage
npm install --save @ionic/storage 

Now that the Storage package is installed, we need to add it to our Ionic 4 AppModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {ApiDjangoService} from './services/api-django.service'
import { HttpClientModule } from '@angular/common/http';
import { IonicStorageModule } from '@ionic/storage';
@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule,
     IonicModule.forRoot(), 
     IonicStorageModule.forRoot(),
     HttpClientModule,
     AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    ApiDjangoService,
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Then we modify our AppComponent class to include it.

import { Component } from '@angular/core';
import { Storage } from '@ionic/storage';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ApiDjangoService } from '../app/services/api-django.service'
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public apiService: ApiDjangoService,
    public storage : Storage
  ) {
    this.initializeApp();
  }
  accessAuthorizedWithUrl() {
  }
  getToken() {
    console.log("Pas deja token ")
    this.apiService.checkOauthToken().then((token) => {
      if (token) {
        console.log("Retour WS token "+token+" on sauve token en local "+token)
        this.accessAuthorizedWithUrl()
      }
      else {
        // Get token
        this.apiService.getOAuthToken().then((token) => {
          if (token) {
            console.log("Retour WS token "+token+" on sauve token en local "+token)
            this.accessAuthorizedWithUrl()
          }
          else {
              console.log("ERREUR RETOUR TOKEN"); 
          }
        });
      }
    });
  }
  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      // check if we have a token to use API
      this.apiService.checkOauthToken().then((result) => {
        if (result) {
          console.log("ACCESS DIRECT  Deja token " + result);
          this.accessAuthorizedWithUrl()
        }
        else {
          console.log("ACCESS DIRECT PAS DE TOKEN TOKEN")
          this.getToken()
        }
      });
    });
  }
}

Now the getToken() method has been modified, and ask our apiService to check if we have a token or not. If not, we try to generate a new token calling this.apiService.getOAuthToken(). If not token is return it means our application will not be able to work properly since we will not be able to call our django protected API.

Update our Api service provider to implement the oAuth2 token verification

In our apiSevice we will implement the method getOAuthToken() which will call our django backend.

getOAuthToken() {
    let url = this.getOauthUrl
    console.log("pas de token appel WS url avec nouveau headers " + url);
    let body = 'client_id=' + this.oAuth2ClientId + '&client_secret=' + this.oAuth2ClientSecret + '&username=' + this.oAuth2Username + '&password=' + this.oAuth2Password + '&grant_type=password';
    //console.log("body "+body);
    const httpOptions = {
      headers: new HttpHeaders({
        'content-type': "application/x-www-form-urlencoded",
      })
    };
    return new Promise(resolve => {
      this.http.post(url, body, httpOptions)
        .pipe(
          retry(1)
        )
        .subscribe(res => {
          let token = res["access_token"];
          this.tokenSSO = token
          console.log("ok TOKEN " + token);
          let expireIn = res["expires_in"]; // Secondes
          this.expireDate = (Date.now() / 1000) + expireIn;
          // Save expireDate
          this.storage.set(this.appName + '_expireAccessToken', this.expireDate);
          this.storage.set(this.appName + '_accessToken', this.tokenSSO).then((result) => {
              resolve(token)
          })
        }, error => {
          console.log("ERREUR APPEL TOKEN ");// Error getting the data
          console.log(error)
          resolve()
        });
    });
  }

  • We construct the url to call by adding to it the paramaters required to get be authentified.
  • Then we call our Django oAuth2 webservice and get the results
  • We extract the received token from the response and save it to local storage
  • If you remember, a token can be limited in time and expire, so we get the expiry time of the token, and add it to the current date, to know the date of the expiration. Then we save the results in the local storage

Now we can implement the method checkOauthToken, which will look up into the local storage if we have a token or not.

checkOauthToken() {
    console.log("on check OAUTH TOKEN");
    return new Promise(resolve => {
      this.storage.get(this.appName + '_accessToken').then((result) => {
        console.log("OK CHECK TOKEN " + result);
        if (result) {
          this.tokenSSO = result
          // Set expire date
          let expireFS = this.getExpireDate().subscribe((date) => {
            // check date expire
            expireFS.unsubscribe()
            let now = Date.now() / 1000;
            console.log("on compare " + this.expireDate + " avec " + now);
            if (Number(this.expireDate) < now) {
              console.log("date expirée, on va chercher nouveau token")
              resolve()
            }
            else {
              resolve(result)
            }
          })
        }
        else {
          resolve()
        }
      });
    });
  } 

This method checks if we a token in our storage, and if so, checks if the expiration date is reached or not. If everything is ok, we can return resolve(true) to our promises, otherwise we return no value resolve().
Here is the full code of our ApiDjangoProvider:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Storage } from '@ionic/storage';
import { Observable } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
@Injectable({
  providedIn: 'root'
})
export class ApiDjangoService {
  tokenSSO: String = "";
  expireDate: any;
  networkConnected: boolean = true;
  virtualHostName: string = ''
  oAuth2ClientId: string = 'VbXH6Jwp7FSMqgL9fuXn3Qo6dI57BEYH1FGEMQPF';
  oAuth2ClientSecret: string = 'Vur9PaOf39LaWEWxIAitTUrvbQOCDegCUpwCPCmKqcbZDz5FhbTKfYeI0IUBFvY4g3hxf4CctEWzw2KJrOfEsAVTDQ2z4LPImoRoGxyzZMmulswIwNLJHRIK8JIfTD93';
  oAuth2Username: string = 'csurbier@idevotion.fr'; // Set your oAuth2 username
  oAuth2Password: string = ''; // Set your oAuth2 password
  appName: string = 'bikeapplication';
  apiPrefix = "/api"
  getOauthUrl = "https://app-b86ed9a3-cfa5-4d79-a572-890b5a4e545f.cleverapps.io/o/token/";
  constructor(public http: HttpClient,
    public storage: Storage) {
  }
  getExpireDate() {
    return Observable.create(observer => {
      this.storage.get(this.appName + '_expireAccessToken').then((date) => {
        if (date) {
          this.expireDate = date;
          console.log("on met en mémoire dateExpire " + date);
          observer.next(this.expireDate);
          observer.complete();
        }
        else {
          observer.next();
          observer.complete();
        }
      })
        .catch(error => {
          observer.next();
          observer.complete();
        })
    });
  }
  checkOauthToken() {
    console.log("on check OAUTH TOKEN");
    return new Promise(resolve => {
      this.storage.get(this.appName + '_accessToken').then((result) => {
        console.log("OK CHECK TOKEN " + result);
        if (result) {
          this.tokenSSO = result
          // Set expire date
          let expireFS = this.getExpireDate().subscribe((date) => {
            // check date expire
            expireFS.unsubscribe()
            let now = Date.now() / 1000;
            console.log("on compare " + this.expireDate + " avec " + now);
            if (Number(this.expireDate) < now) {
              console.log("date expirée, on va chercher nouveau token")
              resolve()
            }
            else {
              resolve(result)
            }
          })
        }
        else {
          resolve()
        }
      });
    });
  }
  getOAuthToken() {
    let url = this.getOauthUrl
    console.log("pas de token appel WS url avec nouveau headers " + url);
    let body = 'client_id=' + this.oAuth2ClientId + '&client_secret=' + this.oAuth2ClientSecret + '&username=' + this.oAuth2Username + '&password=' + this.oAuth2Password + '&grant_type=password';
    //console.log("body "+body);
    const httpOptions = {
      headers: new HttpHeaders({
        'content-type': "application/x-www-form-urlencoded",
      })
    };
    return new Promise(resolve => {
      this.http.post(url, body, httpOptions)
        .pipe(
          retry(1)
        )
        .subscribe(res => {
          let token = res["access_token"];
          this.tokenSSO = token
          console.log("ok TOKEN " + token);
          let expireIn = res["expires_in"]; // Secondes
          this.expireDate = (Date.now() / 1000) + expireIn;
          // Save expireDate
          this.storage.set(this.appName + '_expireAccessToken', this.expireDate);
          this.storage.set(this.appName + '_accessToken', this.tokenSSO).then((result) => {
              resolve(token)
          })
        }, error => {
          console.log("ERREUR APPEL TOKEN ");// Error getting the data
          console.log(error)
          resolve()
        });
    });
  }
  doSomeRequest() {
    this.http.get('https:///posts').subscribe((response) => {
      console.log(response);
    });
  }
} 

Ok we have almost done. Now we will try to call our Django API to get the list of users and check that everything is ok.