Form.io Authentication

Introduction

While many topics such as: user login, roles and permission, email configuration, and application layer logic can be found throughout our docs, particularly within our integrations section, this guide combines these concepts an Angular 2+ application that integrates many of these aforementioned concepts. That said, many of the concepts will focus using the Form.io Portal and much of the application code can be easily ported into other frameworks.

Source Code

All of the source code for this application is available online. We encourage you to walk through the full application, but if you would like to have this application up and running quickly, then feel free to run the application from the source code. You can also use the source code as a reference when walking through this tutorial. Additionally, a copy of the project.json used in this guide can be found inside either option. Click on the button below to download the source code for this application.

Repositories

Before diving into the authentication walk through, here is a list of several resource, including application modules that we will be importing throughout this walk-through. Additionally, you will be expected to have:

  • A Form.io Account - visit portal.form.io to register
  • A email service - this demo will use Mailtrap but you are welcome your own service.

Feel free to antiquate yourself with the following repositories and libraries:

  • @formio.js - view live on github
  • @angular-formio - view live on github
  • Angular - view the official documentation at angular.io
  • Bootstrap - view the official documentation at getbootstrap.com

For specific information regarding our Core Renderer, formio.js, you can find supplemental materials on our wiki.

For specific information regarding our Angular 2+ Wrapper, angular-formio, you can find supplemental materials on our wiki.

Application Setup

To get started, you will need to make sure that you have the following accounts and tools installed on your machine.

  • A Form.io Account - visit portal.form.io to register
  • An installation of Node.js - visit node.org for download instructions, we recommend using the latest LTS version

While not required, we will be using the Angular CLI to build this demo application. Please reference cli.angular.io for additional support. To install and create the application’s development environment, please run the following commands as an administrator from within either the Mac Terminal or Windows PowerShell:

npm install -g @angular/cli
ng new authentication --style=scss
cd authentication
npm install
ng serve

If everything is configured correctly, a default Angular 2+ application should be live at localhost:4200.

Dependencies

To get started we will be adding some additional dependencies to the application. If your project is live, please terminate your local server by hitting Ctrl + C or Command + C depending on your setup. Then run the following npm commands in directory of your project.

npm install --save angular-formio@^2.0.0-alpha.1

While The following libraries are optional, we will be including them in this application as they add both styling and structure to the application.

If you’d like to include bootstrap styling run the following command:

npm install --save bootstrap@^4.0.0

If you’d like to include bootswatch templates run the following command:

npm install --save bootswatch@^4.0.0

Once installed, you will need to include bootstrap and bootswatch libraries in the application by inserting the following code in the src/styles.scss file:

@import "~bootswatch/dist/lux/_variables.scss";
@import "~bootstrap/scss/bootstrap.scss";
@import "~bootswatch/dist/lux/_bootswatch.scss";

Bootstrap requests jquery so if you are planning on using the above dependency add:

npm install --save jquery@^3.3.0

If you would like to include font-awesome to the project, please run the following command:

npm install --save font-awesome@^4.7.0

Once installed, include font-awesome in the the angular-cli.json file at the root level.

{
  "apps": {
    "styles": [
      "styles.scss",
      "../node_modules/font-awesome/scss/font-awesome.scss"
    ],
    "addons": [
      "../node_modules/font-awesome/fonts/*.+(otf|eot|svg|ttf|woff|woff2)"
    ],
    "scripts": [
      "../node_modules/jquery/dist/jquery.js",
      "../node_modules/bootstrap/dist/js/bootstrap.js"
    ]
  }
}

With all of the dependencies installed, feel free to relaunch launch the application by running ng serve. You can leave the development server active while we start working inside the src directory as the project will now automatically update as we make changes to the application. As a point of reference, the above installation commands have explicitly included the version property for each module in case some incompatibility is introduced in the future.

Site Structure

To structure the application, we are going to create all the components and modules we need upfront. Since we’re using the Angular’s CLI tool, we can generate the necessary components via command line.

We shall start by creating the home and navigation component. We will use the navigation to handle to send users to our login, registration, password reset, and authenticated dashboard page.

ng g component home
ng g component navigation

Next, create a module that will be handle routing within the authentication system and another for the authenticated platform.

ng g module auth
ng g module platform

Finally, create the components that will handle auth forms.

ng g component auth/login
ng g component auth/register
ng g component auth/reset-mailer
ng g component auth/reset-password

Once done, the src/app structure should look like this, excluding css and spec.ts files:

  • home ▼
    • home.component.html
    • home.component.ts
  • navigation ▼
    • navigation.component.html
    • navigation.component.ts
  • auth ▼
    • login ▼
      • login.component.html
      • login.component.ts
    • register ▼
      • register.component.html
      • register.component.ts
    • reset-mailer ▼
      • reset-mailer.component.html
      • reset-mailer.component.ts
    • reset-password ▼
      • reset-password.component.html
      • reset-password.component.ts
    • auth.module.ts
  • app.component.html
  • app.component.ts
  • app.module.ts

Lastly, create a config.ts file in the src directory. This file serves as a references for the app and auth configuration. If you have not already, create a new project inside your form.io account on portal. For the purpose

Application Routing

Before we dive into the actual user authentication we need to configure the application to handle the navigation between the various and components and modules we just created. Starting with src -> config.ts insert the follow code. You’ll need to replace the ProjectURL with Live Project Url: found in the top right corner of whichever project on portal.form.io you indent on adding this authentication logic to.

import { FormioAppConfig } from 'angular-formio';
import { FormioAuthConfig } from 'angular-formio/auth';

export const AppConfig: FormioAppConfig = {
  appUrl: 'https://[ProjectURL].form.io',
  apiUrl: 'https://api.form.io',
  icons: 'fontawesome'
};

export const AuthConfig: FormioAuthConfig = {
  login: {
    form: 'user/login'
  },
  register: {
    form: 'user/register'
  }
};

In the app.component.html replace the default content with the code below. This will place the navigation component at the top of the page, while nesting the internal pages inside a general container. The router-outlet is how we view which page is shown to the client.

<app-navigation></app-navigation>
<div class="container" style="margin-top: 10px;">
  <router-outlet></router-outlet>
</div>

In the home component under app -> home -> home.html, replace the internal default content with:

<div class="jumbotron">
  <h3>Welcome to the Form.io Authentication Demo</h3>
</div>

Next, open up app -> navigation -> navigation.html and add the following code. This is a generic Bootstrap 4 HTML nav template with the addition of some Angular 4 logic to handle the active class on our routing links. Additionally, You’ll notice some *ngIf statements which are controlling the view of certain nav elements for example: login and register are replaced with logout when a user is authenticated.

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
  <div class="container">
    <a class="navbar-brand" routerLink="/">Form.io Authentication</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
          <a class="nav-link" routerLink="/"><i class="fa fa-home"></i> Home</a>
        </li>

        <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" *ngIf="auth.authenticated">
          <a class="nav-link" routerLink="/dashboard">Dashboard</a>
        </li>
      </ul>

      <ul class="nav navbar-nav ml-auto">
        <li class="nav-item dropdown" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" *ngIf="!auth.authenticated">
          <a class="nav-link dropdown-toggle" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" >
            Access
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" routerLink="/auth/login">Login</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" routerLink="/auth/register">Register</a>
          </div>
        </li>

        <li class="nav-item dropdown" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" *ngIf="auth.authenticated">
          <a class="nav-link dropdown-toggle" id="navbarDropdown2" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ userEmail }}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" routerLink="/auth/login" (click)="auth.logout()">Logout</a>
          </div>
        </li>
      </ul>
    </div>
  </div>
</nav>

Open navigation -> navigation.ts and add the following. This logic will interface with the FormioAuthService to provide the useful information such as the user’s email address, account level, and if they are authenticated.

import { Component, OnInit } from '@angular/core';
import { FormioAuthService } from 'angular-formio/auth';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';

@Component({
  selector: 'app-navigation',
  templateUrl: './navigation.component.html',
  styleUrls: ['./navigation.component.scss']
})

export class NavigationComponent implements OnInit {
  userEmail: string;
  constructor(public auth: FormioAuthService, public router: Router) {
    router.events.forEach((event) => {
      if (event instanceof NavigationStart) {
        if (auth.authenticated && auth.user.data) {
          this.userEmail = auth.user.data['email'];
        }
      }

      if (event instanceof NavigationEnd) {
        if (auth.authenticated && auth.user.data) {
          this.userEmail = auth.user.data['email'];
        }
      }
    });
  }

  ngOnInit() {
  }
}

We need to address the app.module.ts file to handle the actual navigation stack in addition to adding the config.ts reference which contains our application and API URLs.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormioAuthService, FormioAuthConfig } from 'angular-formio/auth';
import { RouterModule, Routes } from '@angular/router';
import { AuthConfig, AppConfig } from '../config';
import { FormioAppConfig } from 'angular-formio';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
import { PlatformModule } from './platform/platform.module';
import { AuthModule } from './auth/auth.module';

const navStack: Routes = [
  { path: '', component: HomeComponent },
  { path: 'auth', loadChildren: () => AuthModule },
  { path: 'dashboard', loadChildren: () => PlatformModule },
  { path: '**', pathMatch: 'full', redirectTo: ''},
];


@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NavigationComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(navStack)
  ],
  providers: [
    FormioAuthService,
    {provide: FormioAuthConfig, useValue: AuthConfig},
    {provide: FormioAppConfig, useValue: AppConfig}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Finally, open up the app -> auth -> auth.module.ts and add the following. This code handles the routing for actual login interface and will automatically pull the corresponding user login and registration forms.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FormioAuth, FormioAuthRoutes } from 'angular-formio/auth';
import { FormioModule } from 'angular-formio';

import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { ResetMailerComponent } from './reset-mailer/reset-mailer.component';
import { ResetPasswordComponent } from './reset-password/reset-password.component';

@NgModule({
  imports: [
    FormioAuth,
    CommonModule,
    FormioModule,
    RouterModule.forChild(FormioAuthRoutes()),
  ],
  declarations: [
    LoginComponent,
    RegisterComponent,
    ResetMailerComponent,
    ResetPasswordComponent,
  ]
})
export class AuthModule { }

At present the application looks and functions as shown below. Starting with a view of the database hosted on portal.form.io there are no entries within the User Resource. Up registering within the application, the Dashboard becomes visible as defined in the navigation controller. Additionally, the local storage for the application now contains the formioToken, formioUser, and formioAppUser key value pair which is used by the aforementioned FormioAuthService to authenticate the user. Reloading the User Resource we can now see that support@form.io is now registered in the system.

Routing Example

While the application authenticates and routes correctly there are a few quirks that need to be addressed. First, the app dashboard isn’t configured yet. Secondly, registering or logging in won’t automatically route the user to their dashboard. Third visiting “/auth/login” when logged in will result in an “Unauthorized” error message and finally we sti;l have to add a password reset function.

Email Provider

At the onset of this walk through, we mentioned that mailtrap.io would be used to simulate the email process. Before we configure the password reset logic, let’s start with a simple example to make sure our testing email service works. We will be sending a ‘new user’ registration email to a hypothetical admin and registry anytime a new user is added to the application.

Mailtrap

Start by creating a new inbox and click the gear or settings icon to under the actions header.

Mailtrap

Taking note of the username and password head over to portal.form.io and go to settings -> integrations -> email providers -> SMTP Settings. Import the relevant information retrieved from Mailtrap and make sure to click Save Settings before proceeding.

Mailtrap

Visiting the User Resource Actions panel we’ll be adding to 2 email actions as mentioned above.

Mailtrap

Here are the settings for the admin notification email.

Mailtrap

And here are the settings for the individual who registered in the app.

Mailtrap

Here is what the complete Actions panel looks like after adding both events.

Mailtrap

Visiting the app once again create a new user.

Mailtrap

After submitting the form, going to your Mailtrap inbox should reveal both email captured by the system.

Mailtrap

Email Token - Portal

It’s time to add the portal configuration for handling the password reset function. Start by creating two blank forms, a Reset Mailer form which will accept an email address and then send the user a temp token via url, and a Reset Auth form which will have 2 password fields that will update the current users system credentials.

Portal Password Reset Config

Starting with the mailer add an email component and make sure the API key is set to email.

Portal Password Reset Config

Save the form and head over to the Actions Tab.

Portal Password Reset Config

Create a new email action that will control sending a reset password link inside the email’s body to our Mailtrap service. We create a temp token via the following notation: [[token(data.email=user)]].

Portal Password Reset Config

Save your form action.

Portal Password Reset Config

Test the form with a user that already exists in the system.

Portal Password Reset Config

Mailtrap should receive and email with a link to the application with an auth token included in the url parameters. At this point in time the app isn’t configured to handle our new route, /auth/reset

Portal Password Reset Config

Email Token - App

With portal configured to send a password reset link, we need to incorporate the new form into our application. But before we do that we need to make sure that the unauthenticated users can interact with out form. Go to the Access tab on the Reset Mailer Form and add Anonymous to the create own submissions field.

Mailer Interface

Starting in our app -> auth -> auth.module.ts we need to configure our modules routes to accommodate the reset password forms we’ll be adding.

import ...

const authRoutes = FormioAuthRoutes({
    login: LoginComponent,
    register: RegisterComponent
  });

authRoutes[0].children.push({
  path: 'mailer',
  component: ResetMailerComponent
});

authRoutes[0].children.push({
  path: 'reset',
  component: ResetPasswordComponent
});

@NgModule({
  imports: [
    ...
    RouterModule.forChild(authRoutes)
  ],
...
export class AuthModule { }

With that taken care of let’s start by adding a Reset Password option at the bottom of our login form. go to auth -> login -> login.component.html and paste the following. Included in this snippet is the logic to alert the user if they somehow find themselves on the login page and they are already authenticated.

<div *ngIf="!auth.authenticated">
  <formio [src]="service.loginForm" (submit)="service.onLoginSubmit($event); onSubmit($event)"></formio>
  <p><a routerLink="/auth/mailer">Reset Password</a></p>
</div>

<div *ngIf="auth.authenticated">
  <p>You are already Logged in - go to <a routerLink="/dashboard">dashboard</a> </p>
</div>

In the auth -> login -> login.component.ts include the following. This portion of code comes with added benefit of handling the redirect to the users dashboard once they’ve logged in.

import { Component, OnInit } from '@angular/core';
import { FormioAuthService, FormioAuthLoginComponent } from 'angular-formio/auth';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent extends FormioAuthLoginComponent implements OnInit {

  constructor(public auth: FormioAuthService, private router: Router) {
    super(auth);
  }

  onSubmit(event) {
    this.router.navigate(['/dashboard']);
  }
  ngOnInit() {
  }
}

Mailer Interface

We’ll do the same thing for the auth -> register -> register.component.html page as well.

<div *ngIf="!auth.authenticated">
  <formio [src]="service.registerForm" (submit)="service.onRegisterSubmit($event); onSubmit($event)"></formio>
</div>

<div *ngIf="auth.authenticated">
  <p>You are already Logged in - go to <a routerLink="/dashboard">dashboard</a> </p>
</div>

And again for auth -> register -> register.component.ts

import { Component, OnInit } from '@angular/core';
import { FormioAuthService, FormioAuthRegisterComponent } from 'angular-formio/auth';
import { Router } from '@angular/router';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent extends FormioAuthRegisterComponent implements OnInit {

  constructor(public auth: FormioAuthService, private router: Router) {
    super(auth);
  }

  onSubmit(event) {
    this.router.navigate(['/dashboard']);
  }

  ngOnInit() {
  }
}

Moving right along let’s address the app -> mailer-reset -> mailer-reset.component.html

<div *ngIf="!reset">
  <formio [src]="mailerEmbedURL" (submit)="onSubmit($event)"></formio>
</div>

<div *ngIf="reset">
  <div class="alert">
    <h4> A reset email has been sent to: {{ localEmail }}</h4>
  </div>
</div>

And to control the logic behind this interface add the following to app -> mailer-reset -> mailer-reset.component.ts. This code will dynamically fetch the form url from our AppConfig file and append the url with the proper embed code.

import { Component, OnInit } from '@angular/core';
import { AuthConfig, AppConfig } from '../../../config';

@Component({
  selector: 'app-reset-mailer',
  templateUrl: './reset-mailer.component.html',
  styleUrls: ['./reset-mailer.component.scss']
})
export class ResetMailerComponent implements OnInit {
  reset = false;
  localEmail: string;
  mailerEmbedURL =  AppConfig.appUrl + '/resetmailer';
  constructor() {}

  onSubmit(submission) {
    this.reset = true;
    this.localEmail = submission.data['email'];
  }

  ngOnInit() {
  }
}

Mailer Interface

Additionally, there is some good UX involved where after submission, a notification informs the user that an email has been sent to the entered email address.

Mailer Interface

Password Reset - Portal

Head back over to portal and either enter or create you Reset Password Form. In it we will be placing two password components.

Portal Password Reset

Inside the Verify Password text field, add the following custom validation:

valid = (input === data.password) ? true : 'Passwords Must Match';

Portal Password Reset

Password Reset - App

We already configure the states during the Email Token generation in our app -> app.module.ts which means we can head over to the app -> reset-password -> reset-password.component.html and paste the following:

<formio [src]="passwordEmbedURL" (submit)="onSubmit($event)"></formio>

Now comes the heavy lifting, this is where we handle the Token passed along though the URL to authenticate the user and authorize a password reset. The following code does resolves said functionality.

import { Component, OnInit } from '@angular/core';
import { FormioAuthService } from 'angular-formio/auth';
import { AuthConfig, AppConfig } from '../../../config';
import { Router } from '@angular/router';
import { Formio } from 'formiojs';

@Component({
  selector: 'app-reset-password',
  templateUrl: './reset-password.component.html',
  styleUrls: ['./reset-password.component.scss']
})
export class ResetPasswordComponent implements OnInit {
  query = {};
  passwordEmbedURL =  AppConfig.appUrl + '/resetpassword';
  constructor( public auth: FormioAuthService, private router: Router ) {
    location.search.substr(1).split("&").forEach((item) => {
      this.query[item.split("=")[0]] = item.split("=")[1] && decodeURIComponent(item.split("=")[1]);
    });
    const hasToken = localStorage.getItem('formioToken');
    if (this.query['token'] && !hasToken) {
      Formio.setToken(this.query['token']);
      localStorage.removeItem('formioAppUser');
      localStorage.removeItem('formioUser');
    }
  }
  onSubmit(submission) {
    Formio.currentUser().then((user: any) => {
      Formio.setUser(user);
      user.data.password = submission.data.password;
      const userFormio = new Formio(AppConfig.appUrl + '/user/submission/' + user._id);
      userFormio.saveSubmission(user).then((sub: any) => {
        this.router.navigateByUrl('/dashboard');
      });
    });
  }

  ngOnInit() {
  }
}

Dashboard

With everything setup, the only thing left to do is to create a dashboard form on portal.form.io and allow authenticated users access to its content.

If you are interested in building out an authenticated platform you can view Angular 2+ video that showcases how to create an event management system here.