Creating an Angular Login With the Latest Version
In this article, we will look over creating an Angular application. This will provide us with options to authenticate, register new users, and log out. It will be a proof of concept for OAuth 2.0 implementation and refresh token. This example will communicate with a backend REST API implemented with .NET Core.
We will cover only the front-end part using the IdentityMicroservice created in a previous article .
Creating a new Angular project with Material
To develop the angular login application we will use Visual Studio Code. You can find out more about the editor in a previous article I wrote a while back.
For the Angular Identity application, I chose to also use Angular Material. You can find out more about it here .
Let’s start by creating our Angular application:
- ng new AngularIdentity
- Add angular routing yes
- Stylesheet SCSS
And now we can add Angular Material to our application:
ng add @angular/material
Angular project structure
The most common change to my angular projects was the folder projects. I always found myself not happy with it and with the way I had to have references to different sections from my projects. Most likely I will change my mind in the future and come up with a different folder structure, but for now, this is the folder structure that I am using:
Layouts
As mentioned above, the user will be able to register a new user, log in, and log out. For this, I chose to use three different layouts. One for authentication, another for the main layout, and the last one for errors.
Auth Layout
In this section, we will design how the UI will look for the pages before accessing the main application. Under this layout, we will have Login and Register page.
Error Layout
We will use this layout to design the UI for our error pages.
Main Layout
Under this layout, we will display all other components used across the application. In this layout, we will have a header and a sidenav that will allow the user to navigate across the application. The authentication guard restricts access to all pages from this layout.
Authentication Guard
This guard restricts access to some resources and allows access to others for authenticated users.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private userPersistenceService: UserPersistenceService
) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.userPersistenceService.getUser()) {
return true;
}
this.router.navigate(['/auth/login'], {
queryParams: { returnUrl: state.url },
});
return false;
}
}
We reference the guard inside the routing module file. This restricts access to all pages from the Main Layout based on the AuthGuard rules.
component: MainLayoutComponent,
canActivate: [AuthGuard],
During the authentication process, we save the user data into the session storage. For this, we are using the UserPersistenceService.
Authentication Interceptor
The authentication guard is protecting different sections in the UI and now is time to allow the requests to our Web API. For this, I have created an authentication interceptor. When a user is logging in to the application, the Web API is sending back the user together with the access token and the refresh token. We save all this data into the session storage with the help of our UserPersistenceService.
The authentication interceptor is adding an Authorization header to all the web requests. Besides this, the interceptor is checking if the server is replying with 401 Unauthorized. If the server is sending back 401, most likely the token has expired. In this case, the authentication interceptor is trying to get a new refresh token based on the refresh token. We use the new access token to execute again the failed request.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
null
);
constructor(
private userPersistenceService: UserPersistenceService,
private authenticationService: AuthenticationService,
private progressService: ProgressService
) { }
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
this.progressService.start();
const currentUser = this.userPersistenceService.getUser();
if (currentUser) {
request = this.addHeaders(request, currentUser);
}
return next.handle(request).pipe(
catchError((err: HttpErrorResponse) => {
if (
err instanceof HttpErrorResponse && err.status === 401
) {
return this.handle401Error(request, next);
} else {
return throwError(() => err);
}
}),
finalize(() => this.progressService.complete())
);
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
const currentUser = this.userPersistenceService.getUser() ?? new User();
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authenticationService
.refreshToken(
new TokenModel(currentUser.token, currentUser.refreshToken, currentUser.ipAddress)
)
.pipe(
switchMap((tokenModel: TokenModel) => {
this.isRefreshing = false;
currentUser.token = tokenModel.token;
currentUser.refreshToken = tokenModel.refreshToken;
this.userPersistenceService.setUser(currentUser);
this.refreshTokenSubject.next(tokenModel.token);
if (request.url.indexOf(endpoints.auth.logout) >= 0 && request.body instanceof TokenModel) {
return next.handle(this.addHeaders(this.handleLogout(request, tokenModel), currentUser));
}
return next.handle(this.addHeaders(request, currentUser));
})
);
} else {
return this.refreshTokenSubject.pipe(
filter((token) => token != null),
take(1),
switchMap((jwt) => {
return next.handle(this.addHeaders(request, jwt));
})
);
}
}
private handleLogout(request: HttpRequest<any>, tokenModel: TokenModel) {
var logoutTokenModel = request.body as TokenModel;
logoutTokenModel.refreshToken = tokenModel.refreshToken;
var newRequest = request.clone({
body: logoutTokenModel
});
return newRequest;
}
private addHeaders(request: HttpRequest<any>, user: User) {
if (request.url.indexOf(endpoints.auth.refreshToken) >= 0) {
return request.clone();
}
return request.clone({
setHeaders: {
Authorization: `Bearer ${user.token}`
},
});
}
}
Besides the Authentication Interceptor, you can find another one that is handling errors.
Services
In this section, we can find different files that will help us through the application.
Authentication Service
We use this service to make calls to the Web API.
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
constructor(private http: HttpClient) { }
public login(loginCredentials: LoginCredentials) {
return this.http.post<any>(
`${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.authenticate}`,
loginCredentials
);
}
public register(entity: RegisterUser) {
return this.http.post(
`${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.register}`,
entity
);
}
public refreshToken(tokenModel: TokenModel) {
return this.http.post<TokenModel>(
`${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.refreshToken}`,
tokenModel
);
}
public logout(tokenModel: TokenModel) {
return this.http.post<string>(
`${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.logout}`,
tokenModel
);
}
public logoutEverywhere() {
return this.http.post<string>(
`${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.logoutEverywhere}`,
undefined
);
}
}
User Persistence Service
When users are logging in we are keeping their data in the browser’s session storage. This service is taking care of that.
@Injectable({
providedIn: 'root'
})
export class UserPersistenceService {
user?: User;
@Output() changeUser: EventEmitter<User> = new EventEmitter();
constructor() { }
public getUser(): User | undefined {
var sessionUser = sessionStorage.getItem('user');
if (sessionUser && sessionUser != null) {
this.user = JSON.parse(sessionUser);
} else {
this.user = undefined;
}
return this.user;
}
public setUser(user: User) {
sessionStorage.setItem('user', JSON.stringify(user));
this.user = user;
this.changeUser.emit(this.user);
}
public removeUser() {
sessionStorage.removeItem('user');
this.user = undefined;
}
}
Features Module
Here we find all pages and what the user will see in the browser. In the features section, we keep the routing as well. I chose to split the features module between multiple modules, one for each major section of the application. Each of the sub-modules is then registered as a route in the routing module. Here is the features routing module:
const routes: Routes = [
{
path: '',
component: MainLayoutComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: '',
loadChildren: () =>
import('./dashboard/dashboard.feature.module').then(
(m) => m.DashboardFeatureModule
),
},
],
},
{
path: 'auth',
component: AuthLayoutComponent,
children: [
{
path: '',
loadChildren: () =>
import('./auth/auth.feature.module').then(
(m) => m.AuthFeatureModule
),
},
],
},
{
path: 'error',
component: ErrorLayoutComponent,
children: [
{
path: '',
loadChildren: () =>
import('./errors/errors.feature.module').then(
(m) => m.ErrorsFeatureModule
),
},
],
},
{ path: '**', redirectTo: 'error/not-found' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class FeaturesRoutingModule { }
Resources
- Source Code: https://github.com/relaxedcodercom/AngularIdentity
- How To Implement DotNET Core OAuth2 For An API
- .NET Core Web API for OAuth 2.0: https://github.com/relaxedcodercom/IdentityMicroservice
Subscribe to my newsletter
Subscribe to Relaxed Coder and be up to date with the latest articles.
Conclusions
We have implemented an Angular application that is allowing us to authenticate, register new users, and log out. The application is communicating with a .NET Core Web API using OAuth 2.0. If you have any questions or need any clarifications please use the comments section below.