Authentication is a critical part of any modern web application. In Angular, services provide a centralized and reusable way to handle authentication logic such as logging in users, storing tokens, and managing session state. A well-structured login service improves code maintainability, security, and scalability.
This post will provide a comprehensive guide to setting up a login service in Angular, including JWT-based authentication, token storage, error handling, and best practices.
1. Understanding Angular Services for Authentication
Angular services are singleton objects (if provided at the root level) that encapsulate logic independent of components. For authentication, a service can handle:
- Sending login requests to the backend API.
- Storing and retrieving authentication tokens.
- Managing user session state.
- Providing guards for route protection.
By centralizing authentication logic in a service, multiple components can share the same login state without duplicating code.
2. Creating a Basic Login Service
Use Angular CLI to generate a service:
ng generate service auth
This creates auth.service.ts
. The basic login service using JWT may look like this:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthService {
private token: string | null = null;
constructor(private http: HttpClient) {}
login(username: string, password: string): Observable<{ token: string }> {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(res => this.token = res.token)
);
}
getToken(): string | null {
return this.token;
}
}
login()
sends credentials to the backend and stores the returned token.getToken()
retrieves the stored token for use in HTTP headers or guards.
3. Handling Tokens Securely
Storing tokens in memory is safe but lost on page reload. Common strategies include:
- Local Storage: Persistent but accessible by JavaScript (vulnerable to XSS).
- Session Storage: Cleared when the browser/tab is closed.
- Cookies: Can be HTTP-only to prevent XSS access.
Example using localStorage:
login(username: string, password: string): Observable<{ token: string }> {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(res => localStorage.setItem('authToken', res.token))
);
}
getToken(): string | null {
return localStorage.getItem('authToken');
}
logout() {
this.token = null;
localStorage.removeItem('authToken');
}
4. Adding Error Handling
Use RxJS operators like catchError
to handle login failures:
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
login(username: string, password: string): Observable<{ token: string }> {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(res => localStorage.setItem('authToken', res.token)),
catchError(err => {
console.error('Login failed', err);
return throwError(() => new Error('Login failed, please try again.'));
})
);
}
5. Using the AuthService in a Component
Inject the AuthService
into your login component:
import { Component } from '@angular/core';
import { AuthService } from './auth.service';
@Component({
selector: 'app-login',
template: `
<form (submit)="onLogin()">
<input [(ngModel)]="username" name="username" placeholder="Username">
<input [(ngModel)]="password" name="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
<p *ngIf="error">{{ error }}</p>
`
})
export class LoginComponent {
username = '';
password = '';
error: string | null = null;
constructor(private authService: AuthService) {}
onLogin() {
this.authService.login(this.username, this.password).subscribe({
next: res => console.log('Logged in!', res),
error: err => this.error = err.message
});
}
}
ngModel
binds form inputs to component variables.- Submitting the form calls
authService.login()
. - Errors are handled gracefully and displayed to the user.
6. Protecting Routes with Guards
Angular route guards use the login service to restrict access:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.authService.getToken()) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}
Apply the guard in routing:
const routes = [
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent }
];
7. Attaching JWT to HTTP Requests
To send the token with every HTTP request, use HTTP interceptors:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = this.authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: Bearer ${token}
}
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Register in app.module.ts
:
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
})
export class AppModule {}
8. Refresh Tokens and Session Management
For secure authentication, consider refresh tokens:
- Store a short-lived access token for API calls.
- Use a long-lived refresh token to request a new access token.
- Implement logic in the
AuthService
and interceptor to refresh tokens automatically.
Example:
refreshToken(): Observable<{ token: string }> {
return this.http.post<{ token: string }>('/api/refresh', {})
.pipe(tap(res => localStorage.setItem('authToken', res.token)));
}
9. Logging Out
Logging out should clear stored tokens and optionally redirect the user:
logout() {
localStorage.removeItem('authToken');
this.token = null;
this.router.navigate(['/login']);
}
10. Storing User Profile Data
You can extend the service to store authenticated user info:
private user: any = null;
getUser() {
if (!this.user) {
const token = this.getToken();
if (token) {
this.user = this.decodeToken(token); // Decode JWT to extract user info
}
}
return this.user;
}
private decodeToken(token: string) {
return JSON.parse(atob(token.split('.')[1]));
}
- This allows components to access user info without extra API calls.
11. Testing the AuthService
Use Angular testing utilities to ensure login functionality works:
it('should store token after login', () => {
const service: AuthService = TestBed.inject(AuthService);
const mockResponse = { token: '12345' };
spyOn(service['http'], 'post').and.returnValue(of(mockResponse));
service.login('user', 'pass').subscribe(res => {
expect(service.getToken()).toBe('12345');
});
});
12. Best Practices for a Login Service
- Centralize authentication logic in a single service.
- Use interceptors to attach tokens automatically.
- Handle errors gracefully in both service and components.
- Secure token storage, preferring HTTP-only cookies for sensitive apps.
- Implement route guards to protect secure routes.
- Support refresh tokens for long-lived sessions.
- Keep user state reactive if using Observables or BehaviorSubjects.
13. Example: Complete AuthService
@Injectable({ providedIn: 'root' })
export class AuthService {
private token: string | null = null;
private user: any = null;
constructor(private http: HttpClient, private router: Router) {}
login(username: string, password: string): Observable<{ token: string }> {
return this.http.post<{ token: string }>('/api/login', { username, password })
.pipe(
tap(res => {
this.token = res.token;
localStorage.setItem('authToken', res.token);
this.user = this.decodeToken(res.token);
}),
catchError(err => throwError(() => new Error('Login failed')))
);
}
getToken(): string | null { return localStorage.getItem('authToken'); }
getUser(): any {
if (!this.user && this.token) this.user = this.decodeToken(this.token);
return this.user;
}
logout() {
this.token = null;
this.user = null;
localStorage.removeItem('authToken');
this.router.navigate(['/login']);
}
private decodeToken(token: string) {
return JSON.parse(atob(token.split('.')[1]));
}
}
Leave a Reply