Implement a basic authentication service

Now, let's build a local authentication service that will enable us to demonstrate a robust login form, caching, and conditional navigation concepts based on authentication status and a user's role:

  1. Start by installing a JWT decoding library, and for faking authentication, a JWT encoding library:
$ npm install jwt-decode fake-jwt-sign
$ npm install -D @types/jwt-decode
  1. Define your imports for auth.service.ts:
src/app/auth/auth.service.ts
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'

import { sign } from 'fake-jwt-sign' // For fakeAuthProvider only
import * as decode from 'jwt-decode'

import { BehaviorSubject, Observable, of, throwError as observableThrowError } from 'rxjs'
import { catchError, map } from 'rxjs/operators'

import { environment } from '../../environments/environment'
import { Role } from './role.enum'
...
  1. Implement an IAuthStatus interface to store decoded user information, a helper interface, and the secure by-default defaultAuthStatus:
src/app/auth/auth.service.ts
...
export interface IAuthStatus {
isAuthenticated: boolean
userRole: Role
userId: string
}

interface IServerAuthResponse {
accessToken: string
}

const defaultAuthStatus = { isAuthenticated: false, userRole: Role.None, userId: null }
...

IAuthUser is an interface that represents the shape of a typical JWT that you may receive from your authentication service. It contains minimal information about the user and its role, so it can be attached to the header of server calls and optionally cached in localStorage to remember the user's login state. In the preceding implementation, we're assuming the default role of a Manager.

  1. Define the AuthService class with a BehaviorSubject to anchor the current authStatus of the user and configure an authProvider that can process an email and a password and return an IServerAuthResponse in the constructor:
src/app/auth/auth.service.ts
...
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly authProvider: (
email: string,
password: string
) => Observable<IServerAuthResponse>

authStatus = new BehaviorSubject<IAuthStatus>(defaultAuthStatus)

constructor(private httpClient: HttpClient) {
// Fake login function to simulate roles
this.authProvider = this.fakeAuthProvider
// Example of a real login call to server-side
// this.authProvider = this.exampleAuthProvider
}
...

Note that fakeAuthProvider is configured to be the authProvider for this service. A real auth provider may look like the following code, where users' email and password are sent to a POST endpoint, which verifies their information, creating and returning a JWT for our app to consume:

example
private exampleAuthProvider(
email: string,
password: string
): Observable<IServerAuthResponse> {
return this.httpClient.post<IServerAuthResponse>(`${environment.baseUrl}/v1/login`, {
email: email,
password: password,
})
}

It is pretty straightforward, since the hard work is done on the server side. This call can also be made to a third party.

Note that the API version, v1, in the URL path is defined at the service and not as part of the baseUrl. This is because each API can change versions independently from each other. Login may remain v1 for a long time, while other APIs may be upgraded to v2, v3, and such.
  1. Implement a fakeAuthProvider that simulates the authentication process, including creating a fake JWT on the fly:
src/app/auth/auth.service.ts
...
private fakeAuthProvider(
email: string,
password: string
): Observable<IServerAuthResponse> {
if (!email.toLowerCase().endsWith('@test.com')) {
return observableThrowError('Failed to login! Email needs to end with @test.com.')
}

const authStatus = {
isAuthenticated: true,
userId: 'e4d1bc2ab25c',
userRole: email.toLowerCase().includes('cashier')
? Role.Cashier
: email.toLowerCase().includes('clerk')
? Role.Clerk
: email.toLowerCase().includes('manager') ? Role.Manager : Role.None,
} as IAuthStatus

const authResponse = {
accessToken: sign(authStatus, 'secret', {
expiresIn: '1h',
algorithm: 'none',
}),
} as IServerAuthResponse

return of(authResponse)
}
...

The fakeAuthProvider implements what would otherwise be a server-side method right in the service, so you can conveniently experiment the code while fine-tuning your auth workflow. It creates and signs a JWT, with the temporary fake-jwt-sign library so that we can also demonstrate how to handle a properly-formed JWT.

Do not ship your Angular app with the fake-jwt-sign dependency, since it is meant to be server-side code.
  1. Before we move on, implement a transformError function to handle mixed HttpErrorResponse and string errors in an observable stream under common/common.ts:
src/app/common/common.ts
import { HttpErrorResponse } from '@angular/common/http'
import { throwError } from 'rxjs'

export function transformError(error: HttpErrorResponse | string) {
let errorMessage = 'An unknown error has occurred'
if (typeof error === 'string') {
errorMessage = error
} else if (error.error instanceof ErrorEvent) {
errorMessage = `Error! ${error.error.message}`
} else if (error.status) {
errorMessage = `Request failed with ${error.status} ${error.statusText}`
}
return throwError(errorMessage)
}
  1. Implement the login function that will be called from LoginComponent, shown in the next section
  2. Add import { transformError } from '../common/common'

 

  1. Also implement a corresponding logout function, which may be called by the Logout button in the top toolbar, a failed login attempt, or if an unauthorized access attempt is detected by a router auth guard as the user is navigating the app, which is a topic covered later in the chapter:
src/app/auth/auth.service.ts
...
login(email: string, password: string): Observable<IAuthStatus> {
this.logout()

const loginResponse = this.authProvider(email, password).pipe(
map(value => {
return decode(value.accessToken) as IAuthStatus
}),
catchError(transformError)
)

loginResponse.subscribe(
res => {
this.authStatus.next(res)
},
err => {
this.logout()
return observableThrowError(err)
}
)

return loginResponse
}

logout() {
this.authStatus.next(defaultAuthStatus)
}
}

The login method encapsulates the correct order of operations by calling the logout method, the authProvider with the email and password information, and throwing errors when necessary.

The login method adheres to the Open/Closed principle, from SOLID design, by being open to extension by our ability to externally supply different auth providers to it, but it remains closed to modification, since the variance in functionality is encapsulated with the auth provider.

In the next section, we will implement the LoginComponent so that users can enter their username and password information and attempt a login.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.20.224.107