Vai al contenuto

Autenticazione SSO con Google

Questa pagina descrive il flusso di autenticazione in People FE v2 dal punto di vista sia dell’utente (pagina di login) sia tecnico (servizi Angular, interceptor, guard).

Panoramica

Layer Responsabilità
Google (GIS) Autenticazione utente, redirect con code OAuth
Frontend Angular Avvio login, cattura code, chiamata API login, storage token, guard e interceptor
Backend auth (Lambda) Scambio authCode → JWT (accessToken, refreshToken, ruolo, profilo)

Il punto centrale è AuthService (src/services/auth.service.ts). I file collegati direttamente o nel flusso sono elencati nella mappa dei file.

Esperienza utente: pagina di login

  • Alla prima visita, se l’utente non è autenticato, viene reindirizzato alla pagina di login.
  • La pagina di login (src/pages/login/) mostra il pulsante “Accedi con Google” esposto da GoogleLoginComponent.
  • Al clic sul pulsante, viene avviato il flusso SSO descritto sotto (OAuth 2.0 con redirect).
  • In caso di errore (403 o altri errori API), la pagina mostra messaggi coerenti con lo stato della richiesta.

Comportamento di base

  • Utente non loggato → pagina di login con pulsante Google.
  • Utente già loggato che apre /login → redirect automatico verso /dashboard.
  • Logout esplicito o sessione scaduta → redirect a /login.

Flusso end-to-end

sequenceDiagram
  participant U as Utente
  participant GL as GoogleLoginComponent
  participant AS as AuthService
  participant G as Google GIS
  participant M as main.ts
  participant LC as LoginComponent
  participant API as POST /v1/auth/login
  participant LS as localStorage

  U->>GL: Clic "Accedi con Google"
  GL->>AS: initializeGoogle()
  AS->>G: initCodeClient + requestCode()
  G->>U: Redirect Google SSO
  G->>M: Redirect a redirectUri?code=...
  M->>LS: setItem("authCode", code)
  M->>M: Rimuove ?code dall'URL
  LC->>AS: handleLoginResponse() (se authCode e non loggato)
  AS->>API: POST { authCode } + Basic Auth
  API-->>AS: Auth (token, role, name, ...)
  AS->>LS: Salva token e role
  AS->>AS: navigateToDashboard()

Fasi in dettaglio

1. Bootstrap e script Google

In src/index.html viene caricato lo script GIS:

<script src="https://accounts.google.com/gsi/client" async></script>

AuthService non usa providedIn: 'root': è registrato esplicitamente in app.config.ts come provider di applicazione per evitare errori del tipo google is not defined se il servizio venisse istanziato prima del caricamento dello script.

2. Avvio login (redirect OAuth)

GoogleLoginComponent espone il pulsante e chiama authService.initializeGoogle(), che configura il client OAuth Google:

Parametro Valore Fonte
client_id OAuth Client ID Google environment.idClient
scope openid email profile fisso in codice
ux_mode redirect redirect full-page (non popup)
redirect_uri URL app corrente environment.redirectUri

Poi invoca client.requestCode(): l’utente viene reindirizzato a Google e, dopo l’autenticazione, torna all’app con un query param code.

3. Cattura del codice OAuth (main.ts)

Il parametro code non è gestito dal router Angular. Al bootstrap, main.ts lo intercetta prima che il router possa scartarlo:

  1. Legge window.location.searchcode.
  2. Salva in localStorage con chiave "authCode".
  3. Rimuove code dall’URL con history.replaceState (evita di lasciare il codice visibile nella barra indirizzi).

4. Scambio codice → JWT

LoginComponent, in ngOnInit:

  • Se esiste authCode in localStorage e l’utente non è già loggato → authService.handleLoginResponse().

handleLoginResponse():

  1. Imposta loading a true.
  2. Chiama login(authCode)POST {apiUrl}/v1/auth/login con body { authCode }.
  3. Header Authorization: Basic {base64(environment.basicAuthorization)} (credenziali client backend, non il token utente).
  4. In caso di successo: saveDataInLocalStorage, aggiorna il signal userLogged, poi PoliciesService.load() (GET /v1/auth/policies).
  5. Se le policies non si caricano → logout() (niente dashboard senza ACL).
  6. Se ok → carica profilo corrente e redirect a /dashboard.
  7. In caso di errore login: error signal, rimuove authCode, mostra messaggi in UI (403 vs altri errori).

Al restore di sessione (token già in localStorage), AuthService decodifica il JWT e, in afterNextRender, richiama PoliciesService.load() e CurrentUserProfileService.load().

Endpoint e base URL:

  • Path: Endpoints.login/v1/auth/login.
  • Base URL: getApiUrl(SignGroupLambda.auth) → in dev usa environment.apiUrl.local, altrimenti environment.apiUrl.auth.

5. Persistenza sessione

Dopo login, saveDataInLocalStorage scrive in localStorage:

Chiave Contenuto
accessToken JWT access
accessTokenExpireIn Timestamp scadenza access
refreshToken JWT refresh
refreshTokenExpireIn Timestamp scadenza refresh

Il ruolo applicativo vive nel JWT (decodificato in tokenData / userLogged), non come chiave separata. Le policies ACL restano in memoria in PoliciesService (non in localStorage).

authCode viene rimosso dopo login riuscito o fallito.

6. Ripristino sessione al reload

AppComponent, in ngOnInit:

  • Se isLoggedIn() (presenza di accessToken o refreshToken in localStorage).
  • Decodifica l’access token con jwt-decode e popola userLogged (name, surname, email, role).

Richieste HTTP autenticate

authInterceptor (registrato in app.config.ts) gestisce tutte le chiamate tranne il login:

  1. Legge token e scadenze da localStorage via AuthService.
  2. Se l’access token non è scaduto → header Authorization: Bearer {accessToken}.
  3. Altrimenti, se il refresh token non è scaduto → Bearer {refreshToken}.
  4. Se entrambi scaduti → logout() e errore “Sessione scaduta”.
  5. Su risposta 401logout().
  6. Su risposta 403 → toast “Non sei autorizzato…” (senza logout automatico).

Il login usa Basic Auth sul client backend; l’interceptor non aggiunge Bearer su quella URL (Endpoints.login).

Protezione route e comportamento di navigazione

authGuard è applicato a /login e /dashboard (e figli):

Caso Comportamento
URL /login + utente già loggato Redirect a /dashboard
URL /login + non loggato Accesso consentito
Altre route protette + !isAuthorized() Redirect a /login
Altre route + autorizzato Accesso consentito

isAuthorized() verifica che il ruolo nel JWT decodificato sia uno dei valori di RoleEnum.

Le decisioni ACL oltre l'ingresso in dashboard usano PoliciesService (non check hard-coded sul ruolo). Dettaglio in Accesso e ruoli.

Guard Policy / criterio Effetto
policyChildGuard clients.show / buyers.show / buyerContracts.show Figlie procurement
requirePolicy("leaveRequests.show") leaveRequests.show Route permessi
teamDetailGuard canManageTeams() Dettaglio team

Metodi utili:

  • AuthService.isSelf(email) — confronto con utente loggato
  • AuthService.role — ruolo JWT solo display
  • PoliciesService.can(policy) / hasAny(...) / computed canManage*

Logout

logout():

  1. localStorage.clear() (token, eventuale authCode residuo).
  2. Reset dei signal error e userLogged, PoliciesService.reset(), clear profilo corrente.
  3. Navigazione a /login.

Viene invocato da:

  • authInterceptor (sessione scaduta / 401).
  • settings-items.ts (voce menu impostazioni).

Configurazione per ambiente

File in src/environments/:

Variabile Uso
idClient Google OAuth Client ID (diverso stage/prod)
redirectUri Deve coincidere con URI autorizzato in Google Cloud Console
basicAuthorization Credenziali Basic per POST /auth/login
apiUrl.local Backend locale in dev (http://localhost:3005)
apiUrl.auth API Gateway Lambda auth (stage/prod)
Ambiente File redirectUri tipico
Locale environment.ts http://localhost:4200
Stage environment.stage.ts https://people-stage.paradigma.me
Prod environment.prod.ts https://people.paradigma.me

isDevelopment() (src/utils/isDevelopment.ts) è true quando né stageproduction sono attivi → getApiUrl punta al backend locale.

Mappa dei file

src/
├── main.ts                          # Cattura ?code= → localStorage.authCode
├── index.html                       # Script Google GSI
├── app/
│   ├── app.config.ts                # AuthService + authInterceptor
│   ├── app.component.ts             # Ripristino userLogged da JWT
│   └── app.routes.ts                # authGuard su login e dashboard
├── services/
│   └── auth.service.ts              # Core SSO e sessione
├── interceptors/
│   └── auth.interceptor.ts          # Bearer token sulle API
├── guards/
│   ├── auth.guard.ts                # Login vs area protetta
│   ├── executive.guard.ts           # Route solo executive
│   ├── permissions.guard.ts         # Route permessi per ruolo
│   └── teams.guard.ts               # Dettaglio team per ruolo
├── models/
│   ├── auth.ts                      # Auth, Token, RoleEnum
│   └── jwt.ts                       # Payload JWT decodificato
├── pages/login/                     # Pagina login post-redirect
├── components/google-login/         # Pulsante Google SSO
├── utils/
│   ├── endpoints.ts                 # /v1/auth/login
│   ├── getApiUrl.ts                 # Base URL per ambiente
│   ├── isDevelopment.ts
│   ├── paths.ts                     # Path route login/dashboard
│   └── settings-items.ts            # Voce logout
└── environments/
    ├── environment.ts
    ├── environment.stage.ts
    └── environment.prod.ts

Note operative

Ordine di caricamento

  1. Script GIS (index.html).
  2. main.ts → cattura code.
  3. Bootstrap Angular → AuthService disponibile.
  4. Router → /login o route protetta.
  5. LoginComponent completa lo scambio se c’è authCode.

Sicurezza lato frontend

  • Il code OAuth ha vita breve e viene rimosso dall’URL subito dopo la cattura.
  • I token restano in localStorage (considerare implicazioni XSS in audit di sicurezza).
  • basicAuthorization nel bundle frontend è la credenziale del client verso l’API auth, non la password utente.

Debug tipico

Problema Cosa verificare
Redirect Google fallisce redirectUri in environment vs Google Console
Login API 403 Account non registrato/autorizzato lato backend
Loop login/dashboard role assente o non in RoleEnum; token scaduti
google is not defined Script GIS non caricato; ordine provider AuthService
API 401 su tutte le chiamate Scadenze token in localStorage; clock client

Nota architetturale

La soluzione implementata evita la gestione manuale delle password, delegando la sicurezza all'infrastruttura Google e riducendo la superficie di rischio. Il frontend si occupa solo di orchestrare redirect, scambio codice → token e gestione della sessione.