Configurando un Cliente Apache Cordova con Ionic e IdentityServer3

Introducción

Apache Cordova es una plataforma de desarrollo que permite construir aplicaciones híbridas para dispositivos móviles a través de CSS, HTML y JavaScript, mientras que IONIC es un framework utilizado sobre Apache Cordova que facilita el desarrollo de las aplicaciones haciendo uso de AngularJS y brindando al programador los elementos necesarios para construir  una aplicación con un front-end dinámico y escalable.

Objetivos

  • Describir los puntos esenciales que se han requerido para configurar el Servidor IdentityServer3.
  • Comentar la plantilla y plugin que se han usado para construir el aplicativo con Visual Studio 2015 RC.
  • Detallar los elementos esenciales al configurar una Aplicación Cliente en Apache Cordova a fin de que se autentique contra el Servidor IdentityServer3.

Requerimientos

  • Conocimientos intermedios de C# y AngularJS
  • Conocimientos básicos de desarrollo en aplicativos móviles.
  • Visual Studio 2015 RC

Esta aplicación se encuentra publicada en mi repositorio de GitHub en el siguiente enlace https://github.com/GinoLlerena/DemoAppCordovaIonicIdsv3

Configurando IdentityServer3

Con respecto a este proceso,haremos uso  de una configuración básica de IdentityServer3, se ha tomado como base el proyecto de ejemplo SelfHost (Minimal) que fue modificado en un artículo anterior,  y en esta oportunidad solo se van a comentar un par de cambios que se han tenido que llevar a cabo a fin de hacer viable el aplicativo actual, en caso de más detalles acerca de la Configuracion de Identity Server revisar el articulo Configurando un Cliente AngularJS con IdentityServer3.

Clase Startup

Con respecto a la Clase Startup, a través del método de extensión UseIdentityServer es que se lleva a cabo la configuración de IdentityServer v3, y mediante el uso de la instancia de la Clase IdentityServerOptions es que se suministran los parámetros de configuración, como ya se ha mencionado éste caso es una ligera modificación del presentado en el artículo anterior, por lo cual queda indicar que la línea resaltada en el siguiente código es lo que se ha agregado: RequireSsl = false, la razón de este cambio solo es por simplificar el proceso de desarrollo ya que que no se cuenta con un Certificado Original emitido por una real entidad Certificadora, sin embargo en ambientes de producción su uso es sumamente necesario.


 internal class Startup
 {
   public void Configuration(IAppBuilder appBuilder)
   {
      var factory = InMemoryFactory.Create(
      users: Users.Get(),
      clients: Clients.Get(),
      scopes: Scopes.Get());

      var options = new IdentityServerOptions
     {
       IssuerUri = "https://idsrv3.com",
       SiteName = "Thinktecture IdentityServer3 (self host)",

       SigningCertificate = Certificate.Get(),
       RequireSsl = false,
       Factory = factory,
       CorsPolicy = CorsPolicy.AllowAll,

    };

    appBuilder.UseIdentityServer(options);
   }
 }

Clase Program

La Clase Program, la cual se constituye en el punto de entrada de nuestra aplicación, en ella se ha modificado la definición de la ruta y puerto de escucha siendo establecida en la constante url definida en la línea 8 del código mostrado a continuación; la razón de este cambio implica dejar de usar el puerto seguro y el «*» agregado fuerza al servidor de autorización atender las peticiones a través de todos las interfaces disponibles.

 internal class Program
 {
   private static void Main(string[] args)
   {
      Console.Title = "IdentityServer3 SelfHost";
      LogProvider.SetCurrentLogProvider(new DiagnosticsTraceLogProvider());

      const string url = "http://*:44333/core";

      using (WebApp.Start<Startup>(url)) {
          Console.WriteLine("\n\nServer listening at {0}. Press enter to stop", url);
          Console.ReadLine();
      }
    }
 }

Con los cambios llevados a cabo ahora es momento de probar la operatividad de nuestro Servidor de Autorización haciendo un GET a la dirección http://localhost:44333/core/.well-known/openid-configuration a traves del Cliente Postman, tal como se puede apreciar a continuación éste muestra sus características, soporte y endpoints publicados.

IS-ApacheCordova

Tal como se aprecia, se ha resaltado un endpoint de los publicados, este será usado para retornar el profile del usuario registrado y así verificar la operatividad del aplicativo cliente.

Visual Studio 2015 RC y Apache Cordova

Entre las principales ventajas de Visual Studio 2015 RC destaca su capacidad para desarrollar aplicaciones híbridas para dispositivos móviles como es el caso de Visual Studio Tools for Apache Cordova, por lo cual se cuentan con plantillas para iniciar el desarrollo de un aplicativo movil con  HTML, CSS, y JavaScript, sin embargo para nuestro caso en particular necesitamos el uso de una plantilla que no viene pre-instalada, denominada Ionic Tabs Template.

Debemos iniciar Visual Studio 2015 y desde la opción del menú Herramientas, seleccionar Extensiones y actualizaciones, lo siguiente es buscar el template Ionic Tabs Template y proceder con su instalación.

ApacheCordova - Template

Ahora podemos crear un Nuevo Proyecto y seleccionar desde el grupo de plantillas de  Javascript, del grupo  Apache Cordova Apps, la referida a Ionic Tabs App.

ApacheCordova - Nuevo Proyecto

El aplicativo en proceso requiere el uso de un plugin que nos permitirá recibir los tokens provenientes desde el servidor de autorización, este proceso se lleva  a cabo instalando InAppBrowser, el cual nos permite lanzar el navegador desde el aplicativo, por lo tanto se requiere abrir el archivo config.xml, el cual nos muestra una plantilla de configuración, solo queda seleccionar el plugin mencionado y proceder con su instalación.

ApacheCordova - InAppBrowser

Finalmente solo queda mencionar que en el siguiente enlace de Codepen se puede encontrar una serie de ejemplos de como poder iniciar un proyecto en Ionic.

El Cliente Apache Cordova con Ionic

En esta parte el objetivo es describir el proceso de funcionamiento del cliente Apache Cordova con el uso de Ionic.

Describiendo la Aplicación en JavaScript

El aplicativo se define a través de la creación de un módulo; en nuestro caso lo denominamos «ionicApp»; dentro de un archivo del nombre app.js, y luego junto con su definición  se procede a inyectar todos aquellos módulos adicionales que serán requeridos por la aplicación, ya sea por sus sus servicios y/o controladores.

angular.module('ionicApp', ['ionic', 'ngStorage', 'ngCordova'])

A continuación el aplicativo cuenta con un método de inicialización, este garantiza que la plataforma Ionic se encuentre debidamente cargada una vez que el aplicativo es lanzado.

.run(function($ionicPlatform) {
     $ionicPlatform.ready(function() {
     // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
     // for form inputs)
     if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
        cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
     }
     if (window.StatusBar) {
        // org.apache.cordova.statusbar required
        StatusBar.styleLightContent();
     }
 });

Definiendo las rutas de navegación

El Cliente Apache Cordova con Ionic se encuentra basado  en AngularJS por lo cual requiere la definición de las rutas que hagan posible su navegación dentro del aplicativo, de tal manera que inyecte la vista requerida según la ruta solicitada, esto se lleva a cabo a través de un documento javascript al cual hemos denominado appRoute.js, el cual tiene la siguiente estructura:

</pre>
<pre>  'use strict';
  angular
        .module("ionicApp")
        .config(config) ;

  function config($stateProvider, $urlRouterProvider, $httpProvider) {

     $httpProvider.interceptors.push('authInterceptorService');

     $stateProvider
           .state('signin', {
                     url: '/sign-in',
                     templateUrl: 'templates/sign-in.html',
                     controller: 'SignInCtrl'
            })
           .state('tabs', {
                     url: '/tab',
                     abstract: true,
                     templateUrl: 'templates/tabs.html'
                  })
           .state('tabs.home', {
                     url: '/home',
                          views: {
                                  'home-tab': {
                                         templateUrl: 'templates/home.html',
                                         controller: 'HomeTabCtrl'
                                   }
                          }
            })
           .state('tabs.about', {
                   url: '/about',
                   views: {
                           'about-tab': {
                                  templateUrl: 'templates/about.html'
                            }
                    }
             });
            $urlRouterProvider.otherwise('/sign-in');
 };</pre>
<pre>

Es preciso señalar que una vez recibidos los tokens de autorización, se requiere que estos sean enviados cada vez que se realice una petición dentro de la solicitud a los recursos publicados, a fin de simplificar dicho proceso se hace uso de un Interceptor, el cual tiene por función insertar en el header el token requerido, esto se lleva a cabo a través de un servició, al cual denominamos authInterceptorService y será descrito más adelante sin embargo éste se encuentra registrado en el archivo anterior a través de la siguiente línea de código:

 $httpProvider.interceptors.push('authInterceptorService');

El proceso de login

El punto de inicio de navegación es a través de la página index.html, ésta en su estructura tiene una referencia a la aplicación Ionic que está basada en AngularJS definida con anterioridad,  la cual muestra como vista inicial sign-in.html que se encuentra definida dentro de las rutas de navegación y está asociado al controller denominado SignInCtrl, ubicado en el archivo signInCtrl.js.

El ámbito de la Aplicación, el cual es a través del uso de la directiva ng-app está definido en el tag body.

  <body ng-app="ionicApp">

Luego corresponde describir index.html, el cual en este caso las librerías javascript requeridas se encuentran referenciadas en el header y las vistas que son inyectadas se encuentran gestionadas a través de la siguiente directiva <ion-nav-view></ion-nav-view>.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title></title>

    <link href="http://code.ionicframework.com/1.0.0-rc.5/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">

    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
    <link href="css/ionic.app.css" rel="stylesheet">
    -->
    <!-- ionic/angularjs js -->
    <script src="http://code.ionicframework.com/1.0.0-rc.5/js/ionic.bundle.min.js"></script>

    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>

    <script src="js/jquery-2.1.0.js"></script>
    <script src="js/ng-cordova.min.js"></script>
    <script src="js/OAuthClient.js"></script>
    <script src="js/ngStorage.js"></script>
    <!-- your app's js -->
    <script src="js/App/app.js"></script>
    <script src="js/App/security/authInterceptorService.js"></script>
    <script src="js/App/appRoute.js"></script>
    <script src="js/App/security/authService.js"></script>
    <script src="js/App/controllers/homeTabCtrl.js"></script>
    <script src="js/App/controllers/signInCtrl.js"></script>
</head>
<body ng-app="ionicApp">
    <!--
      The nav bar that will be updated as we navigate between views.
    -->
    <ion-nav-bar class="bar-stable">
        <ion-nav-back-button>
        </ion-nav-back-button>
    </ion-nav-bar>
    <!--
      The views will be rendered in the <ion-nav-view> directive below
      Templates are in the /templates folder (but you could also
      have templates inline in this html file if you'd like).
    -->
    <ion-nav-view></ion-nav-view>
</body>
</html>

La vista utilizada para el proceso de logueo es sumamente sencilla, y tal como se va apreciando en el análisis del código la estructura de esta plataforma está basada en el uso de tabs, los cuales hacen de paginas que se navegan a través de multiples tabs.

<ion-view view-title="Sign-In">
   <ion-content>
      <div class="padding">
         <button class="button button-block button-positive" ng-click="signIn(user)">
            Sign-In
         </button>
      </div>
   </ion-content>
</ion-view>
Una vez definido nuestro sencillo layout de referencia tenemos la siguiente presentación:

ApacheCordova-01

 

El Controller Asociado a signin.html

Este controller se encuentra definido a través del documento javascript signInCtrl.jsy su finalidad es controlar los eventos relacionados al login de la aplicación.

La función signIn tiene por finalidad gestionar una ventana hija que permita cargar la pantalla de logueo que es enviada por el Servidor de Autorización, esto se hace a través del plugin instalado InAppBrowser, que posteriormente retornará el access token a través de la función authorize,  debo señalar que ha sido de mucha utilidad los siguientes enlaces que me han permitido conectar el aplicativo con el Servidor de Autorización y además ayudaran a entender el código que se ha tomado de ellos:

  'use strict';
    angular
        .module("ionicApp")
        .controller('SignInCtrl',SignInCtrl);

    SignInCtrl.$inject = ['$ionicPlatform', '$scope', '$rootScope', '$state', '$localStorage'];

    function SignInCtrl($ionicPlatform, $scope, $rootScope, $state, $localStorage) {

        var otherOptions = {
            location: 'no',
            clearcache: 'yes',
            toolbar: 'no'
        };

        var oauth = new OAuthClient('http://192.168.178.48:44333/core/connect/authorize');

        $scope.authorize = function (options) {
            var deferred = $.Deferred();
            var req = oauth.createImplicitFlowRequest(options.client_id, options.redirect_uri, options.scope, options.response_type);
            // Now we need to open a window.
            if (window.cordova) {
                var cordovaMetadata = cordova.require("cordova/plugin_list").metadata;
                if (cordovaMetadata.hasOwnProperty("cordova-plugin-inappbrowser") === true || cordovaMetadata.hasOwnProperty("org.apache.cordova.inappbrowser") === true) {
                    var browserRef = window.open(req.url, '_blank', 'location=yes');
                    browserRef.addEventListener("loadstart", function (e) {
                        var url = e.url;
                        if (url.indexOf(options.redirect_uri + '#') !== 0) return;
                        browserRef.close();
                        var error = /\#error=(.+)$/.exec(url);
                        if (error) {
                            deferred.reject({
                                error: error[1]
                            });
                        } else {
                            var uriFragment = url.substring(url.indexOf('#') + 1);
                            var result = oauth.parseResult(uriFragment);
                            // Mitigate against CSRF attacks by checking we actually sent this request
                            // We could also assert the nonce hasn't been re-used.
                            if (result.state == req.state) {
                                deferred.resolve(result)
                            }
                            else {
                                deferred.reject({
                                    error: "The state received from the server did not match the one we sent."
                                });
                            }
                        }
                    });
                    browserRef.addEventListener('exit', function (event) {
                        deferred.reject("The sign in flow was canceled");
                    });
                }
            }
            return deferred.promise();
        };

        $scope.signIn = function () {
            $ionicPlatform.ready(function () {
                $scope.authorize({
                    client_id: 'implicitclient',
                    redirect_uri: 'http://localhost:4400/index.html',
                    scope: 'openid profile email webapi',
                    response_type: 'id_token token'
                }).done(function (data) {
                    $localStorage.accessToken = data.access_token;
                    $state.go('tabs.home');
                    //alert('Access Token: ' + data.access_token);
                }).fail(function (data) {
                    alert(data.error);
                });
            });
        };

    };

En la siguiente imagen apreciamos la ventana de dialogo enviada por el Servidor de Autorización, la cual es una  ventana de navegación que es gestionada por el plugin insertado.

ApacheCordova-04

Una vez que se han suministrado las credenciales correspondientes, el Servidor de Autorización notifica la información de acceso que es solicitada por el aplicativo cliente tal como se aprecia en la siguiente imagen, en este caso hemos dejado a propósito el toolbar para notar la naturaleza de esta pantalla la cual es una ventana de navegación donde se aprecia el url de referencia:

ApacheCordova-02

Una vez obtenidos los permisos y recibido el token, el proceso automáticamente redirecciona hacia el tab definido en home.html, cuya finalidad es cargar automáticamente el profile del usuario, el cual es obtenido accediendo al enpoint /connect/userinfo

ApacheCordova-03

Acerca del Interceptor

Tal como se comentó al inicio, a fin de facilitar la transmisión de tokens en las peticiones efectuadas por el aplicativo cliente, es que se elabora un interceptor que inyecta en el header el token de acceso, como se puede apreciar a continuación:

angular
    .module('ionicApp')
    .factory('authInterceptorService', ['$q', '$location', '$localStorage', function ($q, $location, $localStorage) {

        var authInterceptorServiceFactory = {};

        var _request = function (config) {

            config.headers = config.headers || {};

            if ($localStorage.hasOwnProperty("accessToken") === true) {
                if ($localStorage.accessToken !== undefined) {
                    config.headers.Authorization = 'Bearer ' + $localStorage.accessToken;
                }
            }

            return config;
        }

        var _responseError = function (rejection) {
            if (rejection.status === 401) {
                $location.path('/');
            }
            return $q.reject(rejection);
        }

        authInterceptorServiceFactory.request = _request;
        authInterceptorServiceFactory.responseError = _responseError;

        return authInterceptorServiceFactory;
}]);

Servicio de Autorización

Finalmente describimos el servicio de autorización que se ha usado, éste cuenta con una función que es la encargada de hacer un GET al UserInfo Endpoint, el cual nos devolverá el profile del usuario, tal como se ha mostrado anteriormente.


    'use strict';
    angular
        .module("ionicApp")
        .factory('authService', authService);

    function authService($http, $q, $localStorage) {

        var authServiceFactory = {};
        authServiceFactory.data = [];

        var _getProfile = function (response) {
            return $http.get("http://192.168.178.48:44333/core/connect/userinfo")
           .success(function (data) {
               authServiceFactory.data = data;
           });

        }

        authServiceFactory.getProfile = _getProfile;

        return authServiceFactory;
    };

Comentarios Finales

  • En este artículo se ha buscado cubrir los aspectos esenciales de los mecanismos que se requieren para conectar una aplicación Apache Cordova con un servidor de Autorización.
  • Si bien se ha inyectado la librería ngCordova su principal utilidad está basada en la flexibilidad que brinda para hacer uso de los periféricos del dispositivo móvil.
  • El uso de la librería ngStorage tiene por finalidad permitirnos almacenar el token de acceso recibido, el cual es usado en cada una de las peticiones que se harán con posterioridad como por ejemplo el acceso a la información del profile.

Referencias

Un pensamiento en “Configurando un Cliente Apache Cordova con Ionic e IdentityServer3

  1. Pingback: Conectando Arduino UNO con Dispositivo Android mediante Apache Cordova a través de bluetooh, recepcionando mensajes y mostrandolos en una pantalla LCD | Gino Llerena

Deja un comentario