This module serves as a starting point for adding authentication and RBAC (Role-Based Access Control) to your application. Before using the module for the first time, we recommend reading the official platformOS documentation on User Authentication Basics and on Example Application.
This module follows the platformOS DevKit best practices and includes the core module as a dependency, enabling you to implement patterns such as Commands and Events.
For more information,
- read the documentation about the built-in User table,
- learn about how platformOS manages sessions,
- and gain a high-level overview of authentication strategies in platformOS.
The platformOS User Module is available on the Partner Portal Modules Marketplace.
Before installing the module, please ensure you have pos-cli installed. This tool is essential for managing and deploying platformOS projects.
The platformOS User Module is fully compatible with platformOS Check, a linter and language server that supports any IDE with Language Server Protocol (LSP) integration. For Visual Studio Code users, you can enhance your development experience by installing the VSCode platformOS Check Extension.
-
Navigate to your project directory where you want to install the User Module.
-
Run the installation command:
pos-cli modules install user
pos-cli modules download user
This command installs the User Module along with its dependencies (such as pos-module-core and pos-module-common-styling and updates or creates the app/pos-modules.json
file in your project directory to track module configurations.
-
Install the module using the pos-cli.
-
Configure the common-styling to include default styles. It is recommended that you familiarize with the common-styling module by reading its README file. At ensures that your Layout includes:
a) pos-app
class in the root html
tag
b) css files from the common-styling module in the head section
c) js code to create a global pos
namespace in the head section
d) liquid code which displays built-in notifications after {{ content_for_layout }}
in the body section
Navigate to app/views/layouts/application.liquid to see a complete example of a layout file with the common-styling and user module stylesheets and js code included.
To generate a new migration, can use pos-cli migrations generate
command. Remember to change env
to your environment that you've used for the pos-cli env add
command. If you don't remember it, check the .pos
file.
pos-cli migrations generate <env> setup_user_default_role
Add the following line of code to the newly created migration file to set the default role to member
:
{% liquid
function result = 'modules/core/commands/variable/set', name: 'USER_DEFAULT_ROLE', value: 'member'
log result, type: 'setup_user_default_role result'
%}
Don’t forget to deploy your code to invoke the newly created migration:
pos-cli deploy <env>
- Overwrite default views that you would like to customize by following the guide on overwriting a module file. This allows you to add functionality based on your project requirements, such as extending the registration form with additional fields. At a minimum, you should overwrite the permissions file, where you will configure RBAC authorization roles and permissions for your application:
mkdir -p app/modules/user/public/lib/queries/role_permissions
cp modules/user/public/lib/queries/role_permissions/permissions.liquid app/modules/user/public/lib/queries/role_permissions/permissions.liquid
- Create a superadmin using the GraphQL Explorer.
- To create a new user, use the user_create mutation. The query to create a user with the email
[email protected]
and passwordpassword
would look as follows:
mutation user_create {
user: user_create(user: {
email: "[email protected]",
password: "password"
}) {
id
email
}
}
The default behavior of modules is that the files are never deleted. It is assumed that developers might not have access to all of the files, and thanks to this feature, they can still overwrite some of the module's files without breaking them. Since the User Module is fully public, it is recommended to delete files on deployment. To do this, ensure your app/config.yml
includes the User Module and its dependencies in the list modules_that_allow_delete_on_deploy
:
modules_that_allow_delete_on_deploy:
- core
- user
We recommend creating a new Instance and deploying this module as an application to get a better understanding of the basics and the functionality this module provides. When you install the module using pos-cli modules install user
, only the contents of the modules/user
will be available in your project. The app
directory serves as an example of how you could incorporate the User Module into your application. When analyzing the code in the app
directory, pay attention to the following files:
app/config.yml
andapp/user.yml
: These files are defined according to the Setup instructions.app/views/page/index.liquid
: Implements the example application homepage, displaying information about the currently logged-in user if available, or providing links for registration, sign-in, and password reset functionality for unauthenticated users.app/views/pages/admin/index.liquid
: Implements the/admin
page, demonstrating permission-checking functionality.app/modules/user/public/lib/queries/role_permissions/permissions.liquid
: Demonstrates how to configure permissions in your application by overwriting a module file.
Once the module is installed and you have completed the setup, you will immediately gain access to the new endpoints created by this module. For example, you can navigate to /users/new
for the registration form or /sessions/new
for the login form. This section describes all the functionalities provided by this module and includes a roadmap for potential future enhancements.
To provide your own HTML (for example, for styling purposes), you can leverage the fact that all of the endpoints use the theme_render_rc Liquid tag. This means you can easily provide your own HTML for each endpoint by creating a partial in the app
directory. For example, you can create app/views/partials/users/new.liquid
for the registration endpoint. Refer to the Endpoints section for each functionality to see which partial to create for which endpoint.
- Registration:
Provides CRUD operations (Create, Read, Update, Delete) for user management and implements the necessary endpoints for user registration. These views are located in themodules/user/public/views/pages/users/
directory. This handles essential user registration processes in platformOS. - Session-Based Authentication:
This feature enables Sign In and Sign Out forms, providing session-based authentication. It includes endpoints and forms for securely managing user sessions on the platform. - Reset Password:
Enables users to reset their password through defined endpoints in themodules/user/public/views/pages/passwords/
directory. Note that on the staging environment, email notifications require additional configuration to send email. - RBAC Authorization: Implements Role-Based Access Control (RBAC), allowing fine-grained authorization management based on user roles. This lets you define who can access specific parts of your platform based on assigned roles.
- Impersonation - logging as another user: This feature allows logging in as another user, providing access to all of their functionalities. It comes with a dedicated logout process which logs user back to their original profile. It includes security consideration that only superadmin can impersonate another superadmin user.
- OAuth Module Integration:
This feature allows users to authenticate using external identity providers (such as Google, Facebook, or GitHub).
TODO (Upcoming Features)
- 2FA Authentication:
Adds Two-Factor Authentication (2FA), including:- Displaying OTP secret QR code for setting up 2FA. More details can be found in the OTP secret object documentation.
- OTP Verification: After setting up 2FA, the user will need to verify the code. Refer to the OTP verification documentation.
- Mandatory Email Verification (Feature Flag):
Adds a feature flag to enforce mandatory email verification, ensuring users validate their email addresses before gaining full access to the platform. - Mandatory SMS Verification (Feature Flag):
Similar to email verification, this feature flag will enforce mandatory SMS verification during the registration process to improve account security. - JWT Authentication:
Enables JWT (JSON Web Token) authentication, allowing for secure, stateless authentication through token-based authentication mechanisms. - Built-in CAPTCHA Protection:
Adding CAPTCHA to various user authentication forms to prevent spam and automated sign-ups, improving platform security.
The User module is fully functional and requires no modifications to implement a basic registration process. This module provides essential CRUD operations for the built-in User object in platformOS, enabling you to manage users.
With these CRUD commands, you can handle typical user management operations such as registering new users, updating user information, and managing user data through platformOS’s API.
The table below outlines the resourceful routes provided for registration functionality:
HTTP method | slug | page file path | description | partial rendering HTML |
---|---|---|---|---|
GET | /users/new | modules/user/public/views/pages/users/new.liquid |
Renders a registration form with inputs for email and password. | app/views/users/new.liquid |
POST | /users | modules/user/public/views/pages/users/create.liquid |
Adds a new User to the database and logs the user in or re-renders the form if validation errors occur. You can modify the redirect path using the redirect_to param or by setting return_to Session field. |
app/views/users/new.liquid |
The User module provides basic CRUD (Create, Read, Update, Delete) functionality for managing users in platformOS. You can run commands such as creating or deleting users.
The User module provides basic CRUD (Create, Read, Update, Delete) functionality for managing users in platformOS. You can run commands such as creating or deleting users.
You can create:
function result = 'modules/user/commands/user/create', email: '[email protected]', password: 'password', roles: []
load:
function user = 'modules/user/queries/user/load', id: '1'
Note
Usually, you will want to load the currently authenticated user. You can achieve this by providing context.current_user.id as the ID:
function user = 'modules/user/queries/user/load', id: context.current_user.id
update:
function user = 'modules/user/queries/user/update', id: '1', email: '[email protected]', password: 'password'
and delete:
function result = 'modules/user/commands/user/delete', id: '1'
The POST /users
endpoint defined in modules/user/public/views/pages/users/create.liquid
assigns the default role specified by the constant DEFAULT_USER_ROLE
—see Setup for more information.
The following table outlines the resourceful routes for sign-in and sign-out functionality:
HTTP method | slug | page file path | description | partial rendering HTML |
---|---|---|---|---|
GET | /sessions/new | modules/user/public/views/pages/sessions/new.liquid |
Renders a sign-in form with inputs for user's email and password. | app/views/partials/sessions/new.liquid |
POST | /sessions | modules/user/public/views/pages/sessions/create.liquid |
Creates a session for the authenticated user based on password authentication or re-renders the sign in form if credentials do not match. You can modify the redirect path with a param called redirect_to or by setting return_to Session field. |
app/views/partials/sessions/new.liquid |
DELETE | /sessions | modules/user/public/views/pages/sessions/delete.liquid |
Invalidates the current session and logging the user out. | N/A |
Example: You can define a hook_user_login
that redirects a user to /admin
if they have the admin_pages.view
permission.
assign redirect_to = '/'
function can = 'modules/user/helpers/can_do', requester: params.user, do: 'admin_pages.view'
if can
assign redirect_to = '/admin'
endif
assign res = '{}' | parse_json | hash_merge: redirect_to: redirect_to
return res
You can log the user in (which creates a new session) using the following command:
function res = 'modules/user/commands/session/create', email: '[email protected]', password: 'password'
This command also triggers the hook_user_login
hooks.
To access information about the currently logged-in user, use the following command provided by the module:
function profile = 'modules/user/helpers/current_profile'
This command is implemented in modules/user/public/lib/helpers/current_profile.liquid
. When you investigate the file, you'll notice that it not only loads the user's profile information from the database but also extends the profile's roles with either authenticated or anonymous if the user is not currently logged in. User object is also available under profile.user when the user is logged in.
To log out a user and destroy their session, run the following command:
function res = 'modules/user/commands/session/destroy'
This command triggers the hook_user_logout
hooks.
It’s possible to skip password validation and create a session while triggering the hook_user_login
by setting the validate_password
boolean argument to false
when calling the modules/user/commands/session/create
command. In this case, the user_id
argument must be provided.
function res = 'modules/user/commands/session/create', user_id: '1', validate_password: false
The reset password functionality consists of two resources: password
and authentication_link
.
The table below contains the resourceful routes provided for the reset password functionality, ordered based on the flow. The process begins with GET /passwords/reset
and ends at POST /passwords/create
, which updates the password and redirects the user to the sign-in page.
HTTP method | slug | page file path | description | partial rendering HTML |
---|---|---|---|---|
GET | /passwords/reset | modules/user/public/views/pages/passwords/reset.liquid |
Renders a reset password form. | app/views/partials/passwords/reset.liquid |
POST | /authentication_links | modules/user/public/views/pages/authentication_links/create.liquid |
Generates a link with temporary token and sends an email using the modules/user/commands/emails/auth-link command to the email address provided by the user in the reset password step. |
app/views/partials/passwords/reset.liquid |
GET | /passwords/new | modules/user/public/views/pages/passwords/new.liquid |
This endpoint authenticates the user using the temporary token using modules/user/helpers/user_from_temporary_token helper and, if successful, it renders a form where the user can provide their new password. |
app/views/partials/passwords/new.liquid |
POST | /passwords/create | modules/user/public/views/pages/passwords/create.liquid |
Overwrites the existing user's password with the new password and redirects the user to the GET /sessions/new endpoint, so they can log in. |
app/views/partials/passwords/new.liquid |
To create or update a given user's password:
function result = 'modules/user/commands/passwords/create', object: object
To create an authentication link
that points to the GET /passwords/edit
endpoint, which will be sent to the user's email, use the following command. The authentication link will include a temporary token that authenticates a user based on their email only:
function object = 'modules/user/commands/authentication_links/create', email: "[email protected]", host: context.location.host
The module provides the foundation for implementing Role-Based Access Control (RBAC) in your platformOS application. As described in the registration section, all users initially receive the role defined by the DEFAULT_USER_ROLE
constant.
There are three built-in roles that are provided out of the box by the module:
This role is artificially granted in modules/user/public/lib/queries/user/current.liquid
. It represents an anonymous user, for which you might want to still want to check some permission. For example, only anonymous users should be able to sign in or register into the system. This is why you will see built-in permissions in modules/user/public/lib/queries/role_permissions/permissions.liquid
for users.register
and sessions.create
, which are checked in Endpoints for Sign-In and Endpoints for the registration.
A typical use case for the anonymous user role is on an eCommerce website, where anonymous users can purchase items without registering.
This role is artificially granted in modules/user/public/lib/queries/user/current.liquid
.It represents an authenticated user. The typical use case for this role is to restrict access to certain areas for anonymous users, without assigning a specific role to the user yet. For example, authenticated users might access free content, but to access paid content, they would need to subscribe, after which the system would grant them an additional role like member
or subscriber
with extra permissions.
Any user with the superadmin
role will have immediate access to any permission checked in the Authorization Commands.
The roles are stored in User's property named roles
.
You can append role using the append
command:
function result = 'modules/user/commands/user/roles/append', id: 1, role: "admin"
You can remove role using the remove
command:
function result = 'modules/user/commands/user/roles/remove', id: 1, role: "admin"
You can set multiple roles at once using the set
command:
assign roles = '["member", "admin"]' | parse_json
function result = 'modules/user/commands/user/roles/set', id: 1, roles: roles
You can fetch all defined roles in the system by using modules/user/queries/roles/all
query:
function roles = 'modules/user/queries/roles/all'
You can fetch all custom roles (without authenticated
and anonymous
) defined in the system by using modules/user/queries/roles/custom
query:
function roles = 'modules/user/queries/roles/custom'
The module offers several helper commands to authorize users:
This command returns true
or false
depending on whether the user has permission to perform the operation defined by the do
argument. It is useful for modifying the UI based on permissions, ensuring that functionalities a user does not have access to are not displayed.
function current_user = 'modules/user/queries/user/current'
function can = 'modules/user/helpers/can_do', requester: current_user, do: 'admin_pages.view'
It can also accept additional entity
and access_callback
arguments, which allow you to [write your own authorization rules](#creating your-own-authorization-commands) cleanly.
If the user does not have permission, the system renders a 403 Unauthorized page, and the flow stops. Optionally, it accepts redirect_anonymous_to_login
(boolean) parameter - if set to true, if the user is unauthenticated, instead of rendering 403, it will redirect the user to /sessions/new endpoint and upon login redirect back to anonymous_return_to
parameter (defaults to current page - context.location.href).
This command uses the deprecated include
tag to work with the break
Liquid tag properly - we do not want to execute code past this point if the user has no permission.
function current_user = 'modules/user/queries/user/current'
# platformos-check-disable ConvertIncludeToRender, UnreachableCode
include 'modules/user/helpers/can_do_or_unauthorized', requester: current_user, do: 'users.register', redirect_anonymous_to_login: true
# platformos-check-enable ConvertIncludeToRender, UnreachableCode
The platformos-check-disable
and platformos-check-enable
tags are used to prevent the platformOS-check from reporting a warning for using the include
tag instead of the recommended render
tag.
If the user does not have permission, they will be redirected to the URL provided as an argument. This command uses the deprecated include
tag to work with the break
Liquid tag properly - we do not want to execute code past this point if the user has no permission.
function current_user = 'modules/user/queries/user/current'
# platformos-check-disable ConvertIncludeToRender, UnreachableCode
include 'modules/user/helpers/can_do_or_redirect', requester: current_user, do: 'users.register', return_url: '/sessions/new'
# platformos-check-enable ConvertIncludeToRender, UnreachableCode
The platformos-check-disable
and platformos-check-enable
will let platformos-check to not report a warning for using the include
tag instead of the render
tag.
You can leverage existing commands while providing custom authorization rules by using the access_callback
and entity
arguments. Consider the following complex real-life example:
{% assign order = '{"buyer_id": 1, "seller_id": 2}' | parse_json %}
function can = 'modules/user/helpers/can_do', requester: user, do: 'orders.confirm', entity: order, access_callback: 'can/orders'
In this example, the access_callback
is set to can/orders
. We recommend placing all your authorization rules in one location: app/lib/can
, naming the file after the relevant Resource.
The entity
represents the object for which you want to check if the requester has access. In this example, it’s an eCommerce order.
The example implementation of the app/lib/can/orders.liquid
might be as follows:
{% liquid
assign order = entity
if order == blank
return false
endif
# For simplicity, there is a special `orders.manage.all` permission that allows the requester to do anything with orders.
# Because it leverages the `can_do` helper, it will always return true for superadmins.
function can_manage_all = 'modules/user/helpers/can_do', requester: requester, do: 'orders.manage.all'
if can_manage_all
return true
endif
# Check if the requester has a role that grants permission equal to `do`
function can = 'modules/user/helpers/can_do', requester: requester, do: do
if can
case do
when 'orders.confirm'
if requester.id == order.seller_id
return true
endif
when 'orders.buyer_cancel'
if requester.id == order.buyer_id
return true
endif
endcase
endif
return can
%}
The rules can be summarized as follows:
- If the requester has access to the
orders.manage.all
permission, do not check granular permissions (likeorders.confirm
in the example), just grant access to all actions related to orders. - If the requester does not have
orders.manage.all
, check whether they have permission for the specific action represented bydo
( likeorders.confirm
in the example). - If the requester has the
do
permission, check if they should have access to that particular order. Having theorders.confirm
permission does not imply access to confirm all orders - sellers should only be able to confirm their own orders. In this example, theseller_id
must match therequester.id
. In more complex scenarios, you may need to perform additional GraphQL queries to enrich therequester
ororder
data, ensuring accurate authorization.
Roles and their permissions are defined in the modules/user/public/lib/queries/role_permissions/permissions.liquid
file in a simple JSON format. You can modify this file to suit your application’s requirements. For example, if you want to add a new role, such as foo, with permissions foo.show and foo.manage, you can extend the JSON as follows:
"foo": [
"foo.show",
"site.manage"
]
If you receive a 500 error after modifying the permissions.liquid
file, check the logs for hints. The error might be due to an invalid JSON format, such as a missing or extra comma. For example, the following modification will raise an error due to a trailing comma:
"foo": [
"foo.show",
"site.manage", <- this comma will cause an error
]
-
- Naming Convention for Permissions: Use the format
<resource>.<do>
for naming your permissions. For example:
article.create
article.edit
comment.delete
- Naming Convention for Permissions: Use the format
- Managing Permissions for Administrators/Moderators: Use
<resource>.manage.all
for permissions granted to administrators or moderators. Regular users should typically have permission only for the entities they create. For example, in a blog application that allows users to write comments, users should be able to edit and delete their own comments. In contrast, administrators and moderators need permission to manage all comments. We recommend creating a<resource>.manage.all
permission for this purpose and handling it as described in the Creating Your Own Authorization Commands example.
This functionality allows a user with users.impersonate
permission to log in as another user (unless they are superadmin - in which scenario users.impersonate_superadmin
permission is needed). It comes with a dedicated logout process which logs the user back to their original profile.
This feature allows users to authenticate using external identity providers such as Google, Facebook or GitHub.
Configuring OAuth2 requires installing selected additional OAuth2 provider modules. Please follow the instructions provided by each installed module to configure it.
To implement a custom OAuth2 provider, you must provide two helper methods:
get_redirect_url
- which generates a redirect URL to begin the OAUTH2 flow.
get_user_info
- which returns a dictionary:
Key | Value |
---|---|
sub | User's id in the external system |
first_name | User's first name. |
last_name | User's last name. |
User's email. | |
valid | A boolean indicating whether the flow was successful or not. |
The table below contains the resourceful routes provided for the logging as functionality, ordered based on the flow.
HTTP method | slug | page file path | description | partial rendering HTML |
---|---|---|---|---|
POST | sessions/impersonations | modules/user/public/views/pages/sessions/impersonation/create.liquid |
This feature allows an administrator to log in as another based on user_id parameter. Only users with the superadmin role can log in as another superadmin user. The original user ID is stored in the session field named original_user_id |
|
DESTROY | /sessions/impersonations | modules/user/public/views/pages/sessions/impersonation/destroy.liquid |
Logging back in as the original user whose id was stored in original_user_id session field. |
modules/community/public/views/partials/components/organisms/logging_back.liquid |
You can easily customize the User module by overwriting a module file. This allows you to modify specific functionalities without changing the original code. Below are some common customization examples.
By default, the module creates a Page with the /users/new
endpoint for the registration form. If you'd like to change it (e.g., to /sign-up
), you can do so by overwriting this page and updating the slug
.
Steps:
- Create the necessary nested directories to be able to place your overwrite file there:
mkdir -p app/modules/user/public/views/pages/users/
- Copy the original page file you would like to overwrite:
cp modules/user/public/views/pages/users/new.liquid app/modules/user/public/views/pages/users/new.liquid
-
Edit the overwrite file located in
app/modules/user/public/views/pages/users/new.liquid
. Modify the YAML section of the page to change theslug
. (Make sure you are modifying the overwrite and not the original file—overwrites are located in the<project root>/app/modules
directory, whereas the original files are located in the<project root>/modules
directory.) -
Modify the Liquid code - explicitly define
slug
property tosign-up
of theapp/modules/user/public/views/pages/users/new.liquid
Page to define/sign-up
endpoint:
---
slug: sign-up
---
{% raw %}
{% liquid
function current_user = 'modules/user/queries/user/current'
include 'modules/user/helpers/can_do_or_unauthorized', requester: current_user, do: 'users.register'
function registration_fields = 'modules/user/queries/registration_fields/load'
theme_render_rc 'modules/user/users/new', context: context, registration_fields: registration_fields
%}
{% endraw %}
As a result, the registration form will now be available at /sign-up
, and the /users/new
URL will render a 404 Not Found error.
If you want to modify the Sign-In form's HTML, you can overwrite the Partial responsible for rendering the form. If you want to modify the Sign-In form's HTML, you can overwrite the Partial responsible for rendering the form.
Steps:
- Create the directory for the overwrite. Since we are overriding the presentation layer, we should overwrite the Partial.
mkdir -p modules/user/public/views/partials/sessions
- Copy the partial responsible for rendering the sign-in form:
cp modules/user/public/views/partials/sessions/new.liquid app/modules/user/public/views/partials/sessions/new.liquid
Note
Please note that in this example, we are working with the sessions
directory, as this is where the Sign-In functionality is located.
Now, you can freely modify the HTML or presentation layer of the sign-in form in the copied file.
[!NOTE]
Please note that in this example, we are working with the sessions
directory, as this is where the Sign-In functionality is located.
The module also provides several useful queries and commands to help you manage users and permissions
function users_count = 'modules/user/queries/user/count'
function current_user = 'modules/user/queries/user/current'
function current_user = 'modules/user/queries/user/load', id: 1
To manage versioning with Git and npm, you can follow these commands:
git fetch origin --tags
npm version major | minor | patch