ng-implementation/devops/design-system.md
2025-10-03 11:08:34 +02:00

418 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Authentification Cognito pour webapps Angular
Ce guide décrit comment intégrer lauthentification 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 dautres 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 dentré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 dauth):
```typescript
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`):
```typescript
{ path: 'callback', component: CallbackComponent }
```
Dans `CallbackComponent`, appelez `handleCallback()` puis redirigez lutilisateur vers lapp.
## 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 nexpose 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 lheure système (skew de plus de 5 min peut invalider les tokens)
- Évitez dexposer 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 lapp
- 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 denvironnement + Guard Angular
Nous supportons un mode où lauthentification Cognito est optionnelle selon une variable denvironnement, et une Guard Angular assure la redirection vers une page de login configurable.
### Variables denvironnement
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 dAngular
Nous utilisons les fichiers `.env` dAngular 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<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 loutillage Angular, laccès peut être `import.meta.env.NG_APP_...` au lieu de lindexation par chaîne. Gardez le préfixe `NG_APP_` pour garantir lexposition 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 lapp.
- 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 lapp.
### Guard Angular (générique)
Exemple de Guard compatible avec Amplify ou angular-oauth2-oidc: elle vérifie `requireAuth`. Si lauth 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<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 lutiliser comme cidessus.
### 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 limplémentation:
- Avec Amplify:
```typescript
import { Auth } from 'aws-amplify';
login(): Promise<void> {
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 lespace protégé.