asgardeo
2023/02/27
 
27 Feb, 2023 | 3 min read

How to Build a CRUD App in Angular With Asgardeo Authentication

  • Sanjula Madurapperuma
  • Senior Software Engineer - WSO2

Introduction

In this guide, we'll be building a staff management application that can add, update, and delete records, along with authentication functionality using the angular-oauth2-oidc library and Asgardeo. This application aims to solve the identity and access management (IAM) problem and provide a secure way to manage sensitive information.

The IAM problem

Sensitive information that never saw the light of day is now just sitting behind APIs, waiting to be consumed by users. With the increasing popularity of JavaScript frameworks like Angular and React, there are many applications out there performing hundreds of thousands of client-side processing and rendering each day. This continues to deteriorate the security of important and sensitive information.

An identity provider like Asgardeo can help streamline the user onboarding, authentication, and account management processes for modern applications. By using a specialized identity provider, you can avoid the overhead and expertise required to maintain an in-house identity and access management (IAM) solution. This also ensures industry-standard security and compliance for you and your users, as modern identity providers are equipped to comply with regulations and stay ahead of cybersecurity events.

As we progress through this guide, we will discover how easy it is to integrate an external identity provider into your Angular application. We will also come across some powerful identity and access management features that will help you stay ahead in protecting both your application and user data.

Some of the notable features of this application include: the ability to search for users, protect resources from being invoked by unauthenticated users (Auth Guard with forced login), and an admin console to view access tokens, refresh tokens, identity claims, and ID tokens. This is made possible by valuable libraries and services like the angular-oauth2-oidc library and Asgardeo IDaaS.

The technology stack

  • Angular: the open-source Javascript framework that is written in TypeScript will be used for the frontend.
  • Spring Boot: the open-source Java framework will be used for the backend. 
  • MySQL: the open-source database will be used as the database to maintain the records.
  • The angular-oauth2-oidc library will be used along with Asgardeo to provide authentication for the application that we will be building. These will be described a little more below.

angular-oauth2-oidc library

The angular-oauth2-oidc library is a popular library that provides the groundwork we require to set up authentication with Auth Guard. It is OpenID-certified, actively maintained by the open-source community, and ready for the upcoming OAuth 2.1 version. Some of the notable features include logging in via Code Flow + PKCE, Implicit Flow, Password Flow, automatic token refreshing, and additional custom validations via a hook.

Asgardeo

Asgardeo is an IDaaS (Identity as a Service) that allows integrating customer identity and access management (CIAM) features into apps within minutes. You can find more information on the official Asgardeo website.

Prerequisites

  • Install the preferred JDK (I will be using JDK 11 in this guide)
  • Install MySQL command line (I will be using MySQL version 8.0 in this guide)
  • Install npm
  • Download and install an IDE of your choice (for example, Visual Studio Code or IntelliJ)
  • Download Postman or any tool that would allow us to test the backend APIs.
  • Sign up for an Asgardeo account in order to facilitate the authentication of this application.

Setting up the backend and database

We will start off by setting up the backend using the Spring Initializr tool that is readily available. I will be using the following settings: Feel free to change the project metadata according to your needs.

  • Project: Maven
  • Language: Java
  • Spring Boot: 2.7.8
  • Project Metadata:
    • Group: com.sanjula.angular.employee.manager
    • Artifact: backend
    • Name: backend
    • Description: AION - A Staff Management Application
    • Package name: com.sanjula.angular.employee.manager.backend
    • Packaging: Jar
    • Java: 11

Note the following dependencies that I have added via the tool itself: 

  1. Spring Web
  2. Spring Data JPA
  3. MySQL Driver

The Spring Web dependency would be useful for us as we are building a web application, while the Spring Data JPA and MySQL driver are required when setting up the database connection, which we will look at a little later.

Once you have filled in the preferred project metadata information and added the above dependencies, click on the “Generate” button to download the generated project structure. Once that is done, extract the compressed file to a preferred location and open it in your preferred IDE. I would be choosing IntelliJ Idea as the IDE for the backend server.

The data structure of the classes implemented in the backend is presented in the Data Structure.md file. You can access all the defined classes in the backend server's GitHub repository.

Setting up the database and its configuration

We now need to create a new database in order to facilitate the persistence of the model class we created previously.

Open a terminal window and enter the MySQL command-line interface (you can instead use a graphical tool like MySQL Workbench for this if you prefer that). Enter the following command in order to enter it into MySQL: Make sure to replace the username and also provide the password of your MySQL user.

                    
  mysql -u <username> -p
 

Now we need to create a database. Enter the following command (make sure to provide a name that you prefer; in this case, I have named our application AION).

                    
  create database aion;
  

In our backend, we need to provide the configurations that would allow it to communicate with our newly created database. Add the following properties to the “application.properties” file:

                      
    # MySQL Configurations for AION backend
    spring.datasource.url=jdbc:mysql://localhost:3306/aion
    spring.datasource.username=root
    spring.datasource.password=admin
    spring.jpa.show-sql=true
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect


Feel free to change the datasource URL, username, and password according to your deployment of the MySQL server. Note that the hibernate dialect is MySQL8Dialect since it corresponds to the version of MySQL that I am using.

Create an interface to allow the persistence of the staff data

There should be a mechanism to save the staff information in the database. We will be using the JpaRepository class for this interface.

Create a new package named “repository” and a new interface inside it called “EmployeeRepository.” This interface will extend the JpaRepository interface, which would allow us to perform basic CRUD operations on the database with the staff data. The methods include

  1. deleteEmployeeById
  2. findEmployeeById

Create an Exception class

Create a new package named “exception” and a class called “UserNotFoundException” within it. Here, we will extend from the RuntimeException class and implement a constructor that takes in a string parameter and passes it on to the superclass.

Create a new Service class that would be used in the controller

We now need to create a service class for the “EmployeeRepository” interface.

Create a new package named “service” and a class called “EmployeeService” within it. Here, we will define the necessary methods (shown below) that would allow us to perform CRUD operations via the EmployeeRepository interface. Make sure to add the @Service annotation to the class to specify that the application logic is present here, the @Autowired annotation to the constructor, and the @Transactional annotation to the deleteEmployee method.

Expose the API used to serve the requests

The HTTP requests from the client would first come to the controller in the backend server, which would then redirect the request to the service, where the service would perform all of the CRUD operations by communicating with the database. The controller is also responsible for returning the necessary responses. Now that we have created our service, we can go ahead and create the controller.

Create a new class called “EmployeeController” in the root package. The following methods would be implemented:

  1. getAllEmployees
  2. getEmployeeById
  3. addEmployee
  4. updateEmployee
  5. deleteEmployee

/employee” would be the root endpoint that we will be using. Also, note the following endpoints for each of the above methods.

  1. /all” -> For retrieving all the employees.
  2. /find/{id}” -> For retrieving an employee by their id.
  3. /add” -> For adding a new employee.
  4. /update” -> For updating details of an employee.
  5. /delete/{id}” -> For deleting an employee.

Test the exposed API endpoints

The following curl can be imported into Postman or used to test the endpoint “/all” to retrieve all of the employees.

    
    curl --location --request GET 'http://localhost:8080/employee/all'
  

The following curl can be imported into Postman or used to test the endpoint “/add” to add a new employee.

                      
    curl --location --request POST 'http://localhost:8080/employee/add' \
    --header 'Content-Type: application/json' \
    --data-raw '{
       "email": "[email protected]",
       "imageUrl": "https://bootdey.com/img/Content/avatar/avatar4.png",
       "jobTitle": "CTO",
       "name": "Trotter Sylvester",
       "phoneNumber": "+1438532854322"
    }'
    
    
  

To verify that the above employee has been added correctly, we can inspect the MySQL database by running the following commands:

                      
    use aion;

    select * from employee;
  

Note that localhost:8080 is where the backend server is configured to run.

Setting up the frontend

As stated at the beginning of this guide, to facilitate the authentication of users, we will be using the angular-oauth2-oidc library. To install this library using npm, run the following command:

                      
  npm i angular-oauth2-oidc
  

Note: I will be walking through only the most important Angular components and files in this guide. The rest of the files would be available in the GitHub repository aion-frontend.

Register a new single-page application in Asgardeo

Before we go any further, we should register our application in Asgardeo. The app will be registered as an OpenID Connect single-page app, and a client ID will be issued for it. Asgardeo does not issue client secrets because single-page apps are public clients. If needed, the PKCE (Proof Key for Code Exchange) extension can be used. This feature is enabled by default in Asgardeo. Let’s register the application by following the steps given below.

  1. Sign in to the Asgardeo Console and navigate to Develop > Applications > New Application > Single-Page Application (Note that you can also directly click on the button highlighted below under the “Onboard and manage apps” section).



  2. Enter the following details (a unique name and the redirect URL, which in this case would be AION and http://localhost:4200/).



  3. Click Register.

Once we register the application, a client ID will be generated. The AION application would use this client ID to identify itself to Asgardeo.

Enable Self-User Registration in Asgardeo

With this feature, users can self-register for the configured organization in Asgardeo through the login page. This creates a new user account in the organization.

To enable this feature, open the Asgardeo console at https://console.asgardeo.io and navigate to Manage > Self Registration.

Then click on the toggle to enable the feature.



Configuring the angular-oauth2-oidc library with Asgardeo to enable authentication

When configuring the angular-oauth2-oidc library, there are a few steps that we should follow to allow it to facilitate communication with Asgardeo and identify the correct endpoints to invoke.

Authentication Configuration

First, the AuthConfig constant needs to be created to persist the corresponding properties relevant to Asgardeo. The AuthConfig object that I am using for this application is shown below.

                      
    export const authConfig: AuthConfig = {
      issuer: 'https://api.asgardeo.io/t/,
      clientId: '',
      responseType: 'code',
      redirectUri: 'http://localhost:4200/',
      tokenEndpoint: 'https://api.asgardeo.io/t//oauth2/token',
      skipIssuerCheck: true,
      logoutUrl: 'https://api.asgardeo.io/t//oidc/logout',
      postLogoutRedirectUri: 'http://localhost:4200/',
      scope: 'openid profile', 
      useSilentRefresh: false,
      silentRefreshTimeout: 5000,
      timeoutFactor: 0.25, 
      sessionChecksEnabled: true,
      showDebugInformation: true,
      clearHashAfterLogin: false,
      nonceStateSeparator : 'semicolon'
     };
     
  

Note that your organization name from the organization that was created when signing up with Asgardeo or any other organization can be appended to the issuer URL. The client ID should represent the value that was shown in the Asgardeo Console for the application that we registered.

Create a new authentication service

A new authentication service would need to be created to serve our authentication operations. I am using the angular-oauth2-oidc library for this. The following are the notable functions that are implemented in the AuthService class, which are named:

  1. canActivateProtectedRoutes - A publisher used to check the latest state of whether the user is authenticated and if the ajax calls for the login have been completed.
  2. runInitialLoginSequence - Responsible for invoking the discovery endpoint of Asgardeo and performing a silent login.
  3. login - Invokes the login flow from OAuthService.
  4. logout - Invokes the logout flow from OAuthService.
  5. refresh - Invokes the silent refresh flow from OAuthService.
  6. hasValidToken - Identify if a valid access token is present.
  7. accessToken - Retrieve the access token for the user.
  8. refreshToken - Retrieve the refresh token for the user.
  9. identityClaims - Retrieve the identity claims of the user.
  10. idToken - Retrieve the ID token for the user.
  11. logoutUrl - Retrieve the logoutUrl configured.

Make sure to pass the discovery endpoint of Asgardeo along with your organization name (shown below) into the loadDiscoveryDocument() function in the OAuthService class when initializing the login sequence in the AuthService class.

https://api.asgardeo.io/t/<organization_name>/oauth2/token/.well-known/openid-configuration

Add configuration to allow OAuthModuleConfig to accept communication from Asgardeo

We need to allow the URLs from Asgardeo within the OAuthModuleConfig object in the angular-oauth2-oidc library (essentially a CORS configuration) and also allow sending access tokens. Create a new file called auth-module-config.ts and add the following:

                      
    export const authModuleConfig: OAuthModuleConfig = {
      resourceServer: {
        allowedUrls: ['https://api.asgardeo.io/t/'],
        sendAccessToken: true,
      }
     };
     
  

Define the backend server base URL

Before the Angular application starts, we need it to identify the base URL for the backend and serve the APIs accordingly. Create a file named environment.ts in a sub-directory named environments and add the following:

                      
    export const environment = {
      production: false,
      apiBaseUrl: 'http://localhost:8080'
     };
     
     
  

Add the dependency injection token

The frontend routers need to be dynamically configurable according to a backend API call. Hence, we can use a dependency injection token called APP_INITIALIZER to enable this. The initialization function that is provided is injected at application startup and executed during the initialization. In our case, the function would return a Promise, so the initialization does not complete until the Promise is resolved. Create a file named auth-app-initializer.factory.ts under the core sub-directory and add the following factory function:

                      
    export function authAppInitializerFactory(authService: AuthService): () => Promise {
      return () => authService.runInitialLoginSequence();
     }
     
  

Enabling Auth Guard with forced login

Since we are building a staff management system, we should not expose sensitive information to anyone who is not signed in to the application. We will be using the canActivate interface to implement this. Create a file named auth-guard-with-forced-login.service.ts and add the following:

                      
    @Injectable()
    export class AuthGuardWithForcedLogin implements CanActivate {
    
    
     constructor(
       private authService: AuthService,
     ) {
     }
    
    
     canActivate(
       route: ActivatedRouteSnapshot,
       state: RouterStateSnapshot,
     ): Observable {
       return this.authService.isDoneLoading$.pipe(
         filter(isDone => isDone),
         switchMap(_ => this.authService.isAuthenticated$),
         tap(isAuthenticated => isAuthenticated ||        this.authService.login(state.url)),
       );
     }
    }
    
  

Note that a private member of the class AuthService is defined in the constructor, which is then used to invoke the login function in the AuthService class if required during the execution of canActivate.

Add the providers required into the root module for authentication

We now need to add the providers required for authentication and also the APP_INITIALIZER as a separate module. Create a file called core.module.ts under the core sub-directory. The following are some of the important additions to this class.

  1. A function to return the local storage as it is not available during build time.
  2. Define the imports of the HttpClientModule and the OAuthModule.forRoot() function in the NgModule
  3. Define the providers AuthService, AuthGuard, and AuthGuardWithForcedLogin in the NgModule
  4. Implement the static function to return a module with the providers APP_INITIALIZER, AuthConfig, OAuthModuleConfig, and OAuthStorage

Set up the home page

Our home page would be displaying staff records to authenticated users and allow performing operations on them but would not show the records to an unauthenticated user. As our AppComponent would house most of the application frontend implementation, we need our home page to show the current location to the user and also display a custom message if the user is not authenticated. Create a sub-directory named feature-basics and a file named home.component.ts and add the following.

  1. A suitable selector
  2. A template or a templateUrl
  3. An Observable variable that can then be used in the constructor to assign a boolean value based on whether the user is authenticated or not

In order to expose the HomeComponent as a child in the RouterModule, let’s implement another class in the same subdirectory named basics.module.ts and add the following:

                      
    @NgModule({
      declarations: [
        HomeComponent,
      ],
      imports: [
        CommonModule,
        SharedModule,
        RouterModule.forChild([
          { path: '', redirectTo: 'home', pathMatch: 'full' },
          { path: 'home', component: HomeComponent },
        ]),
      ],
      providers: [
        ApiService,
      ],
     })
     export class BasicsModule { }
  

Setting up the admin console

The admin console would allow the authenticated user to view the access token, refresh token, identity claims, and ID token of the user, which could be utilized for additional functionality that this guide does not cover. This type of information would not be present in the UI of a typical application, but for the purpose of this guide and to make it easy to understand, let’s include it. Create a new file named admin-console.component.ts and add the necessary functions.

Note that we only have the templateUrl property defined in the Component annotation instead of the template property like in the HomeComponent. This is due to the fact that we have quite a lot more lines of code to include for the AppComponent, and it would help increase readability if we had it in a separate HTML file. Create a file named admin-console.component.html and add the following table, which displays information such as access token, refresh token, ID token, and identity claims, which can be utilized to enhance the application with other cool features available in Asgardeo.

                      
    
IsAuthenticated {{isAuthenticated$ | async}}
HasValidToken {{hasValidToken}}
IsDoneLoading {{isDoneLoading$ | async}}
CanActivateProtectedRoutes {{canActivateProtectedRoutes$ | async}}
IdentityClaims {{identityClaims | json}}
RefreshToken {{refreshToken}}
AccessToken {{accessToken}}
IdToken {{idToken}}

Create the Employee Interface

We need to define the type of data that the requests are going to be returning.

Create a new file called employee.ts and define the following:

                      
    export interface Employee {
      id: number;
      name: string;
      email: string;
      jobTitle: string;
      phoneNumber: string;
      imageUrl: string;
      code: string;
   }
   
  

Create the EmployeeService

Now let’s define the endpoints that the frontend should call the backend for each operation. Create a new file called employee.service.ts and add the following functions:

                      
    @Injectable({
      providedIn: 'root'
   })
   export class EmployeeService {
      private apiServerUrl = environment.apiBaseUrl;
   
   
      constructor(private http: HttpClient) { }
   
   
      public getEmployees(): Observable {
          return this.http.get(`${this.apiServerUrl}/employee/all`);
      }
   
   
      public addEmployee(employee: Employee): Observable {
          return this.http.post(`${this.apiServerUrl}/employee/add`, employee);
      }
   
   
      public updateEmployee(employee: Employee): Observable {
          return this.http.put(`${this.apiServerUrl}/employee/update`, employee);
      }
   
   
      public deleteEmployee(employeeId: number | undefined): Observable {
          return this.http.delete(`${this.apiServerUrl}/employee/delete/${employeeId}`);
      }
   }
   
  

Set up the AppComponent

The main functionalities we need to serve in the AppComponent class are shown below:

  1. Retrieve employees
  2. Search for employees
  3. Add employee
  4. Update employee
  5. Delete employee

Make sure to add the following to the app.component.ts file.

Retrieve Employees

                      
    public getEmployees(): void {
      this.isAuthenticated$.subscribe(data => {
        if (data === true) {
          this.employeeService.getEmployees().subscribe(
            (response: Employee[]) => {
              this.employees = response;
            },
            (error: HttpErrorResponse) => {
              alert(error.message);
            }
          )
        }
      })
    }
   
  

The getEmployees function will first check if the user is authenticated. If yes, the getEmployees function defined in the EmployeeService class is invoked, which returns the list of employees from the backend server.

Search for employees

                      
    public searchEmployees(key: string): void {
      const results: Employee[] = [];
      for (const employee of this.employees) {
        if (employee.name.toLowerCase().indexOf(key.toLowerCase()) !== -1
        || employee.email.toLowerCase().indexOf(key.toLowerCase()) !== -1
        || employee.phoneNumber.toLowerCase().indexOf(key.toLowerCase()) !== -1
        || employee.jobTitle.toLowerCase().indexOf(key.toLowerCase()) !== -1) {
          results.push(employee);
        }
      }
      this.employees = results;
      if (results.length === 0 || !key) {
        this.getEmployees();
      }
    }
   
  

The searchEmployees function iterates through the list of employees and matches the key that the user has provided. If there are results for the given key, the employee records in the results are shown. Once the focus shifts or exits from the search bar, the getEmployees function is invoked again to showcase the complete list of employees. 

Add employee

                      
    public onAddEmployee(addForm: NgForm): void {
      document.getElementById('add-employee-form')?.click();
      this.employeeService.addEmployee(addForm.value).subscribe(
        (response: Employee) => {
          console.log(response);
          this.getEmployees();
          addForm.reset();
        },
        (error: HttpErrorResponse) => {
          alert(error.message);
          addForm.reset();
        }
      );
    }
   
  

The onAddEmployee function retrieves the filled form and passes the form value into the addEmployee function in the EmployeeService class before resetting the form with the response returned from the backend server.

Update Employee

                      
    public onUpdateEmployee(employee: Employee): void {
      this.employeeService.updateEmployee(employee).subscribe(
        (response: Employee) => {
          this.getEmployees();
        },
        (error: HttpErrorResponse) => {
          alert(error.message);
        }
      );
    }
   
  

The onUpdateEmployee function passes the Employee object into the updateEmployee function in the EmployeeService class. If the response returns the updated Employee object, it would invoke the getEmployees function to display the latest changes to the user.

Delete Employee

                      
    public onDeleteEmployee(employeeId: number | undefined): void {
      this.employeeService.deleteEmployee(employeeId).subscribe(
        (response: void) => {
          console.log(response);
          this.getEmployees();
        },
        (error: HttpErrorResponse) => {
          alert(error.message);
        }
      );
    }
   
  

The onDeleteEmployee function passes the employeeId of the employee that is to be deleted into the deleteEmployee function of the EmployeeService class. If the response is returned as expected, the getEmployees function would be invoked to display the latest changes, similar to the onUpdateEmployee method.

Note that adding, updating, and deleting an employee record is only possible if the user has logged in to the application. This is facilitated by the implemented onOpenModal function.

The app.component.html file contains the navigation bar and the Modals being used to display forms to add, update, and delete an employee. The list of employees would be showcased by reading the array of employees defined in the AppComponent class.

Define the AppModule

We now need to tell Angular how it should build and bootstrap the app. Add the following to the app.module.ts file.

                      
    @NgModule({
      declarations: [
        AppComponent,
        AdminConsoleComponent,
      ],
      imports: [
        BrowserModule,
        HttpClientModule,
        FormsModule,
        CommonModule,
        SharedModule,
        CoreModule.forRoot(),
        RouterModule.forRoot([
        { path: '', redirectTo: 'basics/home', pathMatch: 'full' },
        { path: 'basics', loadChildren: () => import('./feature-basics/basics.module').then(m => m.BasicsModule) },
        { path: 'extras/admin-console', component: AdminConsoleComponent, canActivate: [AuthGuardWithForcedLogin] },
     ], {})
      ],
      providers: [EmployeeService],
      bootstrap: [AppComponent]
     })
     export class AppModule { }
  

Add the styling

To put the icing on the cake, we can now go ahead and add the relevant CSS styles to the styles.css file. Note the two minimized CSS files imported from Bootstrap as well.

                      
    @import 'https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css';
    @import 'https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css';
  

You are now good to go!

Starting up the AION application

Start the database server

Before we start the Angular frontend server, we need to start the local MySQL server and then the backend.

The commands for running the MySQL server differ according to the host operating system. On Linux, it would be:

                      
    sudo service mysql start
  

Start the backend server

To start the Spring-Boot backend server, open a terminal, navigate to the root directory of the backend application, and run the following command.

                      
    ./mvnw spring-boot:run
  

Start the frontend server

To start the frontend server, first run the following command to install the relevant dependencies.

                      
    npm install
  

Next, run the following command to start the server.

                      
    npm run start
  

Demonstration

The application home page (after signing in) would now look similar to the following.





Next Steps

Now that we have successfully integrated Asgardeo and the angular-oauth2-oidc library to create a robust record management system with user self-registration and authentication, there are several exciting opportunities to expand its functionality.

Some of the added features you can explore using Asgardeo include:

  • Custom branding for your organization
  • Social login options such as Google, Microsoft, Facebook, and GitHub
  • Multi-factor authentication options (TOTP, email OTP, SMS OTP)
  • Conditional authentication
  • Passwordless login methods like Biometrics and Magic Link

To get started, the complete code base used in this guide is available in the aion-frontend and aion-backend GitHub repositories.

If you want to further harness the power of Asgardeo's CIAM features for your enterprise applications, feel free to contact us for more information. And don't forget to join our growing community of developers!

English