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 daGoogleLoginComponent. - 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:
- Legge
window.location.search→code. - Salva in
localStoragecon chiave"authCode". - Rimuove
codedall’URL conhistory.replaceState(evita di lasciare il codice visibile nella barra indirizzi).
4. Scambio codice → JWT
LoginComponent, in ngOnInit:
- Se esiste
authCodeinlocalStoragee l’utente non è già loggato →authService.handleLoginResponse().
handleLoginResponse():
- Imposta
loadingatrue. - Chiama
login(authCode)→POST {apiUrl}/v1/auth/logincon body{ authCode }. - Header
Authorization: Basic {base64(environment.basicAuthorization)}(credenziali client backend, non il token utente). - In caso di successo:
saveDataInLocalStorage, aggiorna il signaluserLogged, poiPoliciesService.load()(GET /v1/auth/policies). - Se le policies non si caricano →
logout()(niente dashboard senza ACL). - Se ok → carica profilo corrente e redirect a
/dashboard. - In caso di errore login:
errorsignal, rimuoveauthCode, 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 usaenvironment.apiUrl.local, altrimentienvironment.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 diaccessTokenorefreshTokeninlocalStorage). - Decodifica l’access token con
jwt-decodee popolauserLogged(name, surname, email, role).
Richieste HTTP autenticate
authInterceptor (registrato in app.config.ts) gestisce tutte le chiamate tranne il login:
- Legge token e scadenze da
localStorageviaAuthService. - Se l’access token non è scaduto → header
Authorization: Bearer {accessToken}. - Altrimenti, se il refresh token non è scaduto →
Bearer {refreshToken}. - Se entrambi scaduti →
logout()e errore “Sessione scaduta”. - Su risposta 401 →
logout(). - 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 loggatoAuthService.role— ruolo JWT solo displayPoliciesService.can(policy)/hasAny(...)/ computedcanManage*
Logout
logout():
localStorage.clear()(token, eventualeauthCoderesiduo).- Reset dei signal
erroreuserLogged,PoliciesService.reset(), clear profilo corrente. - 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é stage né production 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
- Script GIS (
index.html). main.ts→ catturacode.- Bootstrap Angular →
AuthServicedisponibile. - Router →
/logino route protetta. LoginComponentcompleta lo scambio se c’èauthCode.
Sicurezza lato frontend
- Il
codeOAuth ha vita breve e viene rimosso dall’URL subito dopo la cattura. - I token restano in
localStorage(considerare implicazioni XSS in audit di sicurezza). basicAuthorizationnel 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.