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.
- NgxMaterialAuth
- Table of Contents
- Requirements
- JwtAuthService
- Jwt Interceptor
- HTTP-Error Interceptor
- Two Factor Interceptor
- JwtLoggedInGuard
- JwtNotLoggedInGuard
- JwtRoleGuard
- JwtBelongsToGuard
- NgxMatAuthLoginComponent
- NgxMatAuthRequestResetPasswordComponent
- NgxMatAuthConfirmResetPasswordComponent
- NgxMatAuthErrorDialogComponent
This package relies on the angular material library to render its components.
It also uses fontawesome-icons in some components.
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.
import{BaseAuthData,BaseToken,JwtAuthService}from'ngx-material-auth'; @Injectable({providedIn: 'root'})// ↓ Can be customized ↓exportclassCustomAuthServiceextendsJwtAuthService<BaseAuthData,CustomRoleValues,CustomRole<CustomRoleValues>,CustomToken>{readonlyAPI_LOGIN_URL: string=`${environment.apiUrl}/login`;readonlyAPI_LOGOUT_URL: string=`${environment.apiUrl}/logout`;readonlyAPI_REFRESH_TOKEN_URL: string=`${environment.apiUrl}/refresh-token`;readonlyAPI_REQUEST_RESET_PASSWORD_URL: string=`${environment.apiUrl}/request-reset-password`;readonlyAPI_CONFIRM_RESET_PASSWORD_URL: string=`${environment.apiUrl}/confirm-reset-password`;readonlyAPI_VERIFY_RESET_PASSWORD_TOKEN_URL: string=`${environment.apiUrl}/verify-password-reset-token`;constructor(privatereadonlyhttpClient: HttpClient,privatereadonlymatSnackBar: MatSnackBar,privatereadonlyngZone: 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.
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:
/** * The base class for an authentication service. */exportabstractclassJwtAuthService<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,TokenTypeextendsBaseToken>{/** * The subject of the currently stored authData. */authDataSubject: BehaviorSubject<AuthDataType|undefined>;/** * The key for the authData saved in local storage. */readonlyAUTH_DATA_KEY='authData';/** * The duration of the access token in milliseconds. * * @default 3600000 // 1 hour */readonlyACCESS_TOKEN_DURATION_IN_MS: number=HOUR_IN_MS;/** * The duration of the refresh token in milliseconds. * * @default 8640000000 // 100 days */readonlyREFRESH_TOKEN_DURATION_IN_MS: number=ONE_HUNDRED_DAYS_IN_MS;/** * The message to display inside a snackbar when the mail for resetting a password was sent successfully. */readonlyREQUEST_RESET_PASSWORD_SNACK_BAR_MESSAGE: string='A Mail for changing your password is on its way';/** * The message to display inside a snackbar when password was reset successfully. */readonlyCONFIRM_RESET_PASSWORD_SNACK_BAR_MESSAGE: string='Password changed successfully!';/** * The default url for login requests. */abstractreadonlyAPI_LOGIN_URL: string;/** * The default url for logout requests. */abstractreadonlyAPI_LOGOUT_URL: string;/** * The default url for refresh token requests. */abstractreadonlyAPI_REFRESH_TOKEN_URL: string;/** * The default url for request reset password requests. */abstractreadonlyAPI_REQUEST_RESET_PASSWORD_URL: string;/** * The default url for confirm reset password requests. */abstractreadonlyAPI_CONFIRM_RESET_PASSWORD_URL: string;/** * The default url for verify password reset token requests. */abstractreadonlyAPI_VERIFY_RESET_PASSWORD_TOKEN_URL: string;// eslint-disable-next-line jsdoc/require-returns/** * The currently stored authData value. * Contains at least the token. */getauthData(): AuthDataType|undefined{returnthis.authDataSubject.value;}// eslint-disable-next-line jsdoc/require-jsdocsetauthData(authData: AuthDataType|undefined){authData=this.transformAuthDataBeforeSetting(authData);localStorage.setItem(this.AUTH_DATA_KEY,JSON.stringify(authData));if(!authData){localStorage.removeItem(this.AUTH_DATA_KEY);}this.authDataSubject.next(authData);}constructor(protectedreadonlyhttp: HttpClient,protectedreadonlysnackbar: MatSnackBar,protectedreadonlyzone: NgZone){conststringData=localStorage.getItem(this.AUTH_DATA_KEY);constauthData=stringData ? JSON.parse(stringData)asAuthDataType : undefined;this.authDataSubject=newBehaviorSubject(authData);}/** * Gets called right before auth data is set. * Can be used to transform some of the data. * * DEFAULT: When the api sends roles as a list of strings instead of Role objects, * they are transformed to role objects with displayName and value being the string send by the api. * * @param authData - The auth data that should be set. * @returns The transformed auth data or undefined. */protectedtransformAuthDataBeforeSetting(authData: AuthDataType|undefined): AuthDataType|undefined{if(!authData){returnundefined;}if(typeofauthData.roles[0]==='string'){authData.roles=(authData.rolesasunknownasRoleValue[]).map(r=>{return{displayName: r,value: r};})asunknownasRole[];}returnauthData;}/** * Login a user. * * @param loginData - The data that is sent to the server to login the user. * @returns A promise of the received authData. */asynclogin(loginData: LoginData): Promise<AuthDataType>{this.authData=awaitfirstValueFrom(this.http.post<AuthDataType>(this.API_LOGIN_URL,loginData));returnthis.authData;}/** * Logout the current user. */asynclogout(): Promise<void>{if(!this.authData){return;}awaitfirstValueFrom(this.http.post<void>(this.API_LOGOUT_URL,{refreshToken: this.authData.refreshToken.value}));this.authData=undefined;}/** * Refreshes the token. * No data is sent to the server as the jwt in the header already contains the necessary information. */asyncrefreshToken(): Promise<void>{if(!this.authData){return;}this.authData=awaitfirstValueFrom(this.http.post<AuthDataType>(this.API_REFRESH_TOKEN_URL,{refreshToken: this.authData.refreshToken}));}/** * Requests a new password from the server. * Should sent a reset-link to the given email with a one time short lived (~5 minutes) token. * * @param email - The email of the user that wants to reset his password. */asyncrequestResetPassword(email: string): Promise<void>{awaitfirstValueFrom(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);});}/** * Confirms the reset of the password. * * @param newPassword - The new password. * @param resetToken - The token from the email. Needed to authorize the password reset. */asyncconfirmResetPassword(newPassword: string,resetToken: string): Promise<void>{constbody={password: newPassword,resetToken: resetToken};awaitfirstValueFrom(this.http.post<void>(this.API_CONFIRM_RESET_PASSWORD_URL,body));this.zone.run(()=>{this.snackbar.open(this.CONFIRM_RESET_PASSWORD_SNACK_BAR_MESSAGE);});}/** * Checks if the given reset token is valid. * * @param resetToken - The token from the email. Needed to authorize the password reset. * @returns Whether or not the given token is valid. */asyncisResetTokenValid(resetToken: string): Promise<boolean>{returnawaitfirstValueFrom(this.http.post<boolean>(this.API_VERIFY_RESET_PASSWORD_TOKEN_URL,{value: resetToken}));}/** * Checks whether or not the currently logged in user has one of the provided roles. * * @param allowedRolesValues - All roles that are allowed to do a certain thing. * @returns Whether or not the user has one of the provided allowed roles. */hasRole(allowedRolesValues: RoleValue[]): boolean{if(!this.authData){returnfalse;}if(allowedRolesValues.find(rv=>this.authData?.roles.map(r=>r.value).includes(rv))){returntrue;}returnfalse;}}Can be used either directly or be extended from if your token has additional values.
/** * The minimum values for a token. */exportinterfaceBaseToken{/** * The token itself. */value: string,/** * The timestamp at which the token becomes invalid. * Is needed to determine if the token needs to be refreshed. */expirationDate: Date}Can be used either directly or be extended from if your authData has additional values.
/** * The minimum values for authData. */exportinterfaceBaseAuthData<TokenextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>>{/** * The access token used for authenticating requests. * Consists of the string value and the expiration date. */accessToken: Token,/** * The refresh token used for refreshing access tokens. * Consists of the string value and the expiration date. */refreshToken: Token,/** * All roles of the currently logged in user. * Consists of an displayName and the actual string value. */roles: Role[],/** * The id of the currently logged in user. */userId: string}Can be used either directly or be extended from if your roles have additional values.
/** * Provides base information about a user role. */exportinterfaceBaseRole<RoleValueextendsstring>{/** * The name of the role which can be used to display it in the ui. * This is NOT used to determine if the user can access certain thing. */displayName: string,/** * The actual string value of the role. * This is used to determine whether or not the user can access certain things. */value: RoleValue}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). You can (and should) provide a list of allowed domains to prohibit accidentaly sending tokens to a third party api.
It also handles the refreshing of your token if it going to run out. By default starting at 6 hours before the token expirationDate.
Add this to your app.module.ts:
import{NGX_JWT_INTERCEPTOR_ALLOWED_DOMAINS}from'ngx-material-auth'; ... providers: [ ... {provide: HTTP_INTERCEPTORS,useClass: JwtInterceptor,multi: true},{// This is optional but highly recommended.provide: NGX_JWT_INTERCEPTOR_ALLOWED_DOMAINS,useValue: ['localhost:3000','example.com/api']}, ... ] .../** * Interceptor that sets the authorization header to the current token on every request. * Does nothing when the user isn't logged in. */ @Injectable({providedIn: 'root'})exportclassJwtInterceptor<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsHttpInterceptor{constructor( @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType, @Inject(NGX_JWT_INTERCEPTOR_ALLOWED_DOMAINS)protectedreadonlyallowedDomains?: string[]){this.allowedDomains=this.allowedDomains?.map(ad=>this.getDomainFromUrl(ad));}/** * The main method used by angular to intercept any http-requests and append the jwt. * * @param request - The http-request that was intercepted. * @param next - The next http-handler in angular's chain. * @returns An Observable that is used by angular in the intercept chain. */intercept(request: HttpRequest<unknown>,next: HttpHandler): Observable<HttpEvent<unknown>>{if(this.refreshTokenExpired()){this.authService.authData=undefined;}if(!this.authService.authData?.accessToken){returnnext.handle(request);}if(this.requestIsToDisallowedDomain(request)){returnnext.handle(request);}if(this.requestDoesNotRequireToken(request)){returnnext.handle(request);}if(this.tokenNeedsToBeRefreshed()){returnfrom(this.refreshAndHandle(request,next));}request=request.clone({setHeaders: {authorization: `Bearer ${this.authService.authData.accessToken.value}`}});returnnext.handle(request);}/** * Check if the intercepted request is one of the special cases where no token is required. * By default these are the refresh and the logout request. * * @param request - The http-request that was intercepted. * @returns Whether or not the intercepted request is one of the special cases where no token is required. */protectedrequestDoesNotRequireToken(request: HttpRequest<unknown>): boolean{returnrequest.url===this.authService.API_REFRESH_TOKEN_URL||request.url===this.authService.API_LOGOUT_URL;}/** * Refreshes the token synchronous and sends the request afterwards. * * @param request - The http-request that was intercepted. * @param next - The next http-handler in angular's chain. * @returns A promise of an unknown HttpEvent. Inside the interceptor you need to call "return from(this.refreshAndHandle(...))". */protectedasyncrefreshAndHandle(request: HttpRequest<unknown>,next: HttpHandler): Promise<HttpEvent<unknown>>{awaitthis.authService.refreshToken();request=request.clone({setHeaders: {authorization: `Bearer ${this.authService.authData?.accessToken.value}`}});returnawaitlastValueFrom(next.handle(request));}/** * Checks whether or not the token needs to be refreshed. * * @returns Whether or not the token needs to be refreshed. */protectedtokenNeedsToBeRefreshed(): boolean{consttokenExpirationDate: Date=newDate(this.authService.authData?.accessToken.expirationDateasDate);constexpirationInMs: number=tokenExpirationDate.getTime();returnexpirationInMs<=Date.now();}/** * Checks whether or not the refresh token is expired. * * @returns Whether or not the refresh token is expired. */protectedrefreshTokenExpired(): boolean{consttokenExpirationDate: Date=newDate(this.authService.authData?.refreshToken.expirationDateasDate);constexpirationInMs: number=tokenExpirationDate.getTime();returnexpirationInMs<=Date.now();}/** * Checks if the request is to an allowed domain. * * @param request - The request to check. * @returns Whether the request is to an allowed domain or not. Defaults to true if no allowed host names were provided. */protectedrequestIsToDisallowedDomain(request: HttpRequest<unknown>): boolean{if(!this.allowedDomains){returnfalse;}constdomain=this.getDomainFromUrl(request.url);if(this.allowedDomains.includes(domain)){returnfalse;}returntrue;}/** * Gets a normalized domain from an url. * Is used for comparing the request url with the allowed domains array. * * @param url - The url to get the domain from. * @returns The domain of the url. */protectedgetDomainFromUrl(url: string): string{if(url.startsWith('https://')){url=url.split('https://')[1];}if(url.startsWith('http://')){url=url.split('http://')[1];}if(url.startsWith('www.')){url=url.split('www.')[1];}url=url.split('/')[0];returnurl;}}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.
Add this to your app.module.ts:
... providers: [ ... {provide: HTTP_INTERCEPTORS,useClass: HttpErrorInterceptor,multi: true}...].../** * Interceptor that does error handling for http requests. */ @Injectable({providedIn: 'root'})exportclassHttpErrorInterceptor<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsHttpInterceptor{/** * The route to which the user gets redirected to after he triggers an error which should log him out (eg. 401 Unauthorized). */protectedreadonlyROUTE_AFTER_LOGOUT='/';/** * The message to display when the user has no internet connection. */protectedreadonlyNO_INTERNET_CONNECTION_ERROR_MESSAGE='No Internet Connection.<br>Please try again later.';/** * The message to display when an error with CORS occurs. */protectedreadonlyCORS_ERROR_MESSAGE='CORS Error<br>Check your console for more information.';/** * All error codes for which the user should be logged out. */protectedreadonlylogoutStatuses: number[]=[HttpStatusCode.Unauthorized,HttpStatusCode.Forbidden];/** * Any api urls for which the user shouldn't be logged out. * Eg. The login page. */protectedreadonlyapiUrlsWithNoLogout: string[]=[];constructor(protectedreadonlyrouter: Router, @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType,protectedreadonlydialog: MatDialog){}/** * The main method used by angular to intercept any http-requests with errors. * Displays an error message to the user and logs him out if requested. * * @param request - The http-request that was intercepted. * @param next - The next http-handler in angular's chain. * @returns An Observable that is used by angular in the intercept chain. */intercept(request: HttpRequest<unknown>,next: HttpHandler): Observable<HttpEvent<unknown>>{// eslint-disable-next-line @typescript-eslint/no-unsafe-returnreturnnext.handle(request).pipe(catchError((error: HttpErrorResponse)=>{if(this.userShouldBeLoggedOut(error,request)){voidthis.authService.logout().then(()=>{voidthis.router.navigate([this.ROUTE_AFTER_LOGOUT],{});});}if(this.errorDialogShouldBeDisplayed(error,request)){consterrorData: ErrorData={name: 'HTTP-Error',message: this.getErrorDataMessage(error)};this.dialog.open(NgxMatAuthErrorDialogComponent,{data: errorData,autoFocus: false,restoreFocus: false});}returnthrowError(()=>error);}));}/** * Checks if the user should be logged out after triggering the provided error. * * @param error - The http-error that came from the api. * @param request - Data about the request that caused the error. * @returns Whether or not the current user should be logged out. */protecteduserShouldBeLoggedOut(error: HttpErrorResponse,request: HttpRequest<unknown>): boolean{if(this.apiUrlsWithNoLogout.find(url=>url===request.url)){returnfalse;}return!!this.logoutStatuses.find(s=>s===error.status);}/** * Checks if an dialog for the given error should be displayed to the user. * * @param error - The http-error that was thrown. * @param request - Data about the request that caused the error. * @returns Whether or not an dialog should be displayed for the error. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotectederrorDialogShouldBeDisplayed(error: HttpErrorResponse,request: HttpRequest<unknown>): boolean{returntrue;}/** * Gets the message from the HttpError. * Prefers the most nested one. * * @param error - The http-error that was thrown. * @returns The message of the http-error. */protectedgetErrorDataMessage(error: HttpErrorResponse): string{if(error.error!=null){returnthis.getErrorDataMessage(error.errorasHttpErrorResponse);}if(typeoferror==='string'){returnerrorasstring;}if(error.message){returnerror.message;}if(this.isCORSError(error)){if(!window.navigator.onLine){returnthis.NO_INTERNET_CONNECTION_ERROR_MESSAGE;}returnthis.CORS_ERROR_MESSAGE;}returnJSON.stringify(error);}/** * Checks if the provided error has something to do with CORS. * * @param error - The error to check. * @returns Whether or not the provided error has something to do with CORS. */protectedisCORSError(error: HttpErrorResponse): boolean{conststringifiedError=JSON.stringify(error);returnstringifiedError===JSON.stringify({isTrusted: true})||stringifiedError===JSON.stringify({isTrusted: false});}}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.
Just add the guard to any route that you want to protect:
canActivate: [JwtLoggedInGuard]/** * Contains the necessary base information for an angular logged in guard. * Checks if the user is currently logged in. */ @Injectable({providedIn: 'root'})exportclassJwtLoggedInGuard<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsCanActivate{/** * When the user tries to access a route for which he doesn't have the permission and is logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_LOGOUT='/login';/** * When the user tries to access a route for which he doesn't have the permission but is NOT logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_REDIRECT='/';constructor(protectedreadonlyrouter: Router, @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType){}/** * The main method used by angular to determine if a user can access a certain route. * * @param route - The route that the user tries to access. * @param state - State data of the route. * @returns Whether or not the user can access the provided route. */canActivate(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{if(this.authService.authData==null){if(this.userShouldBeLoggedOut(route,state)){voidthis.authService.logout().then(()=>{voidthis.router.navigate([this.ROUTE_AFTER_LOGOUT],{});});}else{voidthis.router.navigate([this.ROUTE_AFTER_REDIRECT],{});}returnfalse;}returntrue;}/** * Defines whether or not the user should be logged out based on the route he tried to access. * * @param route - The route that the user failed to access. * @param state - The router state. * @returns Whether or not the user should be logged out. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotecteduserShouldBeLoggedOut(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{returntrue;}}This can be used straight out of the box if you have registered your authService.
This is just the inverse of the JwtLoggedInGuard.
Just add the guard to any route that you want to protect:
canActivate: [JwtNotLoggedInGuard]/** * Checks if the user is currently NOT logged in. * This can be useful if you want to disable already logged in user to access the login page etc. */ @Injectable({providedIn: 'root'})exportclassJwtNotLoggedInGuard<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,TokenTypeextendsBaseToken,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsCanActivate{/** * When the user tries to access a route for which he doesn't have the permission and is logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_LOGOUT='/login';/** * When the user tries to access a route for which he doesn't have the permission but is NOT logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_REDIRECT='/';constructor(protectedreadonlyrouter: Router, @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType){}/** * The main method used by angular to determine if a user can access a certain route. * * @param route - The route that the user tries to access. * @param state - State data of the route. * @returns Whether or not the user can access the provided route. */canActivate(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{if(this.authService.authData!=null){if(this.userShouldBeLoggedOut(route,state)){voidthis.authService.logout().then(()=>{voidthis.router.navigate([this.ROUTE_AFTER_LOGOUT],{});});}else{voidthis.router.navigate([this.ROUTE_AFTER_REDIRECT],{});}returnfalse;}returntrue;}/** * Defines whether or not the user should be logged out based on the route he tried to access. * * @param route - The route that the user failed to access. * @param state - The router state. * @returns Whether or not the user should be logged out. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotecteduserShouldBeLoggedOut(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{returntrue;}}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.
Just add the guard to any route that you want to protect:
canActivate: [JwtRoleGuard]/** * Contains the necessary base information for an angular role guard. * Checks if the currently logged in user has the role required for a specific route. */ @Injectable({providedIn: 'root'})exportclassJwtRoleGuard<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsCanActivate{/** * When the user tries to access a route for which he doesn't have the permission and is logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_LOGOUT='/login';/** * When the user tries to access a route for which he doesn't have the permission but is NOT logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_REDIRECT='/';constructor(protectedreadonlyrouter: Router, @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType){}/** * The main method used by angular to determine if a user can access a certain route. * * @param route - The route that the user tries to access. * @param state - State data of the route. * @returns Whether or not the user can access the provided route. */canActivate(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{constallowedRoles=this.getAllowedRoleValuesForRoute(route,state);if(!this.authService.hasRole(allowedRoles)){if(this.userShouldBeLoggedOut(route,state)){voidthis.authService.logout().then(()=>{voidthis.router.navigate([this.ROUTE_AFTER_LOGOUT],{});});}else{voidthis.router.navigate([this.ROUTE_AFTER_REDIRECT],{});}returnfalse;}returntrue;}/** * Gets all allowed roles for the provided route. * * By default this method tries to get these from the routes data property. * * @see https://angular.io/api/router/Route#data * @param route - The route that the user tries to navigate to. * @param state - State data of the route. * @returns The allowed roles for the provided route as an array. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotectedgetAllowedRoleValuesForRoute(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): RoleValue[]{returnroute.data['allowedRoles']asRoleValue[]??[];}/** * Defines whether or not the user should be logged out based on the route he tried to access. * * @param route - The route that the user failed to access. * @param state - The router state. * @returns Whether or not the user should be logged out. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotecteduserShouldBeLoggedOut(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{returntrue;}}Contains base functionality to check if a user is associated with the provided route.
Needs to be overriden.
⚠️
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.
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'})exportclassBelongsToUserGuardextendsJwtBelongsToGuard<CustomAuthData,CustomToken,CustomRoleValue,CustomRole<CustomRoleValue>,CustomAuthService>{constructor(privatereadonlyangularRouter: Router,privatereadonlycustomAuthService: CustomAuthService){super(angularRouter,customAuthService);}protectedgetBelongsToForRoute(route: ActivatedRouteSnapshot): boolean{// your custom logic here}}Just add the guard to any route that you want to protect:
canActivate: [BelongsToUserGuard]/** * Contains the necessary base information for an angular belongs to guard. * Checks if the user is in any way associated with the provided route. * * This can be useful when eg. The user is allowed to display his own user-profile but not the user-profiles of others. */exportabstractclassJwtBelongsToGuard<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsCanActivate{/** * When the user tries to access a route for which he doesn't have the permission and is logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_LOGOUT='/login';/** * When the user tries to access a route for which he doesn't have the permission but is NOT logged out * he gets redirected to this route afterwards. */protectedreadonlyROUTE_AFTER_REDIRECT='/';constructor(protectedreadonlyrouter: Router,protectedreadonlyauthService: AuthServiceType){}/** * The main method used by angular to determine if a user can access a certain route. * * @param route - The route that the user tries to access. * @param state - State data of the route. * @returns Whether or not the user can access the provided route. */canActivate(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{if(!this.getBelongsToForRoute(route,state)){if(this.userShouldBeLoggedOut(route,state)){voidthis.authService.logout().then(()=>{voidthis.router.navigate([this.ROUTE_AFTER_LOGOUT],{});});}else{voidthis.router.navigate([this.ROUTE_AFTER_REDIRECT],{});}returnfalse;}returntrue;}/** * Defines whether or not the user should be logged out based on the route he tried to access. * * @param route - The route that the user failed to access. * @param state - The router state. * @returns Whether or not the user should be logged out. */// eslint-disable-next-line @typescript-eslint/no-unused-varsprotecteduserShouldBeLoggedOut(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean{returntrue;}/** * Gets all allowed roles for the provided route. * * @param route - The route that the user tries to navigate to. * @param state - State data of the route. */protectedabstractgetBelongsToForRoute(route: ActivatedRouteSnapshot,state: RouterStateSnapshot): boolean;}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.
- Import NgxMatAuthLoginModule
- Use in your html:
<!-- All configuration is optional --><ngx-mat-auth-login[loginTitle]="'Custom Login'"></ngx-mat-auth-login>/** * A simple login box. */ @Component({selector: 'ngx-mat-auth-login',templateUrl: './login.component.html',styleUrls: ['./login.component.scss']})exportclassNgxMatAuthLoginComponent<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsOnInit{/** * (optional) A custom function to generate the error-message for invalid inputs. */ @Input()getValidationErrorMessage!: (model: NgModel)=>string;/** * A custom title of the login box. * * @default 'Login' */ @Input()loginTitle!: string;/** * A custom label for the email input. * * @default 'Email' */ @Input()emailInputLabel!: string;/** * A custom label for the password input. * * @default 'Password' */ @Input()passwordInputLabel!: string;/** * A custom label for the login button. * * @default 'Login' */ @Input()loginButtonLabel!: string;/** * Data for the forgot password link. * * @default{ * displayName: 'Forgot your password?', * route: '/reset-password' * } */ @Input()forgotPasswordLinkData!: ForgotPasswordLinkData;/** * The route to which the user gets redirected after he logs in successful. * * @default '/' */ @Input()routeAfterLogin!: string;/** * The password input by the user. */password?: string;/** * The email input by the user. */email?: string;/** * Whether or not the password input is hidden. */hide: boolean=true;constructor( @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType, @Inject(NGX_GET_VALIDATION_ERROR_MESSAGE)protectedreadonlydefaultGetValidationErrorMessage: (model: NgModel)=>string,protectedreadonlyrouter: Router){}ngOnInit(): void{this.getValidationErrorMessage=this.getValidationErrorMessage??this.defaultGetValidationErrorMessage;this.loginTitle=this.loginTitle??'Login';this.emailInputLabel=this.emailInputLabel??'Email';this.passwordInputLabel=this.passwordInputLabel??'Password';this.loginButtonLabel=this.loginButtonLabel??'Login';// eslint-disable-next-line max-lenthis.forgotPasswordLinkData=this.forgotPasswordLinkData??{displayName: 'Forgot your password?',route: '/request-reset-password'};this.routeAfterLogin=this.routeAfterLogin??'/';}/** * The method that gets called when the user tries to login. */onSubmit(): void{if(!this.email||!this.password){return;}this.authService.login({email: this.email,password: this.password}).then(()=>{this.email=undefined;this.password=undefined;voidthis.router.navigate([this.routeAfterLogin]);}).catch(err=>{this.email=undefined;this.password=undefined;throwerr;});}}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.
- Import NgxMatAuthRequestResetPasswordModule
- Use in your html:
<!-- All configuration is optional --><ngx-mat-auth-request-reset-password[requestResetPasswordTitle]="'Custom'"></ngx-mat-auth-request-reset-password>/** * A simple request reset password box. */ @Component({selector: 'ngx-mat-auth-request-reset-password',templateUrl: './request-reset-password.component.html',styleUrls: ['./request-reset-password.component.scss']})exportclassNgxMatAuthRequestResetPasswordComponent<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsOnInit{/** * (optional) A custom function to generate the error-message for invalid inputs. */ @Input()getValidationErrorMessage!: (model: NgModel)=>string;/** * The title of the request reset password box. * * @default 'Forgot Password' */ @Input()requestResetPasswordTitle!: string;/** * A custom label for the email input. * * @default 'Email' */ @Input()emailInputLabel!: string;/** * A custom label for the send email button. * * @default 'Send Email' */ @Input()sendEmailButtonLabel!: string;/** * A custom label for the cancel button. * * @default 'Cancel' */ @Input()cancelButtonLabel!: string;/** * The route to navigate to after the user successfully requests the reset of his password. * * @default '/login' */ @Input()routeAfterRequest!: string;/** * The email for the account which password should be reset. */email?: string;constructor( @Inject(NGX_AUTH_SERVICE)protectedreadonlyauthService: AuthServiceType, @Inject(NGX_GET_VALIDATION_ERROR_MESSAGE)protectedreadonlydefaultGetValidationErrorMessage: (model: NgModel)=>string,protectedreadonlyrouter: Router){}ngOnInit(): void{this.requestResetPasswordTitle=this.requestResetPasswordTitle??'Forgot Password';this.getValidationErrorMessage=this.getValidationErrorMessage??this.defaultGetValidationErrorMessage;this.emailInputLabel=this.emailInputLabel??'Email';this.sendEmailButtonLabel=this.sendEmailButtonLabel??'Send Email';this.cancelButtonLabel=this.cancelButtonLabel??'Cancel';this.routeAfterRequest=this.routeAfterRequest??'/login';}/** * Cancels the password reset. */cancel(): void{voidthis.router.navigate([this.routeAfterRequest]);}/** * Requests the reset of the password for the user with the given email. */onSubmit(): void{if(!this.email){return;}this.authService.requestResetPassword(this.email).then(()=>{this.email=undefined;voidthis.router.navigate([this.routeAfterRequest]);}).catch(err=>{this.email=undefined;throwerr;});}}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.
- Import NgxMatAuthConfirmResetPasswordModule
- Use in your html:
<!-- All configuration is optional --><ngx-mat-auth-confirm-reset-password[confirmResetPasswordTitle]="'Custom Title'"></ngx-mat-auth-confirm-reset-password>/** * The interface for the confirm reset password functionality. * * !!! * Also checks if the provided reset token is valid. * This tries to get the reset token from theActivatedRoute.params['token']. * You have to make sure that your reset password link and the routing makes that possible. * !!! */ @Component({selector: 'ngx-mat-auth-confirm-reset-password',templateUrl: './confirm-reset-password.component.html',styleUrls: ['./confirm-reset-password.component.scss']})exportclassNgxMatAuthConfirmResetPasswordComponent<AuthDataTypeextendsBaseAuthData<TokenType,RoleValue,Role>,TokenTypeextendsBaseToken,RoleValueextendsstring,RoleextendsBaseRole<RoleValue>,AuthServiceTypeextendsJwtAuthService<AuthDataType,RoleValue,Role,TokenType>>implementsOnInit{/** * (optional) A custom function to generate the error-message for invalid inputs. */ @Input()getValidationErrorMessage!: (model: NgModel)=>string;/** * The title of the confirm reset password box. * * @default 'New Password' */ @Input()confirmResetPasswordTitle!: string;/** * The label for the password input. * * @default 'Password' */ @Input()passwordInputLabel!: string;/** * The label for the confirm password input. * * @default 'Confirm Password' */ @Input()confirmPasswordInputLabel!: string;/** * The label for the change password button. * * @default 'Change Password' */ @Input()changePasswordButtonLabel!: string;/** * A custom label for the cancel button. * * @default 'Cancel' */ @Input()cancelButtonLabel!: string;/** * The route to which the user gets redirected when he clicks on the cancel button. * * @default routeAfterReset */ @Input()routeForCancel!: string;/** * The route to which the user gets redirected after the password has been changed successfully. * * @default '/login' */ @Input()routeAfterReset!: string;/** * The route to which the user gets redirected if the reset token is not correct. * * @default '/' */ @Input()routeIfResetTokenInvalid!: string;/** * The error data to display in an dialog when the provided reset token doesn't exist or is invalid. * * @default *{ * 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>' *} */ @Input()invalidResetTokenErrorData!: ErrorData;/** * The password input by the user. */password?: string;/** * The confirm password input by the user. */confirmPassword?: string;/** * Whether or not the password input is hidden. */hide: boolean=true;/** * Whether or not the confirm password input is hidden. */hideConfirm: boolean=true;privateresetToken?: string;privatereadonlydefaultInvalidResetTokenErrorData: 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)protectedreadonlyauthService: AuthServiceType, @Inject(NGX_GET_VALIDATION_ERROR_MESSAGE)protectedreadonlydefaultGetValidationErrorMessage: (model: NgModel)=>string,protectedreadonlyrouter: Router,protectedreadonlyroute: ActivatedRoute,protectedreadonlyzone: NgZone,protectedreadonlydialog: MatDialog){}asyncngOnInit(): Promise<void>{this.initDefaultValues();this.resetToken=(awaitfirstValueFrom(this.route.params))['token']asstring|undefined;if(!this.resetToken||!(awaitthis.authService.isResetTokenValid(this.resetToken))){awaitthis.router.navigate([this.routeIfResetTokenInvalid]);this.zone.run(()=>{this.dialog.open(NgxMatAuthErrorDialogComponent,{data: this.invalidResetTokenErrorData,autoFocus: false,restoreFocus: false});});return;}}privateinitDefaultValues(): void{this.getValidationErrorMessage=this.getValidationErrorMessage??this.defaultGetValidationErrorMessage;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;}/** * Checks if the user input is invalid. * * @returns If the user input is invalid. */inputInvalid(): boolean{if(!this.password){returntrue;}if(this.password!==this.confirmPassword){returntrue;}returnfalse;}/** * Cancels the password reset. */cancel(): void{voidthis.router.navigate([this.routeForCancel]);}/** * Changes the password. */onSubmit(): void{if(!this.password){return;}if(this.password!==this.confirmPassword){return;}this.authService.confirmResetPassword(this.password,this.resetTokenasstring).then(()=>{this.resetInputFields();voidthis.router.navigate([this.routeAfterReset]);}).catch(()=>{this.resetInputFields();voidthis.router.navigate([this.routeAfterReset]);});}privateresetInputFields(): void{this.password='';this.confirmPassword='';this.resetToken='';}}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.
You need to provide an errorData-object to the dialog in order for it to work. This is a really simple model:
/** * Data about an error. * Is used to display it inside a dialog. */exportinterfaceErrorData{/** * The name of the error. */name: string,/** * The message of the error. * This is treated as html. * * CAUTION: Some things are removed by the angular sanitizer. */message: stringWherever you want to display the dialog:
import{NgxMatAuthErrorDialogComponent,ErrorData}from'ngx-material-auth'; ... constructor(privatereadonlydialog: MatDialog){}...this.dialog.open(NgxMatAuthErrorDialogComponent,{data: errorData});/** * A generic Error Dialog that displays ErrorData. */ @Component({selector: 'ngx-material-auth-error-dialog',templateUrl: './error-dialog.component.html',styleUrls: ['./error-dialog.component.scss'],standalone: true,imports: [MatButtonModule]})exportclassNgxMatAuthErrorDialogComponent{constructor(publicdialogRef: MatDialogRef<NgxMatAuthErrorDialogComponent>, @Inject(MAT_DIALOG_DATA)publicerror: ErrorData,){}/** * Closes the dialog. */close(): void{this.dialogRef.close();}}