@acontplus/ng-auth 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,13 +56,13 @@ export const appConfig: ApplicationConfig = {
56
56
  provideHttpClient(
57
57
  withInterceptors([
58
58
  authRedirectInterceptor, // Handles 401 errors
59
- csrfInterceptor, // CSRF protection
60
- ])
59
+ csrfInterceptor, // CSRF protection
60
+ ]),
61
61
  ),
62
-
62
+
63
63
  // Auth services
64
64
  ...authProviders,
65
-
65
+
66
66
  // Environment config
67
67
  {
68
68
  provide: ENVIRONMENT,
@@ -86,11 +86,11 @@ import { authGuard } from '@acontplus/ng-auth';
86
86
  export const routes: Routes = [
87
87
  {
88
88
  path: 'auth',
89
- loadComponent: () => import('./pages/auth').then(m => m.AuthPage),
89
+ loadComponent: () => import('./pages/auth').then((m) => m.AuthPage),
90
90
  },
91
91
  {
92
92
  path: 'dashboard',
93
- loadComponent: () => import('./pages/dashboard').then(m => m.Dashboard),
93
+ loadComponent: () => import('./pages/dashboard').then((m) => m.Dashboard),
94
94
  canActivate: [authGuard], // 👈 Protected route
95
95
  },
96
96
  ];
@@ -108,11 +108,11 @@ import { AuthState } from '@acontplus/ng-auth';
108
108
  template: `
109
109
  <h1>Welcome, {{ authState.user()?.displayName }}!</h1>
110
110
  <p>Email: {{ authState.user()?.email }}</p>
111
-
111
+
112
112
  @if (authState.isLoading()) {
113
113
  <p>Loading...</p>
114
114
  }
115
-
115
+
116
116
  <button (click)="logout()">Logout</button>
117
117
  `,
118
118
  })
@@ -139,10 +139,10 @@ const authState = inject(AuthState);
139
139
 
140
140
  // Reactive state (signals)
141
141
  authState.isAuthenticated(); // Signal<boolean>
142
- authState.user(); // Signal<UserData | null>
143
- authState.isLoading(); // Signal<boolean>
144
- authState.mfaRequired(); // Signal<boolean>
145
- authState.emailVerified(); // Signal<boolean>
142
+ authState.user(); // Signal<UserData | null>
143
+ authState.isLoading(); // Signal<boolean>
144
+ authState.mfaRequired(); // Signal<boolean>
145
+ authState.emailVerified(); // Signal<boolean>
146
146
 
147
147
  // Authentication methods
148
148
  authState.login({ email, password, rememberMe }).subscribe();
@@ -189,6 +189,7 @@ const routes: Routes = [
189
189
  ```
190
190
 
191
191
  **How it works:**
192
+
192
193
  1. User tries to access `/admin/settings`
193
194
  2. `authGuard` checks authentication
194
195
  3. If not authenticated: stores URL → redirects to login
@@ -205,38 +206,36 @@ import { AuthTokenRepositoryImpl } from '@acontplus/ng-auth';
205
206
  const tokenRepo = inject(AuthTokenRepositoryImpl);
206
207
 
207
208
  // Token operations
208
- tokenRepo.getToken(); // Get access token
209
- tokenRepo.getRefreshToken(); // Get refresh token
209
+ tokenRepo.getToken(); // Get access token
210
+ tokenRepo.getRefreshToken(); // Get refresh token
210
211
  tokenRepo.saveTokens(tokens, rememberMe);
211
212
  tokenRepo.clearTokens();
212
213
 
213
214
  // Validation
214
- tokenRepo.isAuthenticated(); // Check if token is valid
215
- tokenRepo.needsRefresh(); // Check if token needs refresh
215
+ tokenRepo.isAuthenticated(); // Check if token is valid
216
+ tokenRepo.needsRefresh(); // Check if token needs refresh
216
217
 
217
218
  // User data extraction from JWT
218
- tokenRepo.getUserData(); // Returns UserData | null
219
- tokenRepo.isRememberMeEnabled(); // Check storage location
219
+ tokenRepo.getUserData(); // Returns UserData | null
220
+ tokenRepo.isRememberMeEnabled(); // Check storage location
220
221
  ```
221
222
 
222
223
  ### Interceptors
223
224
 
224
225
  **Auth Redirect Interceptor** - Handles 401 errors:
226
+
225
227
  ```typescript
226
228
  import { authRedirectInterceptor } from '@acontplus/ng-auth';
227
229
 
228
- provideHttpClient(
229
- withInterceptors([authRedirectInterceptor])
230
- );
230
+ provideHttpClient(withInterceptors([authRedirectInterceptor]));
231
231
  ```
232
232
 
233
233
  **CSRF Interceptor** - Adds CSRF tokens:
234
+
234
235
  ```typescript
235
236
  import { csrfInterceptor } from '@acontplus/ng-auth';
236
237
 
237
- provideHttpClient(
238
- withInterceptors([csrfInterceptor])
239
- );
238
+ provideHttpClient(withInterceptors([csrfInterceptor]));
240
239
  ```
241
240
 
242
241
  ## Login Component
@@ -269,10 +268,10 @@ export class AuthPage {}
269
268
  [additionalSignupControls]="extraSignupFields"
270
269
  [footerContent]="footer"
271
270
  />
272
-
271
+
273
272
  <ng-template #footer>
274
273
  <div class="text-center">
275
- <a href="/terms">Terms</a> |
274
+ <a href="/terms">Terms</a> |
276
275
  <a href="/privacy">Privacy</a>
277
276
  </div>
278
277
  </ng-template>
@@ -282,7 +281,7 @@ export class AuthPage {
282
281
  extraSigninFields = {
283
282
  companyId: new FormControl('', Validators.required),
284
283
  };
285
-
284
+
286
285
  extraSignupFields = {
287
286
  role: new FormControl('user', Validators.required),
288
287
  };
@@ -291,16 +290,16 @@ export class AuthPage {
291
290
 
292
291
  ### Component Inputs
293
292
 
294
- | Input | Type | Default | Description |
295
- |-------|------|---------|-------------|
296
- | `title` | `string` | `'Login'` | Card title |
297
- | `showRegisterButton` | `boolean` | `true` | Show register toggle |
298
- | `showRememberMe` | `boolean` | `true` | Show remember me checkbox |
299
- | `additionalSigninControls` | `Record<string, AbstractControl>` | `{}` | Extra login fields |
300
- | `additionalSignupControls` | `Record<string, AbstractControl>` | `{}` | Extra signup fields |
301
- | `additionalSigninFields` | `TemplateRef` | `null` | Custom login template |
302
- | `additionalSignupFields` | `TemplateRef` | `null` | Custom signup template |
303
- | `footerContent` | `TemplateRef` | `null` | Custom footer |
293
+ | Input | Type | Default | Description |
294
+ | -------------------------- | --------------------------------- | --------- | ------------------------- |
295
+ | `title` | `string` | `'Login'` | Card title |
296
+ | `showRegisterButton` | `boolean` | `true` | Show register toggle |
297
+ | `showRememberMe` | `boolean` | `true` | Show remember me checkbox |
298
+ | `additionalSigninControls` | `Record<string, AbstractControl>` | `{}` | Extra login fields |
299
+ | `additionalSignupControls` | `Record<string, AbstractControl>` | `{}` | Extra signup fields |
300
+ | `additionalSigninFields` | `TemplateRef` | `null` | Custom login template |
301
+ | `additionalSignupFields` | `TemplateRef` | `null` | Custom signup template |
302
+ | `footerContent` | `TemplateRef` | `null` | Custom footer |
304
303
 
305
304
  ## Architecture
306
305
 
@@ -330,6 +329,7 @@ ng-auth/
330
329
  ```
331
330
 
332
331
  **Key decisions:**
332
+
333
333
  - ❌ No use cases layer (unnecessary for a library)
334
334
  - ✅ AuthState consolidates all auth operations
335
335
  - ✅ Signals for reactive state
@@ -353,15 +353,17 @@ import { Login, AuthState } from '@acontplus/ng-auth';
353
353
  <acp-login title="Welcome to Acontplus" />
354
354
  </div>
355
355
  `,
356
- styles: [`
357
- .auth-container {
358
- min-height: 100vh;
359
- display: flex;
360
- align-items: center;
361
- justify-content: center;
362
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
363
- }
364
- `],
356
+ styles: [
357
+ `
358
+ .auth-container {
359
+ min-height: 100vh;
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
364
+ }
365
+ `,
366
+ ],
365
367
  })
366
368
  export class AuthPage {}
367
369
  ```
@@ -387,15 +389,15 @@ import { Router } from '@angular/router';
387
389
  }
388
390
  </div>
389
391
  </header>
390
-
392
+
391
393
  <main>
392
394
  <p>Welcome back, {{ user()?.displayName }}!</p>
393
395
  <p>Email: {{ user()?.email }}</p>
394
-
396
+
395
397
  @if (user()?.roles?.includes('admin')) {
396
398
  <a routerLink="/admin">Admin Panel</a>
397
399
  }
398
-
400
+
399
401
  @if (!emailVerified()) {
400
402
  <div class="warning">
401
403
  Please verify your email
@@ -409,17 +411,17 @@ import { Router } from '@angular/router';
409
411
  export class Dashboard {
410
412
  private readonly authState = inject(AuthState);
411
413
  private readonly router = inject(Router);
412
-
414
+
413
415
  // Reactive state
414
416
  user = this.authState.user;
415
417
  emailVerified = this.authState.emailVerified;
416
-
418
+
417
419
  logout() {
418
420
  this.authState.logout().subscribe(() => {
419
421
  this.router.navigate(['/auth']);
420
422
  });
421
423
  }
422
-
424
+
423
425
  resendVerification() {
424
426
  const email = this.user()?.email;
425
427
  if (email) {
@@ -434,15 +436,16 @@ export class Dashboard {
434
436
  If you're upgrading from the old version:
435
437
 
436
438
  ### ❌ Before (Use Cases)
439
+
437
440
  ```typescript
438
441
  import { LoginUseCase, LogoutUseCase } from '@acontplus/ng-auth';
439
442
 
440
443
  export class MyComponent {
441
444
  constructor(
442
445
  private loginUseCase: LoginUseCase,
443
- private logoutUseCase: LogoutUseCase
446
+ private logoutUseCase: LogoutUseCase,
444
447
  ) {}
445
-
448
+
446
449
  login() {
447
450
  this.loginUseCase.execute({ email, password }).subscribe();
448
451
  }
@@ -450,13 +453,14 @@ export class MyComponent {
450
453
  ```
451
454
 
452
455
  ### ✅ After (AuthState)
456
+
453
457
  ```typescript
454
458
  import { inject } from '@angular/core';
455
459
  import { AuthState } from '@acontplus/ng-auth';
456
460
 
457
461
  export class MyComponent {
458
462
  private authState = inject(AuthState);
459
-
463
+
460
464
  login() {
461
465
  this.authState.login({ email, password }).subscribe();
462
466
  }
@@ -566,17 +570,13 @@ import { Login } from '@acontplus/ng-auth';
566
570
  selector: 'app-auth',
567
571
  standalone: true,
568
572
  imports: [Login],
569
- template: `
570
- <acp-login
571
- [enableDomainDiscovery]="true"
572
- [showSocialLogin]="true"
573
- />
574
- `,
573
+ template: ` <acp-login [enableDomainDiscovery]="true" [showSocialLogin]="true" /> `,
575
574
  })
576
575
  export class AuthPage {}
577
576
  ```
578
577
 
579
578
  **How it works:**
579
+
580
580
  1. User types email: `user@acme.com`
581
581
  2. Library calls `/api/auth/domain-discovery` with email
582
582
  3. Backend responds with OAuth provider and tenant info
@@ -601,6 +601,7 @@ export const routes: Routes = [
601
601
  ```
602
602
 
603
603
  Configure these redirect URIs in your OAuth provider:
604
+
604
605
  - Google: `https://your-app.com/auth/callback/google`
605
606
  - Microsoft: `https://your-app.com/auth/callback/microsoft`
606
607
  - GitHub: `https://your-app.com/auth/callback/github`
@@ -617,13 +618,11 @@ import { AuthState } from '@acontplus/ng-auth';
617
618
  selector: 'app-custom-login',
618
619
  template: `
619
620
  <input #emailInput (blur)="checkDomain(emailInput.value)" />
620
-
621
+
621
622
  @if (discovery?.requiresOAuth) {
622
- <button (click)="loginWithOAuth()">
623
- Login with {{ discovery.provider }}
624
- </button>
623
+ <button (click)="loginWithOAuth()">Login with {{ discovery.provider }}</button>
625
624
  }
626
-
625
+
627
626
  @if (discovery?.allowPasswordLogin) {
628
627
  <input type="password" [(ngModel)]="password" />
629
628
  <button (click)="loginWithPassword()">Login</button>
@@ -632,11 +631,11 @@ import { AuthState } from '@acontplus/ng-auth';
632
631
  })
633
632
  export class CustomLogin {
634
633
  private authState = inject(AuthState);
635
-
634
+
636
635
  email = '';
637
636
  password = '';
638
637
  discovery: DomainDiscoveryResponse | null = null;
639
-
638
+
640
639
  checkDomain(email: string) {
641
640
  this.email = email;
642
641
  this.authState.discoverDomain(email).subscribe({
@@ -645,10 +644,10 @@ export class CustomLogin {
645
644
  },
646
645
  });
647
646
  }
648
-
647
+
649
648
  loginWithOAuth() {
650
649
  if (!this.discovery?.provider) return;
651
-
650
+
652
651
  // This redirects to OAuth provider
653
652
  this.authState.startOAuthFlow({
654
653
  provider: this.discovery.provider,
@@ -656,12 +655,14 @@ export class CustomLogin {
656
655
  domain: this.discovery.domain,
657
656
  });
658
657
  }
659
-
658
+
660
659
  loginWithPassword() {
661
- this.authState.login({
662
- email: this.email,
663
- password: this.password,
664
- }).subscribe();
660
+ this.authState
661
+ .login({
662
+ email: this.email,
663
+ password: this.password,
664
+ })
665
+ .subscribe();
665
666
  }
666
667
  }
667
668
  ```
@@ -682,18 +683,14 @@ import { AuthState } from '@acontplus/ng-auth';
682
683
  export class MyOAuthCallback implements OnInit {
683
684
  private authState = inject(AuthState);
684
685
  private route = inject(ActivatedRoute);
685
-
686
+
686
687
  ngOnInit() {
687
688
  const provider = this.route.snapshot.paramMap.get('provider');
688
689
  const code = this.route.snapshot.queryParamMap.get('code');
689
690
  const state = this.route.snapshot.queryParamMap.get('state');
690
-
691
+
691
692
  if (provider && code && state) {
692
- this.authState.handleOAuthCallback(
693
- provider as SocialProvider,
694
- code,
695
- state
696
- ).subscribe({
693
+ this.authState.handleOAuthCallback(provider as SocialProvider, code, state).subscribe({
697
694
  next: () => {
698
695
  // Success - AuthState redirects automatically
699
696
  },
@@ -715,10 +712,10 @@ Here's a Node.js/Express example for Google Workspace multi-tenant:
715
712
  app.post('/api/auth/domain-discovery', async (req, res) => {
716
713
  const { email } = req.body;
717
714
  const domain = email.split('@')[1];
718
-
715
+
719
716
  // Check if domain is configured for OAuth
720
717
  const tenant = await db.tenants.findOne({ domain });
721
-
718
+
722
719
  if (tenant?.oauthProvider === 'google') {
723
720
  return res.json({
724
721
  provider: 'google',
@@ -728,7 +725,7 @@ app.post('/api/auth/domain-discovery', async (req, res) => {
728
725
  allowPasswordLogin: tenant.allowPasswordFallback,
729
726
  });
730
727
  }
731
-
728
+
732
729
  // Default to password login
733
730
  res.json({
734
731
  requiresOAuth: false,
@@ -739,25 +736,25 @@ app.post('/api/auth/domain-discovery', async (req, res) => {
739
736
  // Get OAuth URL
740
737
  app.get('/api/auth/social/google/url', async (req, res) => {
741
738
  const { tenantId, domain } = req.query;
742
-
739
+
743
740
  const tenant = await db.tenants.findOne({ id: tenantId });
744
741
  const state = crypto.randomBytes(32).toString('hex');
745
-
742
+
746
743
  // Store state for verification
747
744
  await redis.set(`oauth:state:${state}`, tenantId, 'EX', 600);
748
-
745
+
749
746
  const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
750
747
  authUrl.searchParams.set('client_id', tenant.googleClientId);
751
748
  authUrl.searchParams.set('redirect_uri', 'https://your-app.com/auth/callback/google');
752
749
  authUrl.searchParams.set('response_type', 'code');
753
750
  authUrl.searchParams.set('scope', 'openid email profile');
754
751
  authUrl.searchParams.set('state', state);
755
-
752
+
756
753
  // Tenant hint for Google Workspace
757
754
  if (domain) {
758
755
  authUrl.searchParams.set('hd', domain);
759
756
  }
760
-
757
+
761
758
  res.json({
762
759
  url: authUrl.toString(),
763
760
  state,
@@ -767,15 +764,15 @@ app.get('/api/auth/social/google/url', async (req, res) => {
767
764
  // OAuth callback
768
765
  app.post('/api/auth/social/google/callback', async (req, res) => {
769
766
  const { code, state, tenantId } = req.body;
770
-
767
+
771
768
  // Verify state
772
769
  const storedTenantId = await redis.get(`oauth:state:${state}`);
773
770
  if (!storedTenantId) {
774
771
  return res.status(400).json({ error: 'Invalid state' });
775
772
  }
776
-
773
+
777
774
  const tenant = await db.tenants.findOne({ id: tenantId });
778
-
775
+
779
776
  // Exchange code for tokens
780
777
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
781
778
  method: 'POST',
@@ -788,20 +785,20 @@ app.post('/api/auth/social/google/callback', async (req, res) => {
788
785
  redirect_uri: 'https://your-app.com/auth/callback/google',
789
786
  }),
790
787
  });
791
-
788
+
792
789
  const tokens = await tokenResponse.json();
793
-
790
+
794
791
  // Get user info
795
792
  const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
796
793
  headers: { Authorization: `Bearer ${tokens.access_token}` },
797
- }).then(r => r.json());
798
-
794
+ }).then((r) => r.json());
795
+
799
796
  // Verify domain matches tenant
800
797
  const userDomain = userInfo.email.split('@')[1];
801
798
  if (tenant.domain !== userDomain) {
802
799
  return res.status(403).json({ error: 'Domain mismatch' });
803
800
  }
804
-
801
+
805
802
  // Create or update user
806
803
  const user = await db.users.upsert({
807
804
  email: userInfo.email,
@@ -809,10 +806,10 @@ app.post('/api/auth/social/google/callback', async (req, res) => {
809
806
  tenantId: tenant.id,
810
807
  emailVerified: userInfo.verified_email,
811
808
  });
812
-
809
+
813
810
  // Generate JWT
814
811
  const jwt = generateJWT(user);
815
-
812
+
816
813
  res.json({
817
814
  token: jwt,
818
815
  refreshToken: generateRefreshToken(user),
@@ -826,23 +823,23 @@ app.post('/api/auth/social/google/callback', async (req, res) => {
826
823
  // Tenant configuration for admin panel
827
824
  interface TenantConfig {
828
825
  tenantId: string;
829
- domain: string; // e.g., "acme.com"
830
- displayName: string; // e.g., "Acme Corporation"
826
+ domain: string; // e.g., "acme.com"
827
+ displayName: string; // e.g., "Acme Corporation"
831
828
  provider: 'google' | 'microsoft';
832
-
829
+
833
830
  // OAuth credentials
834
831
  clientId: string;
835
- clientSecret: string; // Stored securely on backend
836
- issuer?: string; // For OIDC providers
837
-
832
+ clientSecret: string; // Stored securely on backend
833
+ issuer?: string; // For OIDC providers
834
+
838
835
  // Settings
839
- allowedDomains?: string[]; // Additional domains
836
+ allowedDomains?: string[]; // Additional domains
840
837
  allowPasswordLogin: boolean;
841
- autoProvision: boolean; // Auto-create users on first login
842
-
838
+ autoProvision: boolean; // Auto-create users on first login
839
+
843
840
  customParameters?: {
844
- hd?: string; // Google Workspace domain hint
845
- tenant?: string; // Azure AD tenant ID
841
+ hd?: string; // Google Workspace domain hint
842
+ tenant?: string; // Azure AD tenant ID
846
843
  };
847
844
  }
848
845
  ```
@@ -850,6 +847,7 @@ interface TenantConfig {
850
847
  ### Testing OAuth Locally
851
848
 
852
849
  For local development, use these redirect URIs:
850
+
853
851
  - `http://localhost:4200/auth/callback/google`
854
852
  - `http://localhost:4200/auth/callback/microsoft`
855
853
 
@@ -870,22 +868,22 @@ Configure your OAuth apps with these URLs in development mode.
870
868
  // Tenants table
871
869
  interface Tenant {
872
870
  id: string;
873
- domain: string; // "acme.com"
874
- displayName: string; // "Acme Corporation"
871
+ domain: string; // "acme.com"
872
+ displayName: string; // "Acme Corporation"
875
873
  oauthProvider: 'google' | 'microsoft' | 'apple' | null;
876
-
874
+
877
875
  // OAuth credentials (encrypted at rest!)
878
876
  googleClientId?: string;
879
877
  googleClientSecret?: string;
880
878
  microsoftClientId?: string;
881
879
  microsoftClientSecret?: string;
882
- azureTenantId?: string; // For Azure AD
883
-
880
+ azureTenantId?: string; // For Azure AD
881
+
884
882
  // Settings
885
883
  allowPasswordLogin: boolean;
886
884
  autoProvisionUsers: boolean; // Auto-create users on first OAuth login
887
885
  requireEmailVerification: boolean;
888
-
886
+
889
887
  createdAt: Date;
890
888
  updatedAt: Date;
891
889
  }
@@ -895,15 +893,15 @@ interface User {
895
893
  id: string;
896
894
  email: string;
897
895
  displayName: string;
898
- tenantId?: string; // Reference to tenant
896
+ tenantId?: string; // Reference to tenant
899
897
  emailVerified: boolean;
900
-
898
+
901
899
  // Track which auth method they used
902
900
  authProvider: 'password' | 'google' | 'microsoft' | 'github' | 'apple';
903
-
901
+
904
902
  // OAuth user ID from provider
905
903
  oauthProviderId?: string;
906
-
904
+
907
905
  createdAt: Date;
908
906
  lastLoginAt: Date;
909
907
  }
@@ -928,6 +926,7 @@ interface User {
928
926
  - Example: `authUrl.searchParams.set('hd', 'acme.com')`
929
927
 
930
928
  **Scopes needed:**
929
+
931
930
  - `openid` - Basic authentication
932
931
  - `email` - User's email address
933
932
  - `profile` - User's name and photo
@@ -954,6 +953,7 @@ interface User {
954
953
  11. Add: `openid`, `email`, `profile`
955
954
 
956
955
  **For multi-tenant:**
956
+
957
957
  - Use tenant-specific endpoint: `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize`
958
958
  - Or use common endpoint: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`
959
959
 
@@ -969,6 +969,7 @@ interface User {
969
969
  5. Copy **Client ID** and generate **Client Secret**
970
970
 
971
971
  **Scopes needed:**
972
+
972
973
  - `user:email` - Access user's email address
973
974
  - `read:user` - Read user profile information
974
975
 
@@ -982,6 +983,7 @@ interface User {
982
983
  **Cause**: The redirect URI in your OAuth request doesn't match what's configured in the provider's settings.
983
984
 
984
985
  **Solutions**:
986
+
985
987
  1. Ensure exact match including protocol (`http://` vs `https://`), port, and path
986
988
  2. No trailing slash unless configured that way
987
989
  3. Check for typos in domain name
@@ -995,6 +997,7 @@ interface User {
995
997
  **Cause**: OAuth state token expired or doesn't match.
996
998
 
997
999
  **Solutions**:
1000
+
998
1001
  1. Increase Redis/storage TTL for state tokens (recommended: 600 seconds)
999
1002
  2. User may have taken too long to complete OAuth flow
1000
1003
  3. Ensure state is properly stored before redirect
@@ -1008,6 +1011,7 @@ interface User {
1008
1011
  **Cause**: User authenticated with an account from a different organization.
1009
1012
 
1010
1013
  **Solutions**:
1014
+
1011
1015
  1. For Google: Use `hd` parameter to force specific domain
1012
1016
  2. Show clear error message explaining which domain is expected
1013
1017
  3. Verify domain matching logic in backend before issuing JWT
@@ -1021,6 +1025,7 @@ interface User {
1021
1025
  **Cause**: Frontend or backend error during token exchange.
1022
1026
 
1023
1027
  **Solutions**:
1028
+
1024
1029
  1. Check browser console for JavaScript errors
1025
1030
  2. Verify backend endpoint is responding
1026
1031
  3. Check CORS configuration on backend
@@ -1036,29 +1041,29 @@ interface User {
1036
1041
  describe('OAuth Flow', () => {
1037
1042
  let authState: AuthState;
1038
1043
  let httpMock: HttpTestingController;
1039
-
1044
+
1040
1045
  beforeEach(() => {
1041
1046
  TestBed.configureTestingModule({
1042
1047
  imports: [HttpClientTestingModule],
1043
1048
  providers: [AuthState, ...authProviders],
1044
1049
  });
1045
-
1050
+
1046
1051
  authState = TestBed.inject(AuthState);
1047
1052
  httpMock = TestBed.inject(HttpTestingController);
1048
1053
  });
1049
-
1054
+
1050
1055
  it('should discover Google Workspace tenant', (done) => {
1051
- authState.discoverDomain('user@acme.com').subscribe(result => {
1056
+ authState.discoverDomain('user@acme.com').subscribe((result) => {
1052
1057
  expect(result.provider).toBe('google');
1053
1058
  expect(result.tenantId).toBe('acme-123');
1054
1059
  expect(result.requiresOAuth).toBe(true);
1055
1060
  done();
1056
1061
  });
1057
-
1062
+
1058
1063
  const req = httpMock.expectOne('/api/auth/domain-discovery');
1059
1064
  expect(req.request.method).toBe('POST');
1060
1065
  expect(req.request.body).toEqual({ email: 'user@acme.com' });
1061
-
1066
+
1062
1067
  req.flush({
1063
1068
  provider: 'google',
1064
1069
  tenantId: 'acme-123',
@@ -1066,25 +1071,23 @@ describe('OAuth Flow', () => {
1066
1071
  allowPasswordLogin: false,
1067
1072
  });
1068
1073
  });
1069
-
1074
+
1070
1075
  it('should start OAuth flow with tenant context', () => {
1071
1076
  spyOn(window.location, 'href', 'set');
1072
-
1077
+
1073
1078
  authState.startOAuthFlow({
1074
1079
  provider: 'google',
1075
1080
  tenantId: 'acme-123',
1076
1081
  domain: 'acme.com',
1077
1082
  });
1078
-
1079
- const req = httpMock.expectOne(
1080
- req => req.url.includes('/api/auth/social/google/url')
1081
- );
1082
-
1083
+
1084
+ const req = httpMock.expectOne((req) => req.url.includes('/api/auth/social/google/url'));
1085
+
1083
1086
  req.flush({
1084
1087
  url: 'https://accounts.google.com/o/oauth2/v2/auth?...',
1085
1088
  state: 'test-state-token',
1086
1089
  });
1087
-
1090
+
1088
1091
  // Verify state stored in session storage
1089
1092
  expect(sessionStorage.getItem('oauth_state')).toBe('test-state-token');
1090
1093
  expect(sessionStorage.getItem('oauth_tenant_id')).toBe('acme-123');