14 KiB
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
- Callback:
- Prod: adaptez avec votre domaine (ex:
https://app.example.com/callback
ethttps://app.example.com
)
- Dev (Angular local):
Variables de configuration (Angular)
Exemple d’entrée environment.ts
:
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:
npm i aws-amplify @aws-amplify/ui-angular
Configuration (ex: main.ts
ou app.module.ts
):
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):
import { Auth } from 'aws-amplify';
export class AuthService {
signIn(): Promise<void> {
return Auth.federatedSignIn(); // redirige vers Hosted UI
}
async handleCallback(): Promise<void> {
// 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<void> {
return Auth.signOut(); // redirige vers redirectSignOut
}
}
Routing du callback (ex: app-routing.module.ts
):
{ 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:
npm i angular-oauth2-oidc
Configuration (ex: app.module.ts
et un auth.config.ts
):
// 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
):
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:
login(): void { this.oauthService.initLoginFlow(); }
logout(): void { this.oauthService.logOut(); }
getIdToken(): string | null { return this.oauthService.getIdToken(); }
Protection de routes (ex: guard minimal):
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
:
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
:
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
:
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
utilisedevelopment
,ng build --configuration=production
utiliseproduction
)
Lecture dans le code Angular (Angular 17+/Vite) :
const env = (import.meta as any).env as Record<string, string>;
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
) :
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
) :
# 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 mapperimport.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
.
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<boolean | UrlTree> {
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:
{
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:
import { Auth } from 'aws-amplify';
login(): Promise<void> {
return Auth.federatedSignIn();
}
- Avec angular-oauth2-oidc:
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é.