Google Auth

<< ngBootstrap Facebook Auth >>

En esta sección vamos a ver como integrar la autenticación de usuarios con Google en nuestra aplicación Angular. Aunque existen algunas librerías en NPM que hacen esto, suelen estar poco mantenidas, y por lo tanto serán incompatibles con la última versión de Angular, e incluso con la librería de Google si algo ha cambiado desde la última actualización.

Por ello, vamos a usar la librería de JavaScript que ofrece Google, pero crearemos una directiva para renderizar dentro de un elemento el botón de Google. Basícamente, convertiremos esta guía a Angular.

Lo primero que haremos será instalar los archivos de definición de tipos para TypeScript en nuestro proyecto:

npm i -D @types/google.accounts

Para importar estos tipos en el proyecto, tenemos que añadirlos en el array types del archivo tsconfig.app.json:

//...
"compilerOptions": {
  //...
  "types": ["google.accounts", ...],
},
//...

Crear credenciales OAuth2

Para poder usar la autenticación de Google, necesitamos crear credenciales para nuestra aplicación en la consola de desarrollo de Google Cloud.

Lo primero que haremos si aún no lo tenemos, será crear un nuevo proyecto:

Una vez creado y seleccionado el nuevo proyecto, desde el menú lateral selecciona APIs y serviciosPantalla de consentimiento de OAuth.

Aquí haremos una configuración inicial del proyecto. Por ahora la aplicación estará en modo desarrollo, por lo que no podremos usarla con cualquier usuario, sino que debemos dar de alta manualmente las cuentas que queramos que puedan hacer login. Debemos ademar, en la sección de Acceso a los Datos, agregar las opciones de consultar perfil y correo:

Después iremos a la sección Credenciales del menú lateral para crear una id de cliente de OAuth:

Por último copia el código generado como ID de cliente. Este código servirá para identificar tu aplicación.

Cargar la librería

Lo primero que haremos será crear un token de inyección para nuestra aplicación. En este caso servirá para poder registrar la ID de cliente que hemos generado para toda la aplicación desde el archivo de configuración app.config.ts. De esta manera, podríamos crear una librería con los componentes necesarios para el login con Google, y la ID de cliente proporcionarla desde este archivo sin necesidad de tocar código en ninguna otra parte.

Vamos a crear un archivo llamado google-login/google-login.config.ts:

import { InjectionToken, Provider } from '@angular/core';

export const CLIENT_ID = new InjectionToken<string>('client_id');

export function provideGoogleId(clientId: string): Provider {
  return { provide: CLIENT_ID, useValue: clientId };
}

Después, crearemos un servicio de Angular que se encargue de cargar la librería JavaScript. Esto nos ofrecerá varias ventajas:

  • El resto del código de la aplicación se abstrae de la librería de Google y utiliza los métodos del servicio para todo.
  • La librería no se carga con la aplicación, sino cuando se llame al método que la carga por primera vez (ej: cuando se muestra un botón de Google en la aplicación). Lo que se consideraría Lazy Loading.
  • Gestionaremos la carga de la librería con promesas, y los métodos del servicio serán de tipo async.
  • Vamos a usar un observable para que el resto de componentes de la aplicación puedan saber cuando un usuario ha hecho login en Google y reaccionar ante ello (subscripción).

También estamos exportando una función (provideGoogleId) necesaria para registrar el token en el array de providers de la aplicación. Debes llamar a la función pasándole la ID de cliente de Google.

bootstrapApplication(AppComponent, {
  providers: [
    // ...
    provideGoogleId('GOOGLE_ID.apps.googleusercontent.com')
  ],
});

A continuación creamos el servicio. Se encargará de cargar la librería con la API de Google. Comprobaremos también si la ID de cliente ha sido inyectada en la aplicaicón o mostraremos un error con información para el desarrollador. El servicio tendrá las siguientes características:

  • #loader → Esta promesa se crea en el constructor con la llamada a #loadApi. Sirve para controlar en los métodos que la api esté cargada antes de hacer nada más (y esperarla mientras se carga). Como los objetos de los servicios los crea Angular la primera vez que se necesitan, hasta que no mostremos algún botón de Google en la aplicación, no se ejecutará el constructor, y por tanto no se cargará la librería. Es de tipo promesa y no signal para poder esperarla con await.
  • #credential$ → Emite un objeto con las credenciales del usuario cuando este se ha logueado correctamente. Es del tipo Subject porque las credenciales de autenticaciones pasadas no son relevantes (además, pueden haber caducado si ha pasado suficiente tiempo). No es de tipo signal por lo explicado anteriormente.

ng g service google-login/load-google-api

import { Injectable, inject } from '@angular/core';
import { Subject, fromEvent, firstValueFrom } from 'rxjs';
import { CLIENT_ID } from './google-login.config';

@Injectable({
  providedIn: 'root',
})
export class LoadGoogleApiService {
  #loader: Promise<void>;
  #credential$ = new Subject<google.accounts.id.CredentialResponse>();
  #clientId = inject(CLIENT_ID, { optional: true });

  constructor() {
    if (this.#clientId === null) {
      // Error al desarrollador cuando no ha inyectado la id de Google
      throw new Error(
        'LoadGoogleApiService: You must call provideGoogleId in your providers array'
      );
    }
    this.#loader = this.#loadApi(); // Empezamos a cargar la librería
  }

  get credential$() {
    return this.#credential$.asObservable();
  }

  async setGoogleBtn(btn: HTMLElement) {
    await this.#loader; // Espera a que se haya terminado de cargar (si no lo ha hecho ya)
    google.accounts.id.renderButton(
      btn,
      { theme: 'filled_blue', size: 'large', type: 'standard' } // Diseño del botón
    );
  }

  async #loadApi(): Promise<void> {
    const script = document.createElement('script');
    script.src = 'https://accounts.google.com/gsi/client';
    script.async = true;
    document.body.appendChild(script);

    await firstValueFrom(fromEvent(script, 'load'));

    google.accounts.id.initialize({
      client_id: this.#clientId!,
      callback: (response) => {
        this.#credential$.next(response); // Se le llama cada vez que hay un login con Google
      },
    });
  }
}

En el método #loadApi, la función fromEvent crea un observable a partir de un evento, mientras que firstValueFrom convierte ese observable en una promesa (para poder usar await). En definitiva, lo que hay a continuación no se ejecutará hasta que no se haya cargado el script.

Crear directiva para el botón de login

Después crearemos una directiva con un selector de elemento en lugar de atributo. De esta manera crearemos un elemento <google-login> y dentro nos renderizará el botón de Google. 

ng g directive google-login/google-login

En esta directiva nos subscribimos al observable que nos devuelve las credenciales de Google cuando el usuario hace login. Es importante utilizar aquí la función takeUntilDestroyed, ya que el observable nunca termina y la subscripción no se cancelaría automáticamente. Utilizaremos un parámetro de salida (output) para enviar las credenciales al componente que tenga la directiva en su plantilla.

@Directive({
  selector: 'google-login',
})
export class GoogleLoginDirective {
  #element = inject(ElementRef);
  platformId = inject(PLATFORM_ID);
  // Solo inyectamos el servicio y cargamos la librería si estamos en el cliente (SSR)
  #loadService = isPlatformBrowser(this.platformId) ? inject(LoadGoogleApiService) : null;
  login = output<google.accounts.id.CredentialResponse>();

  constructor() {
    // Nos aseguramos que no se ejecuta en el lado del servidor si tenemos SSR activado
    afterNextRender(() =>
      this.#loadService?.setGoogleBtn(this.#element.nativeElement)
    );
    this.#loadService?.credential$
      .pipe(takeUntilDestroyed())
      .subscribe((resp) => this.login.emit(resp));
  }
}

Como se puede observar, solo inyectamos el servicio cuando la página se renderiza en el cliente, por si acaso tenemos SSR activado en la aplicación, ya que no queremos que se cargue la librería (constructor del servicio) de Google cuando se renderiza en el servidor.

Ya solo falta incluir la directiva en un componente y utilizar el botón. La clase btn es de Bootstrap y solo está puesta para limitar el tamaño del botón. En otro caso habría que hacerlo con CSS.

<!-- ... -->
<div>
  <google-login class="btn" (login)="loggedGoogle($event)"></google-login>
</div>
<!-- ... -->

<< ngBootstrap Facebook Auth >>