Setting Up a Login Service in Angular Applications

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&lt;{ token: string }&gt;('/api/login', { username, password })
  .pipe(
    tap(res =&gt; 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:

  1. Local Storage: Persistent but accessible by JavaScript (vulnerable to XSS).
  2. Session Storage: Cleared when the browser/tab is closed.
  3. 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 =&gt; 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 =&gt; localStorage.setItem('authToken', res.token)),
  catchError(err =&gt; {
    console.error('Login failed', err);
    return throwError(() =&gt; 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: `
&lt;form (submit)="onLogin()"&gt;
  &lt;input &#91;(ngModel)]="username" name="username" placeholder="Username"&gt;
  &lt;input &#91;(ngModel)]="password" name="password" type="password" placeholder="Password"&gt;
  &lt;button type="submit"&gt;Login&lt;/button&gt;
&lt;/form&gt;
&lt;p *ngIf="error"&gt;{{ error }}&lt;/p&gt;
` }) export class LoginComponent { username = ''; password = ''; error: string | null = null; constructor(private authService: AuthService) {} onLogin() {
this.authService.login(this.username, this.password).subscribe({
  next: res =&gt; console.log('Logged in!', res),
  error: err =&gt; 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(&#91;'/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 =&gt; 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

  1. Centralize authentication logic in a single service.
  2. Use interceptors to attach tokens automatically.
  3. Handle errors gracefully in both service and components.
  4. Secure token storage, preferring HTTP-only cookies for sensitive apps.
  5. Implement route guards to protect secure routes.
  6. Support refresh tokens for long-lived sessions.
  7. 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&lt;{ token: string }&gt;('/api/login', { username, password })
  .pipe(
    tap(res =&gt; {
      this.token = res.token;
      localStorage.setItem('authToken', res.token);
      this.user = this.decodeToken(res.token);
    }),
    catchError(err =&gt; throwError(() =&gt; new Error('Login failed')))
  );
} getToken(): string | null { return localStorage.getItem('authToken'); } getUser(): any {
if (!this.user &amp;&amp; this.token) this.user = this.decodeToken(this.token);
return this.user;
} logout() {
this.token = null;
this.user = null;
localStorage.removeItem('authToken');
this.router.navigate(&#91;'/login']);
} private decodeToken(token: string) {
return JSON.parse(atob(token.split('.')&#91;1]));
} }

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *