up login page
This commit is contained in:
parent
92cbbb1595
commit
3b95c7871c
49 changed files with 12320 additions and 43 deletions
418
devops/design-system.md
Normal file
418
devops/design-system.md
Normal file
|
@ -0,0 +1,418 @@
|
|||
# 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<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 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<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`) :
|
||||
|
||||
```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<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:
|
||||
|
||||
```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<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 l’espace protégé.
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue