Skip to content
Back to skills

Angular Authentication

ASecurity

Reviews how an Angular SPA authenticates against an OIDC/OAuth2 IdP or the .NET backend — distinct from angular-security's permission gating, which assumes authentication already happened. Flags tokens in localStorage instead of httpOnly Secure cookies, no central auth interceptor, no silent renewal, hand-rolled OIDC/PKCE, decentralized auth state, and no global 401 handling. Outputs pilot-angular authentication standard IDs.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 6, 2026
developmentjavascripttypescriptgojavaangularazuregitapibackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-authentication --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Authentication?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Authentication
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-angular-authentication/badge)](https://www.skillsdirectory.com/skills/agenticpawan-angular-authentication)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-authentication
description: Reviews how an Angular SPA authenticates against an OIDC/OAuth2 IdP or the .NET backend — distinct from angular-security's permission gating, which assumes authentication already happened. Flags tokens in localStorage instead of httpOnly Secure cookies, no central auth interceptor, no silent renewal, hand-rolled OIDC/PKCE, decentralized auth state, and no global 401 handling. Outputs pilot-angular authentication standard IDs.
when_to_use: OIDC, OAuth2, PKCE, login, logout, access token, refresh token, silent renew, token storage, localStorage token, httpOnly cookie, bearer token, auth interceptor, angular-oauth2-oidc, MSAL Angular, Entra ID, Azure AD, 401 handling, session expiry, auth state, isAuthenticated, currentUser signal
applies_to: angular
---

<!-- Version index:
  HttpInterceptorFn (functional interceptors)  Angular 15+ (provideHttpClient)
  inject() in class fields                     Angular 14+
  signal()-based auth state                     Angular 16+
  angular-oauth2-oidc                            all Angular versions (community library)
  MSAL Angular (@azure/msal-angular)             all Angular versions (Entra ID)
-->

## Rule reference

| ID | Standard | Severity |
|----|----------|----------|
| NGAUTH-001 | OWASP A07 | block |
| NGAUTH-002 | InternalPolicy | warn |
| NGAUTH-003 | InternalPolicy | warn |
| NGAUTH-004 | InternalPolicy | warn |
| NGAUTH-005 | InternalPolicy | warn |
| NGAUTH-006 | OWASP A07 | block |

---

## NGAUTH-001 — Tokens in localStorage/sessionStorage instead of httpOnly cookies

`localStorage` and `sessionStorage` are readable by any script running in the page —
one XSS gap anywhere in the app (or in a third-party script, see
`angular-third-party-scripts`) is enough to exfiltrate every stored token. An httpOnly
cookie set by the .NET backend is invisible to JavaScript entirely; the token never
touches app code.

### BAD — token stored and read from localStorage

```typescript
// auth.service.ts
login(credentials: LoginRequest) {
  return this.http.post<TokenResponse>('/api/auth/login', credentials).pipe(
    tap(res => {
      localStorage.setItem('access_token', res.accessToken); // readable by any injected script
      localStorage.setItem('refresh_token', res.refreshToken);
    })
  );
}

getToken(): string | null {
  return localStorage.getItem('access_token');
}
```

### GOOD — backend sets an httpOnly, Secure, SameSite cookie; Angular never sees the token

```csharp
// .NET backend — Program.cs / AuthController
Response.Cookies.Append("access_token", accessToken, new CookieOptions
{
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.Strict,
    Expires = DateTimeOffset.UtcNow.AddMinutes(15)
});
```

```typescript
// auth.service.ts — Angular sends credentials, cookie is set/read by the browser automatically
login(credentials: LoginRequest) {
  return this.http.post<void>('/api/auth/login', credentials, { withCredentials: true });
  // No token is ever stored or read in JS — the cookie rides along on every request.
}
```

---

## NGAUTH-002 — No centralized auth interceptor attaching the bearer token

### Detection

Grep feature services for `Authorization: Bearer` headers built manually per HTTP call, or
services independently reading a token store. Flag when two or more services duplicate this
logic instead of a single functional `HttpInterceptorFn` registered once.

### BAD — each service attaches the header itself

```typescript
// orders.service.ts
getOrders() {
  const token = this.authService.getToken();
  return this.http.get<Order[]>('/api/orders', {
    headers: { Authorization: `Bearer ${token}` } // duplicated in every service
  });
}
```

### GOOD — one functional interceptor attaches the token to every outbound request

```typescript
// auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.accessToken();
  if (!token) return next(req);

  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([authInterceptor]))],
};
```

> If tokens live in an httpOnly cookie (NGAUTH-001's fix), this interceptor is unnecessary —
> the browser attaches the cookie automatically and `withCredentials: true` is all that's needed.

---

## NGAUTH-003 — No silent token renewal before expiry

### Detection

Check whether a short-lived access token has any renewal path before it expires. Flag when
the only recovery from an expired token is a full re-login (or a 401 loop — see NGAUTH-006)
rather than a proactive refresh scheduled ahead of the expiry timestamp.

### BAD — token simply expires and the user is bounced to login

```typescript
// No renewal scheduling anywhere. When the 15-minute access token expires mid-session,
// the next API call 401s and the user is redirected to log in again, losing form state.
```

### GOOD — refresh scheduled ahead of expiry using the library's built-in silent renew

```typescript
// app.config.ts (angular-oauth2-oidc)
export function initAuth(oauthService: OAuthService) {
  return () => oauthService.loadDiscoveryDocumentAndTryLogin().then(() => {
    oauthService.setupAutomaticSilentRefresh(); // renews ~before token expiry, no user interruption
  });
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => initAuth(inject(OAuthService))()),
  ],
};
```

```typescript
// MSAL Angular equivalent — silent token acquisition via acquireTokenSilent
this.msalService.acquireTokenSilent({ scopes: ['api://app-id/access_as_user'] }).subscribe({
  next: result => this.authState.setToken(result.accessToken),
  error: () => this.msalService.acquireTokenRedirect({ scopes: ['api://app-id/access_as_user'] }),
});
```

---

## NGAUTH-004 — Hand-rolled OIDC/PKCE instead of a maintained library

### Detection

Grep for manual construction of authorization-request URLs, manual PKCE `code_verifier`/
`code_challenge` generation, or manual `id_token` JWT parsing/validation. Flag any of these
in favor of `angular-oauth2-oidc` (generic OIDC providers) or `@azure/msal-angular` (Entra ID).
Hand-rolled PKCE is a common source of state/nonce validation bugs and token-replay gaps.

### BAD — PKCE flow implemented by hand

```typescript
function generateCodeVerifier(): string {
  return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32))); // easy to get subtly wrong
}
// ...manual code_challenge, manual redirect URL construction, manual state param, manual
// exchange of the auth code for tokens, manual id_token signature verification...
```

### GOOD — PKCE handled by a maintained library

```typescript
// app.config.ts (angular-oauth2-oidc)
export const authConfig: AuthConfig = {
  issuer: 'https://login.acme.com',
  redirectUri: window.location.origin + '/callback',
  clientId: 'angular-spa',
  responseType: 'code',
  scope: 'openid profile api.read',
  useSilentRefresh: true,
  // PKCE code_verifier/challenge, state, and nonce handling are all internal to the library.
};
```

```typescript
// MSAL Angular equivalent — PKCE is on by default for the authorization code flow
export const msalConfig: Configuration = {
  auth: { clientId: 'entra-app-id', authority: 'https://login.microsoftonline.com/tenant-id' },
};
```

---

## NGAUTH-005 — Auth state not centralized

### Detection

Grep components for independent JWT decoding (`atob(token.split('.')[1])` or similar) or
per-component reads of a token store to determine `isAuthenticated`/`currentUser`. Flag when
two or more components each derive this state independently instead of reading from one
injectable signal-based store.

### BAD — components decode the token themselves

```typescript
// profile.component.ts
ngOnInit() {
  const token = localStorage.getItem('access_token');
  const payload = JSON.parse(atob(token!.split('.')[1])); // duplicated decode logic
  this.userName = payload.name;
}
```

### GOOD — a single AuthState signal store is the source of truth

```typescript
// auth-state.service.ts
@Injectable({ providedIn: 'root' })
export class AuthStateService {
  private readonly _currentUser = signal<CurrentUser | null>(null);
  readonly currentUser = this._currentUser.asReadonly();
  readonly isAuthenticated = computed(() => this._currentUser() !== null);

  setUser(user: CurrentUser | null) {
    this._currentUser.set(user);
  }
}
```

```typescript
// profile.component.ts
readonly authState = inject(AuthStateService);
// template: @if (authState.isAuthenticated()) { <p>{{ authState.currentUser()?.name }}</p> }
```

---

## NGAUTH-006 — No global 401 handling

### Detection

Grep for `catchError` blocks in individual components/services that each redirect to login
on an error response, rather than one interceptor inspecting the status code centrally.
Flag ad-hoc per-call handling as duplicated and inconsistent (some calls redirect, others
silently fail).

### BAD — each component handles its own 401

```typescript
// order-detail.component.ts
this.orderService.getOrder(id).subscribe({
  error: err => {
    if (err.status === 401) this.router.navigate(['/login']); // repeated in every component
  },
});
```

### GOOD — one interceptor handles 401s: attempt refresh, else redirect

```typescript
// auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  return next(req).pipe(
    catchError(err => {
      if (err instanceof HttpErrorResponse && err.status === 401) {
        return authService.tryRefresh().pipe(
          switchMap(() => next(req.clone())), // retry the original request once, post-refresh
          catchError(() => {
            router.navigate(['/login'], { queryParams: { returnUrl: router.url } });
            return throwError(() => err);
          }),
        );
      }
      return throwError(() => err);
    }),
  );
};
```

---

## Angular authentication checklist

- [ ] Tokens live in an httpOnly, Secure, SameSite cookie set by the .NET backend, not in localStorage/sessionStorage
- [ ] `withCredentials: true` (or an interceptor attaching bearer tokens) is applied consistently, never per-service
- [ ] Silent token renewal is scheduled ahead of expiry, not left to fail into a re-login
- [ ] OIDC/PKCE flow uses `angular-oauth2-oidc` or `@azure/msal-angular`, never a hand-rolled implementation
- [ ] `isAuthenticated`/`currentUser` are read from one centralized signal-based `AuthStateService`
- [ ] No component decodes a JWT directly to derive auth state
- [ ] A single interceptor handles 401 responses — attempt one refresh, then redirect to login
- [ ] Client-side auth state is treated as UX only; real enforcement is the .NET backend (see `dotnet-authentication`)
- [ ] Route guards for authentication (`canActivate`) check the centralized auth state, not a locally decoded token
- [ ] Logout clears server-side session/cookie via an API call, not just client-side state

---

## References

- angular-oauth2-oidc: https://github.com/manfredsteyer/angular-oauth2-oidc
- MSAL Angular (Entra ID): https://learn.microsoft.com/en-us/entra/identity-platform/tutorial-v2-angular
- OWASP A07:2021 Identification and Authentication Failures: https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/
- OAuth 2.0 for Browser-Based Apps (PKCE): https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps
- Angular functional interceptors: https://angular.dev/guide/http/interceptors

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…