Custom login requirements
Apostrophe's built-in login form handles the typical case where a user logs in with a username and password. And the @apostrophecms/passport-bridge module handles "single sign-on," in which users log in via a third-party system like Google Workspace, Twitter, Facebook, etc. These two solutions cover most situations. However in some cases developers may wish to extend the built-in login form with additional, custom login requirements. Often these are shipped as npm modules. This guide will explain how to implement such requirements.
The main focus of this guide is on implementing requirements involving custom UI, such as CAPTCHA, two-factor authentication (TOTP), "math problems" and other additional steps at login time. However we'll start by briefly addressing password complexity as it is a common requirement and implemented separately.
Password complexity rules
While most password complexity rules are often counterproductive, some organizations may have a business requirement to implement them. Currently the easiest rule to implement, and the most effective one, is to set a minimum length. You can do that by configuring the
@apostrophecms/user module at project level:
The phases of login
Apart from password complexity, most enhancements to the login form involve at least some new UI code. UI components added this way are integrated into the existing login interface. This involves both backend Node.js development and frontend Vue 2 component development.
Apostrophe allows you to add extra login requirements during two different phases of the login process:
beforeSubmit: Requirements in this phase are displayed before the user clicks the Login button. If the requirement has a visible UI, it appears at the bottom of the login form itself. Best used for requirements that have no visible interface but need to monitor user behavior on the form to determine if the user is "real," like reCAPTCHA v3.
afterPasswordVerified: Requirements in this phase are displayed after the form is completed, the Login button is clicked, and the password is verified. Best used for requirements that have a visible interface and which require any special knowledge about the user, such as a Two-Factor Authentication (2FA) challenge.
The server side
Each distinct requirement has its own server-side code, which must be added to the
requirements(self) module configuration function of the
@apostrophecms/login module at project level, or to an npm module that enhances it via
The object returned by
requirements is similar to that returned by
fields, in that it has an
add property, and each individual login requirement is a sub-property of
Each requirement must have a unique name. The name of each requirement should be CapitalizedInCamelCase, as it doubles as a Vue component name, as explained below.
Each requirement has:
phaseproperty, which must be
props()function, which may be
asyncand returns an object. The properties of the returned object become Vue
propsof the Vue component, as explained below. For
(req, user). The
reqobject allows access to the current Express request, including
req.sessionwhich can be useful for temporary storage not revealed to the browser. The
userobject allows access to the current user even though login is not yet complete.
verify()function, which may be
(req, data), for
(req, data, user). This function is responsible for throwing an exception if the requirement has not been met. Any data provided by the Vue component will be accessible here as the
afterPasswordVerifiedrequirements only, an optional
askForConfirmation: trueproperty. If present the corresponding Vue component is responsible for displaying its own success message and emitting a
confirmevent as described later. Otherwise flow proceeds automatically to the next step.
To illustrate, the general structure on the server side is:
The browser side
Each distinct requirement also has browser-side code, which is implemented as a Vue component. As explained in the custom UI guide, Vue components intended for the admin UI (including login requirements) must be placed in the
ui/apos/components subdirectory of a module in the project. For this purpose they are typically placed in the
modules/@apostrophecms/login/ui/apos/components module at project level, or in an npm module that enhances
The developer is responsible for the appearance of the component. For
beforeSubmit requirements, the component will appear at the bottom of the login form itself. For
afterPasswordVerified requirements, it will appear on its own, after the Login button is clicked and the password is verified. If there are multiple
afterPasswordVerified requirements, the user will see them presented one at a time. Transitions are provided by Apostrophe and do not need to be included in login requirement components.
Each component is responsible for:
- Presenting its own UI, if any. Note that
afterPasswordVerifiedrequirements will appear in isolation, one at a time, while any UI for
beforeSubmitcomponents will appear simultaneously with the login form.
- Accepting the properties of the object returned by the server-side
props(req)function as props.
- Emitting a
doneevent with a payload providing proof that the requirement has been met. This proof is accessible to the
verify(req)function on the server side as
req.body.requirements.RequirementName. In the case of
doneevent should not be emitted until the user has indicated their response is complete in some way.
beforeSubmitrequirements may emit a
blockevent to cancel a previous
doneevent so that the Login button cannot be clicked yet.
afterPaswordVerifiedrequirements are responsible for displaying a custom error message when the
errorVue prop is set to an error object.
afterPasswordVerifiedrequirements that set the
askForConfirmationproperty are also responsible for displaying a custom success message when the
successVue prop is true.
While there is no fixed structure for the Vue components, a typical outline looks like:
Here is complete server-side code for a simple requirement to solve a math problem when logging in. The UI appears at the bottom of the login form because the password has not been verified yet.
What is happening here?
props()function of the requirement is invoked before the form appears and returns an object whose properties become props for the custom Vue component shown below. Because it has access to
req, the Express request object represnting the current browser request, this function can store information about the "challenge" presented by the requirement in
req.sessionso that it remains available for verifying the result without disclosing the right answer to the browser.
verify()function of the requirement is invoked when the login form is completed and the user clicks Login. Note that Apostrophe won't allow that to happen until the custom Vue component emits a
doneevent, as shown below.
verify()also has access to
req, so it can consult
req.sessionto see if the response is correct.
verify()throws an error, the error is displayed and the login form can be submitted again. If no requirements throw an error, login proceeds, to the
afterPasswordVerifiedrequirements if any.
This simple example uses
req.session, however be aware that if your site uses bearer tokens for headless REST API logins you will need to store temporary information between requests in another way, such as by using the
@apostrophecms/cache module. Sessions are not available to headless API clients.
To complete the requirement we also need a Vue component on the browser side. As explained in the custom UI guide, Vue components intended for the admin UI (including login requirements) must be placed in the
ui/apos/components subdirectory of a module in the project, like this:
Don't forget to set the
APOS_DEV environment variable to
1 when developing admin UI code. Otherwise Apostrophe will not rebuild the admin UI automatically when you make changes to admin UI code, such as login requirement components.
If your project is derived from
starter-kit-essentials you can type:
APOS_DEV=1 npm run dev
If this is unfamiliar to you we recommend reading Customizing the user interface first.
What is happening here?
This Vue component has only one job: take the props returned by the server-side
props function, display an appropriate interface, and emit a
done Vue event with the user's response to the challenge. Everything else is handled for us by Apostrophe.
Apostrophe looks for a Vue component with the same name as the requirement.
Unlike the other phase, requirements displayed in the
afterPasswordVerified phase can potentially access information about the user when returning props and verifying responses. This is essential for implementing features like 2FA (such as Google Authenticator support) because an additional secret must be stored in Apostrophe's user document.
Below is a simple example of a requirement to solve a weak form of 2FA challenge: entering a code that is assigned the first time the user logs in successfully.
What is happening here?
This code isn't too different from the
MathProblem requirement. The most important differences are:
(req, user). While the user is not completely logged in yet, we have access to the
userpiece in these functions. This allows us to use a MongoDB
$seta random 2FA challenge code on the first call to
props(). Note that we only return the code the first time, as otherwise any script can "sniff" the right answer and defeat the requirement. In this simple example, the user is expected to remember it.
In a real 2FA system, a new challenge might be texted to the user's device each time — or a stored code much like this one, but longer and more secure, might be used to generate a time-based one-time password (TOTP) as with Google Authenticator.
A real 2FA system requiring a "setup code" like this one would also continue to display it until the user successfully verifies it for the first time, marking the code as confirmed, in case the user doesn't succeed in setting it up on the first try.
Here is the front-end Vue component code for this requirement:
What is happening here?
There's not much new here in comparison to the earlier requirement, which you should review as well. However note that the interface adapts to whether we're displaying a new
code for the first time or not.