NgxMaterialAuth
Provides functionality around authentication and authorization in angular.
This includes:
- A generic JwtAuthService that can easily be extended from
- Multiple Guards and HttpInterceptors that work right out of the box
- Ready to work with but highly customizable components for login, reset-password etc.
Table of Contents
Requirements
This package relies on the angular material library to render its components.
It also uses fontawesome-icons in some components.
JwtAuthService
The JwtAuthService provides functionality for most of the auth requirements:
- It handles syncing authentication data from and to localstorage
- It provides methods for login, logout, resetting the password, but also more advanced things like refreshing the current token
- Using generics you can change the type of the auth data and token data saved in local storage
It is also used in most of the other parts of the library.
In order to use it you need to extend your own service from it and register it in your app.module.ts provider array.
Usage
Define your AuthService
import { BaseAuthData, BaseToken, JwtAuthService } from 'ngx-material-auth';
@Injectable({ providedIn: 'root' })
export class CustomAuthService extends JwtAuthService<BaseAuthData, BaseToken> {
readonly API_LOGIN_URL: string = `${environment.apiUrl}/login`;
readonly API_REFRESH_TOKEN_URL: string = `${environment.apiUrl}/refresh-token`;
readonly API_REQUEST_RESET_PASSWORD_URL: string = `${environment.apiUrl}/request-reset-password`;
readonly API_CONFIRM_RESET_PASSWORD_URL: string = `${environment.apiUrl}/confirm-reset-password`;
readonly API_VERIFY_RESET_PASSWORD_TOKEN_URL: string = `${environment.apiUrl}/verify-password-reset-token`;
constructor(
private readonly httpClient: HttpClient,
private readonly matSnackBar: MatSnackBar,
private readonly ngZone: NgZone
) {
super(httpClient, matSnackBar, ngZone);
}
}
As you can see, you only have to override a few urls that are used in the services login/reset-password etc. methods.
You can however also customize all other parts of the JwtAuthService.
Override the injection token
Everything else is already dealt with, all parts of NgxMaterialAuth already use that service. Now you need to provide it to them by overriding the injection token. Add the following to your app.module.ts:
import { NGX_AUTH_SERVICE } from 'ngx-material-auth';
...
providers: [
...
{
provide: NGX_AUTH_SERVICE,
useExisting: CustomAuthService
}
...
],
...
That's it! Now you are ready to use all the parts NgxMaterialAuth has to offer:
Api
JwtAuthService
export abstract class JwtAuthService<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken
> {
authDataSubject: BehaviorSubject<AuthDataType | undefined>;
readonly AUTH_DATA_KEY = 'authData';
readonly TOKEN_DURATION_IN_MS: number = DAY_IN_MS;
readonly REQUEST_RESET_PASSWORD_SNACK_BAR_MESSAGE: string = 'A Mail for changing your password is on its way';
readonly CONFIRM_RESET_PASSWORD_SNACK_BAR_MESSAGE: string = 'Password changed successfully!';
abstract readonly API_LOGIN_URL: string;
abstract readonly API_REFRESH_TOKEN_URL: string;
abstract readonly API_REQUEST_RESET_PASSWORD_URL: string;
abstract readonly API_CONFIRM_RESET_PASSWORD_URL: string;
abstract readonly API_VERIFY_RESET_PASSWORD_TOKEN_URL: string;
get authData(): AuthDataType | undefined {
return this.authDataSubject.value;
}
set authData(authData: AuthDataType | undefined) {
localStorage.setItem(this.AUTH_DATA_KEY, JSON.stringify(authData));
this.authDataSubject.next(authData);
}
constructor(
private readonly http: HttpClient,
private readonly snackbar: MatSnackBar,
private readonly zone: NgZone
) {
const stringData = localStorage.getItem(this.AUTH_DATA_KEY);
const authData = stringData ? JSON.parse(stringData) as AuthDataType : undefined;
this.authDataSubject = new BehaviorSubject(authData);
}
async login(loginData: LoginData): Promise<AuthDataType> {
this.authData = await firstValueFrom(this.http.post<AuthDataType>(this.API_LOGIN_URL, loginData));
return this.authData;
}
logout(): void {
localStorage.removeItem(this.AUTH_DATA_KEY);
this.authDataSubject.next(null as unknown as AuthDataType);
}
async refreshToken(): Promise<void> {
if (!this.authData) {
return;
}
const newInvalidAfter = new Date();
newInvalidAfter.setMilliseconds(newInvalidAfter.getMilliseconds() + this.TOKEN_DURATION_IN_MS);
this.authData.token.expirationDate = newInvalidAfter;
this.authData = this.authData;
const token: TokenType = await firstValueFrom(this.http.post<TokenType>(this.API_REFRESH_TOKEN_URL, {}));
this.authData.token = token;
this.authData = this.authData;
}
async requestResetPassword(email: string): Promise<void> {
await firstValueFrom(this.http.post<void>(this.API_REQUEST_RESET_PASSWORD_URL, { email: email }));
this.zone.run(() => {
this.snackbar.open(this.REQUEST_RESET_PASSWORD_SNACK_BAR_MESSAGE);
});
}
async confirmResetPassword(newPassword: string, resetToken: string): Promise<void> {
const body = {
password: newPassword,
token: resetToken
};
await firstValueFrom(this.http.post<void>(this.API_CONFIRM_RESET_PASSWORD_URL, body));
this.zone.run(() => {
this.snackbar.open(this.CONFIRM_RESET_PASSWORD_SNACK_BAR_MESSAGE);
});
}
async isResetTokenValid(resetToken: string): Promise<boolean> {
return await firstValueFrom(
this.http.post<boolean>(this.API_VERIFY_RESET_PASSWORD_TOKEN_URL, { token: resetToken })
);
}
hasRole(allowedRoles: Role[]): boolean {
if (!this.authData) {
return false;
}
if (allowedRoles.find(r => this.authData?.roles.includes(r))) {
return true;
}
return false;
}
}
BaseToken
Can be used either directly or be extended from if your token has additional values.
export interface BaseToken {
value: string,
expirationDate: Date
}
BaseAuthData
Can be used either directly or be extended from if your authData has additional values.
export interface BaseAuthData<Token extends BaseToken> {
token: Token,
roles: Role[],
userId: string
}
Jwt Interceptor
This can be used straight out of the box if you have registered your authService.
This interceptor automatically adds the token from your AuthService to the authorization http header of every request (If a token exists).
It also handles the refreshing of your token if it going to run out. By default starting at 6 hours before the token expirationDate.
Usage
Add this to your app.module.ts:
...
providers: [
...
{
provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true
}
...
]
...
Api
@Injectable({ providedIn: 'root' })
export class JwtInterceptor<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements HttpInterceptor {
protected readonly MAXIMUM_MS_BEFORE_EXPIRATION_FOR_REFRESH: number = SIX_HOURS_IN_MS;
constructor(
@Inject(NGX_AUTH_SERVICE)
private readonly authService: AuthServiceType
) { }
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!this.authService.authData?.token) {
return next.handle(request);
}
if (this.tokenNeedsToBeRefreshed()) {
void this.authService.refreshToken();
}
request = request.clone({
setHeaders: {
authorization: `Bearer ${this.authService.authData.token.value}`
}
});
return next.handle(request);
}
protected tokenNeedsToBeRefreshed(): boolean {
const tokenExpirationDate: Date = new Date(this.authService.authData?.token.expirationDate as Date);
const expirationInMs: number = tokenExpirationDate.getTime();
return (expirationInMs - Date.now()) <= this.MAXIMUM_MS_BEFORE_EXPIRATION_FOR_REFRESH;
}
}
HTTP-Error Interceptor
This can be used straight out of the box if you have registered your authService.
This interceptor catches any error that comes from http and displays it inside of an dialog.
If the error has a specific status code (eg. 401 Unauthorized) the current user is logged out.
:warning:
This does not handle CORS-Errors as these come directly from the browser.
Usage
Add this to your app.module.ts:
...
providers: [
...
{
provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true
}
...
]
...
Api
@Injectable({ providedIn: 'root' })
export class HttpErrorInterceptor<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements HttpInterceptor {
protected readonly ROUTE_AFTER_LOGOUT = '/';
protected readonly logoutStatuses: number[] = [
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden
];
protected readonly apiUrlsWithNoLogout: string[] = [];
constructor(
protected readonly router: Router,
@Inject(NGX_AUTH_SERVICE)
protected readonly authService: AuthServiceType,
protected readonly dialog: MatDialog
) { }
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (this.userShouldBeLoggedOut(error, request)) {
this.authService.logout();
void this.router.navigate([this.ROUTE_AFTER_LOGOUT], {});
}
if (this.errorDialogShouldBeDisplayed(error, request)) {
const errorData: ErrorData = { name: 'HTTP-Error', message: this.getErrorDataMessage(error) };
this.dialog.open(ErrorDialogComponent, { data: errorData, autoFocus: false, restoreFocus: false });
}
return throwError(() => error);
})
);
}
protected userShouldBeLoggedOut(error: HttpErrorResponse, request: HttpRequest<unknown>): boolean {
if (this.apiUrlsWithNoLogout.find(url => url === request.url)) {
return false;
}
return !!this.logoutStatuses.find(s => s === error.status);
}
protected errorDialogShouldBeDisplayed(error: HttpErrorResponse, request: HttpRequest<unknown>): boolean {
if (request.url === this.authService.API_REFRESH_TOKEN_URL) {
return false;
}
return true;
}
protected getErrorDataMessage(error: HttpErrorResponse): string {
if (error.error != null) {
return this.getErrorDataMessage(error.error as HttpErrorResponse);
}
if (typeof error === 'string') {
return error as string;
}
if (error.message) {
return error.message;
}
return JSON.stringify(error);
}
}
JwtLoggedInGuard
This can be used straight out of the box if you have registered your authService.
A guard that simply checks if the user is logged in or not.
Usage
Just add the guard to any route that you want to protect:
canActivate: [JwtLoggedInGuard]
Api
@Injectable({ providedIn: 'root' })
export class JwtLoggedInGuard<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements CanActivate {
protected readonly REDIRECT_ROUTE = '/login';
constructor(
private readonly router: Router,
@Inject(NGX_AUTH_SERVICE)
private readonly authService: AuthServiceType
) { }
canActivate(): boolean {
if (this.authService.authData == null) {
this.authService.logout();
void this.router.navigate([this.REDIRECT_ROUTE], {});
return false;
}
return true;
}
}
JwtNotLoggedInGuard
This can be used straight out of the box if you have registered your authService.
This is just the inverse of the JwtLoggedInGuard.
Usage
Just add the guard to any route that you want to protect:
canActivate: [JwtLoggedInGuard]
Api
@Injectable({ providedIn: 'root' })
export class JwtNotLoggedInGuard<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements CanActivate {
protected readonly REDIRECT_ROUTE = '/';
constructor(
protected readonly router: Router,
@Inject(NGX_AUTH_SERVICE)
protected readonly authService: AuthServiceType
) { }
canActivate(): boolean {
if (this.authService.authData != null) {
void this.router.navigate([this.REDIRECT_ROUTE], {});
return false;
}
return true;
}
}
JwtRoleGuard
This can be used straight out of the box if you have registered your authService.
A guard that checks if the logged in user has one of the allowed roles.
Tries to get the allowed roles from the routes data object by default.
Usage
Just add the guard to any route that you want to protect:
canActivate: [JwtRoleGuard]
Api
@Injectable({ providedIn: 'root' })
export class JwtRoleGuard<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements CanActivate {
protected readonly REDIRECT_ROUTE = '/login';
constructor(
private readonly router: Router,
@Inject(NGX_AUTH_SERVICE)
private readonly authService: AuthServiceType
) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const allowedRoles = this.getAllowedRolesForRoute(route, state);
if (!this.authService.hasRole(allowedRoles)) {
this.authService.logout();
void this.router.navigate([this.REDIRECT_ROUTE], {});
return false;
}
return true;
}
protected getAllowedRolesForRoute(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Role[] {
return route.data['allowedRoles'] as Role[] ?? [];
}
}
JwtBelongsToGuard
Contains base functionality to check if a user is associated with the provided route.
Needs to be overriden.
:warning:
If the accessed route already requests some data from the api which throws an Unauthorized/Forbidden Error
this can also be handled by the http-error-interceptor.
Usage
Implement your BelongsToGuard
Here you only need to override the method that decides if a user belongs to a route or not.
import { JwtBelongsToGuard } from 'ngx-material-auth';
@Injectable({ providedIn: 'root' })
export class BelongsToUserGuard extends JwtBelongsToGuard<CustomAuthData, CustomToken, CustomAuthService> {
constructor(
private readonly angularRouter: Router,
private readonly customAuthService: CustomAuthService
) {
super(angularRouter, customAuthService);
}
protected getBelongsToForRoute(route: ActivatedRouteSnapshot): boolean {
}
}
Add it to your route
Just add the guard to any route that you want to protect:
canActivate: [BelongsToUserGuard]
Api
export abstract class JwtBelongsToGuard<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements CanActivate {
protected readonly REDIRECT_ROUTE = '/login';
constructor(
private readonly router: Router,
private readonly authService: AuthServiceType
) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (!this.getBelongsToForRoute(route, state)) {
this.authService.logout();
void this.router.navigate([this.REDIRECT_ROUTE], {});
return false;
}
return true;
}
protected abstract getBelongsToForRoute(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean;
}
NgxMatAuthLoginComponent
This can be used straight out of the box if you have registered your authService.
This component provides a simple login box which is highly customizable.
It uses the login method of your auth service.
Usage
- Import NgxMatAuthLoginModule
- Use in your html:
<ngx-mat-auth-login [loginTitle]="'Custom Login'"></ngx-mat-auth-login>
Api
@Component({
selector: 'ngx-mat-auth-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class NgxMatAuthLoginComponent<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements OnInit {
@Input()
getValidationErrorMessage!: (model: NgModel) => string;
@Input()
loginTitle!: string;
@Input()
emailInputLabel!: string;
@Input()
passwordInputLabel!: string;
@Input()
loginButtonLabel!: string;
@Input()
forgotPasswordLinkData!: ForgotPasswordLinkData;
@Input()
routeAfterLogin!: string;
password?: string;
email?: string;
hide: boolean = true;
constructor(
@Inject(NGX_AUTH_SERVICE)
private readonly authService: AuthServiceType,
private readonly router: Router
) { }
ngOnInit(): void {
this.getValidationErrorMessage = this.getValidationErrorMessage ?? getValidationErrorMessage;
this.loginTitle = this.loginTitle ?? 'Login';
this.emailInputLabel = this.emailInputLabel ?? 'Email';
this.passwordInputLabel = this.passwordInputLabel ?? 'Password';
this.loginButtonLabel = this.loginButtonLabel ?? 'Login';
this.forgotPasswordLinkData = this.forgotPasswordLinkData ?? { displayName: 'Forgot your password?', route: '/reset-password' };
this.routeAfterLogin = this.routeAfterLogin ?? '/';
}
onSubmit(): void {
if (!this.email || !this.password) {
return;
}
this.authService.login({ email: this.email, password: this.password })
.then(() => {
this.email = undefined;
this.password = undefined;
void this.router.navigate([this.routeAfterLogin]);
})
.catch(err => {
this.email = undefined;
this.password = undefined;
throw err;
});
}
}
NgxMatAuthRequestResetPasswordComponent
This can be used straight out of the box if you have registered your authService.
This component provides a simple box for users to request the reset of their password.
It uses the requestResetPassword-method of your auth service.
Usage
- Import NgxMatAuthRequestResetPasswordModule
- Use in your html:
<ngx-mat-auth-request-reset-password [requestResetPasswordTitle]="'Custom'">
</ngx-mat-auth-request-reset-password>
Api
@Component({
selector: 'ngx-mat-auth-request-reset-password',
templateUrl: './request-reset-password.component.html',
styleUrls: ['./request-reset-password.component.scss']
})
export class NgxMatAuthRequestResetPasswordComponent<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements OnInit {
@Input()
getValidationErrorMessage!: (model: NgModel) => string;
@Input()
requestResetPasswordTitle!: string;
@Input()
emailInputLabel!: string;
@Input()
sendEmailButtonLabel!: string;
@Input()
cancelButtonLabel!: string;
@Input()
routeAfterRequest!: string;
email?: string;
constructor(
@Inject(NGX_AUTH_SERVICE)
protected readonly authService: AuthServiceType,
protected readonly router: Router
) { }
ngOnInit(): void {
this.requestResetPasswordTitle = this.requestResetPasswordTitle ?? 'Forgot Password';
this.getValidationErrorMessage = this.getValidationErrorMessage ?? getValidationErrorMessage;
this.emailInputLabel = this.emailInputLabel ?? 'Email';
this.sendEmailButtonLabel = this.sendEmailButtonLabel ?? 'Send Email';
this.cancelButtonLabel = this.cancelButtonLabel ?? 'Cancel';
this.routeAfterRequest = this.routeAfterRequest ?? '/login';
}
cancel(): void {
void this.router.navigate([this.routeAfterRequest]);
}
onSubmit(): void {
if (!this.email) {
return;
}
this.authService.requestResetPassword(this.email)
.then(() => {
this.email = undefined;
void this.router.navigate([this.routeAfterRequest]);
})
.catch(err => {
this.email = undefined;
throw err;
});
}
}
NgxMatAuthConfirmResetPasswordComponent
This can be used straight out of the box if you have registered your authService.
This component provides a simple box for users input their new password after it has been requested.
This also checks if the provided reset token is correct. The reset token needs to be available from route.params['token']
.
It uses the requestResetPassword-method of your auth service.
Usage
- Import NgxMatAuthConfirmResetPasswordModule
- Use in your html:
<ngx-mat-auth-confirm-reset-password [confirmResetPasswordTitle]="'Custom Title'">
</ngx-mat-auth-confirm-reset-password>
Api
@Component({
selector: 'ngx-mat-auth-confirm-reset-password',
templateUrl: './confirm-reset-password.component.html',
styleUrls: ['./confirm-reset-password.component.scss']
})
export class NgxMatAuthConfirmResetPasswordComponent<
AuthDataType extends BaseAuthData<TokenType>,
TokenType extends BaseToken,
AuthServiceType extends JwtAuthService<AuthDataType, TokenType>
> implements OnInit {
@Input()
getValidationErrorMessage!: (model: NgModel) => string;
@Input()
confirmResetPasswordTitle!: string;
@Input()
passwordInputLabel!: string;
@Input()
confirmPasswordInputLabel!: string;
@Input()
changePasswordButtonLabel!: string;
@Input()
cancelButtonLabel!: string;
@Input()
routeForCancel!: string;
@Input()
routeAfterReset!: string;
@Input()
routeIfResetTokenInvalid!: string;
@Input()
invalidResetTokenErrorData!: ErrorData;
password?: string;
confirmPassword?: string;
hide: boolean = true;
hideConfirm: boolean = true;
private resetToken?: string;
private readonly defaultInvalidResetTokenErrorData: ErrorData = {
name: 'Error',
message: '<p>The provided link is no longer active.</p><p>Please check if the url is correct or request a new link.</p>'
};
constructor(
@Inject(NGX_AUTH_SERVICE)
protected readonly authService: AuthServiceType,
protected readonly router: Router,
protected readonly route: ActivatedRoute,
protected readonly zone: NgZone,
protected readonly dialog: MatDialog
) { }
async ngOnInit(): Promise<void> {
this.initDefaultValues();
this.resetToken = (await firstValueFrom(this.route.params))['token'] as string | undefined;
if (
!this.resetToken
|| !(await this.authService.isResetTokenValid(this.resetToken))
) {
await this.router.navigate([this.routeIfResetTokenInvalid]);
this.zone.run(() => {
this.dialog.open(ErrorDialogComponent, { data: this.invalidResetTokenErrorData, autoFocus: false, restoreFocus: false });
});
return;
}
}
private initDefaultValues(): void {
this.getValidationErrorMessage = this.getValidationErrorMessage ?? getValidationErrorMessage;
this.confirmResetPasswordTitle = this.confirmResetPasswordTitle ?? 'New Password';
this.passwordInputLabel = this.passwordInputLabel ?? 'Password';
this.confirmPasswordInputLabel = this.confirmPasswordInputLabel ?? 'Confirm Password';
this.changePasswordButtonLabel = this.changePasswordButtonLabel ?? 'Change Password';
this.cancelButtonLabel = this.cancelButtonLabel ?? 'Cancel';
this.routeAfterReset = this.routeAfterReset ?? '/login';
this.routeIfResetTokenInvalid = this.routeIfResetTokenInvalid ?? '/';
this.routeForCancel = this.routeForCancel ?? this.routeAfterReset;
this.invalidResetTokenErrorData = this.invalidResetTokenErrorData ?? this.defaultInvalidResetTokenErrorData;
}
inputInvalid(): boolean {
if (!this.password) {
return true;
}
if (this.password !== this.confirmPassword) {
return true;
}
return false;
}
cancel(): void {
void this.router.navigate([this.routeForCancel]);
}
onSubmit(): void {
if (!this.password) {
return;
}
if (this.password !== this.confirmPassword) {
return;
}
this.authService.confirmResetPassword(this.password, this.resetToken as string)
.then(() => {
this.resetInputFields();
void this.router.navigate([this.routeAfterReset]);
})
.catch(() => {
this.resetInputFields();
void this.router.navigate([this.routeAfterReset]);
});
}
private resetInputFields(): void {
this.password = '';
this.confirmPassword = '';
this.resetToken = '';
}
}
NgxMatAuthErrorDialogComponent
This can be used straight out of the box.
This component provides a generic dialog to display error messages.
It is used internally by the framework eg. to display error messages from the http-error-interceptor.
Error Data
You need to provide an errorData-object to the dialog in order for it to work.
This is a really simple model:
export interface ErrorData {
name: string,
message: string
Usage
Wherever you want to display the dialog:
import { NgxMatAuthErrorDialogComponent, ErrorData } from 'ngx-material-auth';
...
constructor(
private readonly dialog: MatDialog
) { }
...
this.dialog.open(NgxMatAuthErrorDialogComponent, { data: errorData });
Api
@Component({
selector: 'ngx-material-auth-error-dialog',
templateUrl: './error-dialog.component.html',
styleUrls: ['./error-dialog.component.scss'],
standalone: true,
imports: [
MatButtonModule
]
})
export class NgxMatAuthErrorDialogComponent {
constructor(
public dialogRef: MatDialogRef<NgxMatAuthErrorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public error: ErrorData,
) { }
close(): void {
this.dialogRef.close();
}
}