# Authentification Cognito pour webapps Angular Ce guide décrit comment intégrer l’authentification AWS Cognito (OIDC Code Flow avec PKCE) dans nos applications Angular. ## Pré‑requis côté Cognito - Un User Pool Cognito créé - Un App client « Public client » (sans secret) avec PKCE activé - Un domaine Cognito configuré (ex: `my-tenant.auth.eu-west-1.amazoncognito.com`) - Scopes OIDC: `openid`, `email`, `profile` (ajoutez d’autres scopes si besoin) - URLs de redirection et de logout autorisées - Dev (Angular local): - Callback: `http://localhost:4200/callback` - Logout: `http://localhost:4200` - Prod: adaptez avec votre domaine (ex: `https://app.example.com/callback` et `https://app.example.com`) ## Variables de configuration (Angular) Exemple d’entrée `environment.ts`: ```typescript export const environment = { production: false, cognito: { userPoolId: 'eu-west-1_XXXXXXXXX', userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx', domain: 'my-tenant.auth.eu-west-1.amazoncognito.com', redirectSignIn: 'http://localhost:4200/callback', redirectSignOut: 'http://localhost:4200', region: 'eu-west-1', scopes: ['openid', 'email', 'profile'] } }; ``` Mettez à jour `environment.prod.ts` avec les URLs de production. ## Option A — Hosted UI via AWS Amplify Installation: ```bash npm i aws-amplify @aws-amplify/ui-angular ``` Configuration (ex: `main.ts` ou `app.module.ts`): ```typescript import { Amplify } from 'aws-amplify'; import { environment } from './environments/environment'; Amplify.configure({ Auth: { region: environment.cognito.region, userPoolId: environment.cognito.userPoolId, userPoolWebClientId: environment.cognito.userPoolWebClientId, oauth: { domain: environment.cognito.domain, scope: environment.cognito.scopes, redirectSignIn: environment.cognito.redirectSignIn, redirectSignOut: environment.cognito.redirectSignOut, responseType: 'code' } } }); ``` Initiation du login (Hosted UI) et gestion du callback (ex: dans un service d’auth): ```typescript import { Auth } from 'aws-amplify'; export class AuthService { signIn(): Promise { return Auth.federatedSignIn(); // redirige vers Hosted UI } async handleCallback(): Promise { // Amplify gère automatiquement l’échange du code contre les tokens const session = await Auth.currentSession(); const idToken = session.getIdToken().getJwtToken(); const accessToken = session.getAccessToken().getJwtToken(); // Stockez/consommez les tokens selon vos besoins } signOut(): Promise { return Auth.signOut(); // redirige vers redirectSignOut } } ``` Routing du callback (ex: `app-routing.module.ts`): ```typescript { path: 'callback', component: CallbackComponent } ``` Dans `CallbackComponent`, appelez `handleCallback()` puis redirigez l’utilisateur vers l’app. ## Option B — angular-oauth2-oidc (OIDC Code Flow + PKCE) Installation: ```bash npm i angular-oauth2-oidc ``` Configuration (ex: `app.module.ts` et un `auth.config.ts`): ```typescript // auth.config.ts import { AuthConfig } from 'angular-oauth2-oidc'; export const authConfig: AuthConfig = { issuer: 'https://my-tenant.auth.eu-west-1.amazoncognito.com', clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx', responseType: 'code', redirectUri: 'http://localhost:4200/callback', postLogoutRedirectUri: 'http://localhost:4200', scope: 'openid email profile', showDebugInformation: false, strictDiscoveryDocumentValidation: false, // Cognito n’expose pas toujours toutes les métadonnées standard }; ``` Initialisation au démarrage (ex: `app.module.ts`): ```typescript import { OAuthService, OAuthModule } from 'angular-oauth2-oidc'; import { authConfig } from './auth.config'; providers: [ { provide: APP_INITIALIZER, multi: true, deps: [OAuthService], useFactory: (oauthService: OAuthService) => () => { oauthService.configure(authConfig); oauthService.setupAutomaticSilentRefresh(); return oauthService.loadDiscoveryDocumentAndLogin(); } } ] ``` Déclenchement du login/logout: ```typescript login(): void { this.oauthService.initLoginFlow(); } logout(): void { this.oauthService.logOut(); } getIdToken(): string | null { return this.oauthService.getIdToken(); } ``` Protection de routes (ex: guard minimal): ```typescript canActivate(): boolean { return this.oauthService.hasValidAccessToken() || this.oauthService.hasValidIdToken(); } ``` ## Bonnes pratiques - Utilisez le flow « Authorization Code + PKCE » pour les SPAs (sécurité accrue) - Déclarez toutes les URLs de callback/logout (local, staging, prod) dans Cognito - Synchronisez l’heure système (skew de plus de 5 min peut invalider les tokens) - Évitez d’exposer des secrets côté frontend (client public uniquement) - Renouvelez silencieusement les tokens lorsque possible (silent refresh) ## Dépannage - `invalid_redirect_uri` / `redirect_mismatch`: vérifiez les URLs exactes dans Cognito - Erreur « Not authorized » après login: facteurs possibles — scopes manquants, app client erroné - Boucle de redirection: callback non routé ou handler non exécuté - CORS/API: frontend authentifié ≠ API autorisée; configurez vos authorizers/API Gateway - Cookies SameSite: si vous utilisez des cookies, ajustez SameSite/Lax/None selon le contexte ## Checklist déploiement - Mettre à jour les URLs de callback/logout en prod - Activer HTTPS sur le domaine de l’app - Vérifier que le domaine Cognito est public et résout correctement - Tester login/logout et rafraîchissement de token en environnement prod ## Activation conditionnelle via variable d’environnement + Guard Angular Nous supportons un mode où l’authentification Cognito est optionnelle selon une variable d’environnement, et une Guard Angular assure la redirection vers une page de login configurable. ### Variables d’environnement Dans `environment.ts` et `environment.prod.ts`: ```typescript export const environment = { production: false, requireAuth: true, // si false, la Guard laisse passer sans authentification loginRoute: '/login', // route Angular interne servant le bouton/flux de login cognito: { userPoolId: 'eu-west-1_XXXXXXXXX', userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx', domain: 'my-tenant.auth.eu-west-1.amazoncognito.com', redirectSignIn: 'http://localhost:4200/callback', redirectSignOut: 'http://localhost:4200', region: 'eu-west-1', scopes: ['openid', 'email', 'profile'] } }; ``` Mettez `requireAuth: false` sur des environnements de démonstration internes si nécessaire. En production, laissez `true`. ### Utilisation des fichiers .env d’Angular Nous utilisons les fichiers `.env` d’Angular pour fournir ces variables au build. Créez les fichiers à la racine du projet Angular (même niveau que `package.json`) : - `.env` (valeurs par défaut communes) - `.env.development` (lues en dev) - `.env.production` (lues en prod) Exemple `.env.development` : ```bash NG_APP_REQUIRE_AUTH=true NG_APP_LOGIN_ROUTE=/login NG_APP_COGNITO_USER_POOL_ID=eu-west-1_XXXXXXXXX NG_APP_COGNITO_WEB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx NG_APP_COGNITO_DOMAIN=my-tenant.auth.eu-west-1.amazoncognito.com NG_APP_COGNITO_REDIRECT_SIGNIN=http://localhost:4200/callback NG_APP_COGNITO_REDIRECT_SIGNOUT=http://localhost:4200 NG_APP_COGNITO_REGION=eu-west-1 NG_APP_COGNITO_SCOPES=openid,email,profile ``` Exemple `.env.production` : ```bash NG_APP_REQUIRE_AUTH=true NG_APP_LOGIN_ROUTE=/login NG_APP_COGNITO_USER_POOL_ID=eu-west-1_YYYYYYYYY NG_APP_COGNITO_WEB_CLIENT_ID=yyyyyyyyyyyyyyyyyyyyyyyyyy NG_APP_COGNITO_DOMAIN=prod-tenant.auth.eu-west-1.amazoncognito.com NG_APP_COGNITO_REDIRECT_SIGNIN=https://app.example.com/callback NG_APP_COGNITO_REDIRECT_SIGNOUT=https://app.example.com NG_APP_COGNITO_REGION=eu-west-1 NG_APP_COGNITO_SCOPES=openid,email,profile ``` Conventions : - Préfixe recommandé: `NG_APP_` pour exposer la variable au code frontend - Les `.env.*` sont sélectionnés selon la configuration Angular (ex: `ng serve` utilise `development`, `ng build --configuration=production` utilise `production`) Lecture dans le code Angular (Angular 17+/Vite) : ```typescript const env = (import.meta as any).env as Record; export const environment = { production: env['MODE'] === 'production', requireAuth: String(env['NG_APP_REQUIRE_AUTH']).toLowerCase() === 'true', loginRoute: env['NG_APP_LOGIN_ROUTE'] || '/login', cognito: { userPoolId: env['NG_APP_COGNITO_USER_POOL_ID'], userPoolWebClientId: env['NG_APP_COGNITO_WEB_CLIENT_ID'], domain: env['NG_APP_COGNITO_DOMAIN'], redirectSignIn: env['NG_APP_COGNITO_REDIRECT_SIGNIN'], redirectSignOut: env['NG_APP_COGNITO_REDIRECT_SIGNOUT'], region: env['NG_APP_COGNITO_REGION'], scopes: (env['NG_APP_COGNITO_SCOPES'] || 'openid,email,profile').split(',') } }; ``` Note : selon la version de l’outillage Angular, l’accès peut être `import.meta.env.NG_APP_...` au lieu de l’indexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir l’exposition au build. ### Injection des `.env` via Docker / docker-compose Nos builds frontend sont statiques (servis par `serve`). Les variables doivent donc être connues au BUILD. Utilisez des `build.args` dans `docker-compose.yml` et, côté Dockerfile, écrivez un fichier `.env.production` avant le build Angular. Exemple `docker-compose.yml` (extrait service `frontend`) : ```yaml services: frontend: build: context: ./frontend dockerfile: ./frontend-dockerfile.yml args: NG_APP_REQUIRE_AUTH: "true" NG_APP_LOGIN_ROUTE: "/login" NG_APP_COGNITO_USER_POOL_ID: "eu-west-1_XXXXXXXXX" NG_APP_COGNITO_WEB_CLIENT_ID: "xxxxxxxxxxxxxxxxxxxxxxxxxx" NG_APP_COGNITO_DOMAIN: "my-tenant.auth.eu-west-1.amazoncognito.com" NG_APP_COGNITO_REDIRECT_SIGNIN: "https://app.example.com/callback" NG_APP_COGNITO_REDIRECT_SIGNOUT: "https://app.example.com" NG_APP_COGNITO_REGION: "eu-west-1" NG_APP_COGNITO_SCOPES: "openid,email,profile" ``` Adaptation optionnelle du `frontend-dockerfile.yml` (à ajouter avant `npm run build`) : ```dockerfile # Arguments build-time pour générer .env.production ARG NG_APP_REQUIRE_AUTH ARG NG_APP_LOGIN_ROUTE ARG NG_APP_COGNITO_USER_POOL_ID ARG NG_APP_COGNITO_WEB_CLIENT_ID ARG NG_APP_COGNITO_DOMAIN ARG NG_APP_COGNITO_REDIRECT_SIGNIN ARG NG_APP_COGNITO_REDIRECT_SIGNOUT ARG NG_APP_COGNITO_REGION ARG NG_APP_COGNITO_SCOPES # Écrire le fichier .env.production à partir des args RUN printf "NG_APP_REQUIRE_AUTH=%s\nNG_APP_LOGIN_ROUTE=%s\nNG_APP_COGNITO_USER_POOL_ID=%s\nNG_APP_COGNITO_WEB_CLIENT_ID=%s\nNG_APP_COGNITO_DOMAIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNIN=%s\nNG_APP_COGNITO_REDIRECT_SIGNOUT=%s\nNG_APP_COGNITO_REGION=%s\nNG_APP_COGNITO_SCOPES=%s\n" \ "$NG_APP_REQUIRE_AUTH" \ "$NG_APP_LOGIN_ROUTE" \ "$NG_APP_COGNITO_USER_POOL_ID" \ "$NG_APP_COGNITO_WEB_CLIENT_ID" \ "$NG_APP_COGNITO_DOMAIN" \ "$NG_APP_COGNITO_REDIRECT_SIGNIN" \ "$NG_APP_COGNITO_REDIRECT_SIGNOUT" \ "$NG_APP_COGNITO_REGION" \ "$NG_APP_COGNITO_SCOPES" \ > .env.production ``` Ainsi, le build Angular lit automatiquement ces variables via `import.meta.env` et les injecte dans le bundle. Remarques : - Les `environment.*.ts` peuvent mapper `import.meta.env` pour exposer une API typed au reste de l’app. - Pour des valeurs sensibles, préférez des mécanismes backend (ne jamais exposer de secrets côté SPA). - Si vous avez besoin de « runtime config » (changer après build), utilisez une approche `config.json` servie par le serveur et chargée au démarrage de l’app. ### Guard Angular (générique) Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si l’auth est requise mais absente/expirée, elle redirige vers `loginRoute`. ```typescript import { Injectable } from '@angular/core'; import { CanActivate, Router, UrlTree } from '@angular/router'; import { environment } from '../environments/environment'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private router: Router) {} async canActivate(): Promise { if (!environment.requireAuth) { return true; } // 1) Vérification via Amplify (si utilisé) try { const { Auth } = await import('aws-amplify'); const session = await Auth.currentSession(); const valid = !!session?.getIdToken()?.getJwtToken(); if (valid) return true; } catch (_) { // ignore si Amplify non configuré } // 2) Vérification via angular-oauth2-oidc (si utilisé) try { const { OAuthService } = await import('angular-oauth2-oidc'); const oauth = (window as any).ngInjector?.get?.(OAuthService); // Note: exposez éventuellement l'injector globalement au bootstrap if (oauth && (oauth.hasValidAccessToken() || oauth.hasValidIdToken())) { return true; } } catch (_) { // ignore si non installé } return this.router.parseUrl(environment.loginRoute); } } ``` Astuce: pour récupérer des services dans la Guard sans injection directe (quand on veut la rendre agnostique), on peut exposer l’`Injector` Angular globalement au bootstrap: `window.ngInjector = injector;` et l’utiliser comme ci‑dessus. ### Routage protégé Protégez les routes nécessitant une session valide: ```typescript { path: 'app', canActivate: [AuthGuard], loadChildren: () => import('./features/app/app.module').then(m => m.AppModule) } ``` ### Page de login (route `loginRoute`) La page associée à `environment.loginRoute` doit offrir un bouton « Se connecter ». Selon l’implémentation: - Avec Amplify: ```typescript import { Auth } from 'aws-amplify'; login(): Promise { return Auth.federatedSignIn(); } ``` - Avec angular-oauth2-oidc: ```typescript constructor(private oauthService: OAuthService) {} login(): void { this.oauthService.initLoginFlow(); } ``` Au retour sur `/callback`, suivez la section correspondante (Amplify: `currentSession()`, OIDC: `loadDiscoveryDocumentAndLogin()` déjà appelé au bootstrap) puis redirigez vers l’espace protégé.