Authentication and Authorization

A developers guide to the User Authentication and Authorization processes

Before you read this section, we highly recommend you read the User Authentication and Roles and Permissions within the Users Guide so that you can become familiar with some of the non-developer mechanics of how Users, Roles, and Permissions are handled within Form.io.

User Authentication

Form.io uses JSON Web Tokens (or also called JWT) for all aspects of user authentication. Even when other authentication frameworks are being used, such as SAML or OAuth, the Form.io platform converts these into equivalent JWT tokens. It is highly recommended to read how these tokens work to have a complete understanding of how the Authentication system within Form.io operates. Here are some places to go to read how JWT Tokens work.

In order to understand how these tokens are utilized, it is important to understand how Users are established within the Form.io platform and what it means to actually be "authenticated". For this reason, we recommend reading the User Authentication section of the help guide to get a good understanding of how Users are created and established. Once a user is established, the next task is to authenticate a user to retrieve a JWT token.

Authentication Flow

In order to best describe the authentication flow for Form.io, it is easier to first show a diagram of how a user is authenticated within the Form.io platform.

As this diagram illustrates, the mechanism that performs an authentication is achieved with the Login Action which is a form action that can be attached to any form within the Form.io platform. The series of steps that this Login action performs is as follows.

  1. Inspects a POST body of the submission API call that was made against the form in context.

  2. Extract the "username" and "password" fields from the POST body. These fields are configured as part of the Login action configuration.

  3. Perform a search against the underlying Resource to match a submission for the provided "username". The underlying resources are also configured as part of the Login action allowing you to authenticate against ANY resource.

  4. Once a record has been found, it will then perform a one-way encrypted hash comparison between the password fields of that resource to ensure that the user who submitted the form has matched the password on that resource submission.

  5. If the password hashes match, then the Login Action will then generate a JWT token of the resource submission object which will then become the User that is authenticated.

  6. Assign that JWT token to the "x-jwt-token" response header which is then sent back to the client application.

  7. The renderer will see that a "x-jwt-token" has been introduced and then save this token to the "formioToken" localStorage variable which will be used as the persistent authentication.

This JWT token is then used in every request made to the Form.io platform, which will serve as the authentication context for any request made into Form.io. If you wish to view the JWT token for any application that you are using connected to Form.io, you can navigate to the Local Storage of your browser and take a look at the formioToken localStorage variable like the following shows.

The JWT token can also be read as raw JSON by copying the value of the JWT Token and then taking it to https://jwt.io to view the contents like the following illustrates.

This JWT token forms the basis behind all authentication within Form.io. Even when other authentication providers are utilized such as SAML, Oauth, and LDAP, the goal of those actions is to exchange the tokens of those systems into a JWT token which is then used by the Form.io platform to establish authentication of an end user.

Custom JWT Authentication

Now that it is understood that JWT is the method for all user authentication, any Deployed Enterprise customer can take advantage of this to establish their own custom authentication mechanisms by forging their own JWT tokens for their users.

There are many cases where Form.io needs to be tightly integrated into an existing platform with authentication mechanisms already established.

For these cases, Form.io can be utilized within deployed environments using JWT tokens that can be used as SSO into the Form.io platform. This does NOT require any user accounts within Form.io, but rather creates a way to pass along a dynamically generated JWT token claiming certain roles configured within the project.

Here is how it works.

  • To get started, you will need to deploy your own on-premise deployment into your environment and ensure you set the JWT_SECRET of that deployment to a secret only you know. Please see Docker Deployments for more information.

  • You will now need to create Roles within your Form.io project that you would like to use to control the access to certain Form.io operations. You can then assign those roles to the Permissions of the forms within the Access section of those forms. Please take note of the ID’s of these roles that were created since they will be used when generating the SSO tokens.

  • Now that you have some Roles created, you will then need to generate a special JWT token within your own backend platform, or within an authentication proxy using something like AWS Lambda. The payload for this token needs to be as follows.

{
  external: true,
  form: {
    _id: 'USER_RESOURCE_FORM_ID'
  },
  project: {
    _id: 'PROJECT_ID'
  }
  user: {
    _id: 'external',
    data: {
      name: 'joe'
    }
    roles: [
      'ROLE_ID_1',
      'ROLE_ID_2'
    ]
  }
}

You will then make the following replacements.

  • USER_RESOURCE_FORM_ID - This is the ID of the form/resource that would normally contain user records. NOTE: You do not need to provide a user record ID, but just the form ID.

  • PROJECT_ID - This is the Form.io project ID

  • ROLE_ID_1, ROLE_ID_2, … - These are the ID’s of the roles you would like this token to have when authenticating.

This token can be generated using a number of ways and should be generated within your own backend server or hosted lambda function. Since you know the JWT secret of the Docker deployment, you can generate valid tokens such as the following code.

/**
 * This code does require our Docker deployment where the "JWT_SECRET" would be the
 * same secret of the environment variable when the docker is spun up.
 */
var jwt = require('jsonwebtoken');
var token = jwt.sign({
  external: true,
  form: {
    _id: '59795d259be16e3ee58fddaa',
  },
  project: {
    _id: '59795d259be16e3ee58fdda6'
  }
  user: {
    _id: 'external',
    data: {
      name: 'joe'
    }
    roles: [
      '59795d259be16e3ee58fdda7'
    ]
  }
}, 'JWT_SECRET');
    
// We now have a token!
console.log(token);

This example uses Node.js and the JSON Web Token library, but this could be done within any backend server language. You will also need to make sure to replace JWT_TOKEN string with the password string you used for JWT_TOKEN when you deployed the server via. Docker.

  • Now that you have a token that is generated from the server, you will then send that to the client application when you serve the application. You can then place the following code in your Template to establish a SSO integration into Form.io.

<script type="text/javascript">
localStorage.setItem('formioToken', 'FORMIO_TOKEN');
</script>

Here you would just replace the FORMIO_TOKEN with the actual token generated from the server.

  • Now that you have a token added to localStorage using the special token formioToken, this will be used for all communication to the Form.io API platform and authenticate as the Roles provided in the token!

User Authorization

The first element to establish authorization is the assignment of roles to any user that is authenticated within Form.io. Any Resource Submission within Form.io can be assigned "roles" which essentially means that any Resource can be utilized as a user table. Each submission within a resource has a property called roles, which is just a simple array of MongoDB ID's of the Roles in which that submission has been given. For example, a typical user submission within Form.io may look like the following.

{
  "_id": "5e4de94be413be58d17e6f8a",
  "owner": "5888f1ce7150df0072bacb54",
  "roles": [
    "5e4de92d439cac3c9883e973"
  ],
  "_vid": 0,
  "_fvid": 0,
  "state": "submitted",
  "data": {
    "email": "joe@example.com"
  },
  "access": [],
  "form": "59bbe2ec8c246100079191b2",
  "project": "59bbe2ec8c246100079191ae",
  "externalIds": [],
  "created": "2020-02-20T02:04:59.597Z",
  "modified": "2020-02-20T02:04:59.603Z",
  "metadata": {
    "login": {
      "attempts": 0,
      "last": 1626369807004
    }
  }
}

Within this object, you can see how the roles are assigned, which defines role id's of all the roles that this submission has been granted.

There is currently only ONE way to grant roles to a submission, which is through the Role Assignment action. It is through the configuration of this Role Assignment action that the role that will be assigned is determined and configured within this action.

Only roles defined within the containing project can be added to the configuration of the Role Assignment action.

The reason for the role-adding constraint is that much care must be given when granting roles to any submission and any attempt to add roles through the API will be blocked. Because of this, the Role Assignment action is the only way to add roles. Removing roles, however, can be achieved using a simple PUT request on the submission endpoint defined at https://apidocs.form.io/#fa874508-bff1-1047-a504-d3831576df00 with one of the roles removed. It is always permissible to remove roles, whereas adding roles is dealt with extreme caution within the Form.io platform.

Everyone Role

Within Form.io, there is also a special role called Everyone. The Role ID for the Everyone role is always 000000000000000000000000 which is a MongoID of all 0's. This role is special because it is not an actual role within the MongoDB database, but can be used within configurations within the permission settings to say "Everyone has this permission". Many people assume that this special role is the same thing as Anonymous, but actually, they are different. Anonymous roles are granted to any user who does not have ANY x-jwt-token, whereas the Everyone role is granted to everyone regardless if they have a JWT token or not. This is helpful for example if you wish to allow Anonymous and Authenticated users the ability to view a form, you would use Everyone instead of Anonymous.

Group Roles

The other kind of roles that can be added to a user submission object is a Group Role assignment. This process is documented in great detail within the Group Permissions section of our documentation, so it is recommended to read through this documentation to understand how our group permissions work. From a roles perspective, one thing to note is that if you use the Group Assignment action within a form, this works in a very similar way as the Role Assignment action except that instead of assigning a Role ID as the id within the "roles" property, the ID of the group resource submission is instead used as the role id. This allows you to assign Groups to the permissions of a form and then the ID of that group is then used as a role on the user object.

Now that we understand how roles are assigned to users, we will now explore how Permissions are granted to complete the authorization process of users.

Permissions

In order to achieve authorization, the roles that a user has been assigned is compared to the permissions that have been configured on a resource within the form.io platform. Permissions are explained in detail within the Permissions section of our Users Guide, so we recommend reading this section of our documentation before diving into the mechanics of how our permission system works.

Once you have an understanding of how the Permissions operate, the next thing that is helpful is to understand the JSON configurations of each of these permissions so that you can understand how a user is authorized access to an entity within the Form.io platform.

Permission Scope

All permissions within Form.io are defined within certain "scopes" where the permission is applied. Each scope defines certain permissions to different entities within the Form.io platform, which are categorized as Project Scope, Form Scope, and Submission Scope. The location of the JSON configurations for the permissions for each of these scopes can be found in different places within a project API, and these locations are defined as follows.

Scope

Description

Entity

Property

Project Scope

Defines the project level access, which grants a user access to the form permissions of that project. For example, "read_all" permission on the project scope defines a users access to read all forms within that project. Admin level access defines the ability to update project settings.

project

access

Form Scope

Defines the form level access of a specific form.

form

access

Submission Scope

Defines the access of submissions within a specific form.

form

submissionAccess

As an example, let's suppose you wish to grant Anonymous users the ability to Read the form definition, but also to create and read their own submissions within a form, the following JSON configuration would be applied to the Form JSON definition.

{
  "title": "Test Form",
  "name": "testform",
  "path": "testform",
  "components": [
    ...
    ...  
  ],
  "access": [
    {
      "type": "read_all",
      "roles": ["123456789012345678901234"]
    }
  ],
  "submissionAccess": [
    {
      "type": "read_own",
      "roles": ["123456789012345678901234"]
    },
    {
      "type": "create_own",
      "roles": ["123456789012345678901234"]
    }
  ]
}

Every permission granted receives its own declaration within the scope array of that permission with the following format.

{
    "type": "role_type",
    "roles": ["role_1", "role_2"]
}

These types are described as follows.

Permission Types

As described in the users guide, permissions are categorized in several different types such as Read All, Read Own, Create All, Create Own, and many more. Each of these permissions has an "id" that is used within the JSON to determine the type of permission that is being granted. These permissions can then be granted within a certain permission scope

For example, if you wish to configure a Form to allow Read All access to Authenticated users, and the ID for the Authenticated role within your project is 123456789012345678901234 then the following JSON would represent this configuration within a form.

{
  "title": "Test Form",
  "name": "testform",
  "path": "testform",
  "components": [
    ...
    ...  
  ],
  "submissionAccess": [
    {
      "type": "read_all",
      "roles": ["123456789012345678901234"]
    }
  ]
}

This form configuration ensures that Authenticated users can read all the submissions that have been submitted within a form. This example uses the Submission permissions which are established on the Form JSON under the "submissionAccess" property, which is the scope of that permission.

Using the permission types along with the permission scopes, it is possible to granularly control access to all kinds of areas of the Form.io API and ensure that users only receive the access that they need an nothing more.

Last updated