Modstack Identity and User Management

Enterprise-grade authentication and user management for your applications, powered by the OpenID Connect standard.

Overview

The Modstack Identity and User Management module is a turnkey solution that provides enterprise-grade authentication and user management. Based on the industry-standard OpenID Connect (OIDC) protocol, it is the recommended way to add authentication to any application built on ModStack.

Under the hood, the module is powered by Keycloak, an open-source, OIDC-certified identity provider and one of the most widely adopted solutions for deploying OIDC endpoints in production. By building on Keycloak, the module benefits from a mature, battle-tested codebase with an active open-source community — while ModStack handles the setup, configuration, and operational overhead for you.

The module provides a simple, one-click setup within your own account — no third-party SaaS dependency required. It can serve as the authentication backend for web applications, single-page applications (SPAs), or mobile applications. During installation, you specify a PostgreSQL database that will be used to store all authentication and user data.

The module handles all standard authentication flows out of the box: registration, login, logout, password reset, two-factor authentication, and more.

Why use this module?

When adding authentication to an application, you typically have two choices: build the flows yourself, or use an off-the-shelf solution.

Building authentication from scratch requires a considerable investment of engineering time to correctly implement registration, login, token management, password resets, multi-factor authentication, and session handling. Given this complexity, it is now standard practice to use a proven OIDC solution rather than building all of these scenarios yourself. You could choose a commercial SaaS solution such as Auth0, but these come with significant account limits and feature gates that quickly become expensive as you scale.

How ModStack is different

Simple setup

Deploy a full authentication backend with a single click — no need for an in-depth understanding of OIDC. The module handles token issuance, session management, and protocol compliance for you.

Multi-tenant by default

Tenants are a first-class construct — not an add-on or a premium feature. Create and manage an unlimited number of tenants through the UI or the API, with full user isolation.

No usage limits or feature gates

No caps on users, tenants, or organizations. You do not pay per monthly active user. MFA, recovery codes, invite-only registration, brute force detection, audit logs, and tenant management are all included.

No third-party SaaS dependency

Runs entirely within your own infrastructure. Your authentication data never leaves your environment, and you are not dependent on the availability or pricing changes of an external vendor.

Innovative features

Capabilities you won't find in off-the-shelf solutions. Invite-only registration mode lets you control exactly who can sign up to your service using single-use or bulk invite codes — ideal for early-access launches and controlled rollouts.

Reduced lock-in

Built on the open-source, OIDC-certified Keycloak project. Export your database and import it into a standalone Keycloak deployment at any time — users, tenants, credentials, and configuration all transfer cleanly.

Features

Registration & Authentication

The module handles the full user authentication lifecycle out of the box:

  • User registration — a standard registration screen for new users. The default fields are email and password; optionally, you can enable first name and last name. All screens can be customized with your own HTML templates (see UI Customization).
  • Login — a standard login screen that authenticates users and redirects them to the home URL configured in the OIDC application. If two-factor authentication is enabled, users are prompted for their one-time password after completing the login.
  • Password recovery — if a user has forgotten their password, they can use the password recovery screen to request a reset link.
  • Logout — standard logout with configurable post-logout redirect URLs.

Registration Styles

The module supports two registration styles:

Open registration

This is the default. Any user who has access to the registration URL can sign up for an account.

Invite-only registration

In this mode, users need a valid invite to register. This is typically used when launching a new product and you want to control who has access to the system.

Invites can be generated in two ways:

  • Single-user invite — creates an invite tied to a specific email address. Only that user can register with the invite link, and the link can only be used once.
  • Bulk invite — creates a shared invite link that allows a configurable number of users to register. For example, you can create a bulk invite allowing up to 10 users to register and share the link with a client.

Both types of invites can be generated from the ModStack Runtime API (see Registration Invites).

The default registration style is open, but this can be changed at any time from the module Configuration UI.

Multi-Tenancy & User Management

The system is multi-tenant by default. Each account created on the system is an independent tenant with its own isolated user pool. The owner of a tenant can invite other users to join the tenant — refer to the Runtime API for details on creating tenant invites.

  • Tenant isolation — each tenant has its own user pool, ensuring complete data separation.
  • User administration — create, update, disable, and delete users through the Runtime API.
  • Invite flows — invite users to a tenant via email, with configurable onboarding workflows.
  • Tenant-scoped tokens — issued tokens include the tenant context, making it easy for your services to enforce tenant boundaries.

Two-Factor Authentication

Two-factor authentication (2FA) adds an additional layer of security by requiring a one-time code from an authenticator app during login. By default, 2FA is not enabled.

You can configure 2FA in two ways:

  • Mandatory during registration — if you want every user to set up 2FA when they create their account, enable the Require 2FA during registration option in the Configuration UI.
  • Per-user, on demand — if you want specific users to enable or disable 2FA dynamically, use the ModStack Runtime API (see User Management). When 2FA is enabled for a user who hasn't set it up yet, they will be prompted to configure it on their next login.

Recovery Codes

Recovery codes allow users to log into the system if they have lost their two-factor authentication device. Once logged in with a recovery code, the user can reset their 2FA to set up a new device.

By default, recovery codes are not enabled. You can configure them in two ways:

  • Mandatory during registration — toggle the Enable recovery codes during registration option in the Configuration UI to require all users to save recovery codes when they first register.
  • Per-user, on demand — use the ModStack Runtime API to enable or disable recovery codes for individual users.

Password Strength & History

Configure the minimum password requirements for your users, including minimum length and the required number of lowercase and uppercase characters. By default, the minimum password length is 8 characters and at least one uppercase character is required.

You can also enforce a password history policy to prevent users from reusing recent passwords. For example, you can configure the system to reject any of a user's previous 10 passwords, ensuring that password rotations are meaningful.

All of these requirements are set in the Password strength section of the Configuration UI.

Brute Force Attack Detection

In a brute force attack, an attacker points an automated script at your login page and hammers it with password guesses — either working through a dictionary against one account, or trying leaked passwords across many accounts. Left unchecked, a bot can attempt thousands of guesses per minute. The module detects consecutive login failures per account and responds with two complementary defenses:

  • Quick-retry throttling — real users take seconds to retype a password; bots retry within milliseconds. Successive attempts that land inside the configured quick-retry window (default 1 second) are identified as automated, and the module enforces a minimum wait (default 5 seconds) before the next attempt is accepted. This collapses a bot's guess rate from thousands per minute to a handful, while a human typing at normal speed never notices it.
  • User lockout — throttling slows an attacker down but doesn't stop a patient one. After a configurable number of consecutive failures (default 3), the account is locked so the guessing cannot continue at any speed. A locked account can be unlocked by an administrator through the Runtime API.

Both defenses are enabled by default and work together: throttling absorbs fast automated bursts without inconveniencing real users who mistype once or twice, while lockout caps the total number of guesses an attacker can ever make against an account. The detection switch, failure threshold, quick-retry window, and enforced wait are all configurable in the Brute force attack detection section of the Configuration UI.

Audit Logs

The module maintains a log of user actions over a configurable period of time. Logged events include login, logout, session expiry, password reset, and other authentication-related actions.

UI Customization

All user-facing screens — registration, login, password reset, and email templates — can be fully customized by providing your own HTML templates. This allows you to match the authentication experience to your application's branding and design language. Keycloak uses FreeMarker templates (.ftl) for the UI. You can learn more about Keycloak UI templating in the Keycloak Theme Developer Guide.

The default Keycloak theme used by the Modstack Identity and User Management module is open-sourced and available at github.com/Modstack-dev/modstackpublic/tree/main/keycloak-theme. You can create a new theme based on these files by modifying the styles, colors, fonts, and layout to match your application.

You can also use an AI assistant to generate a custom theme for you. Use the following prompt with your preferred model:

AI Prompt
Generate a new Keycloak UI theme compatible with Keycloak 26.x. Use the .ftl templates provided at https://github.com/Modstack-dev/modstackpublic/tree/main/keycloak-theme but customize the style, colors, fonts etc. based on <enter your site URL here>. Keep the same directory structure with 'modstack' as the top level directory — that will be the name of the theme. Regenerate all the .ftl templates provided at https://github.com/Modstack-dev/modstackpublic/tree/main/keycloak-theme. Store the output directory at /tmp/theme. Also generate static HTML previews of these templates and locate them at /tmp/themepreview. Important: only change presentation (CSS, layout, fonts, colors, imagery). Keep every FreeMarker directive, error-handling guard, and script include exactly as in the source templates — in particular the inline messagesPerField error rendering, the kcSanitize?? guard and statusCode-based 404 branch in error.ftl, the register.ftl file (do not replace it with register-user-profile.ftl), js/ms-loading.js and its script tag, and theme.properties. Generate the same set of files as the source repo — no files added or removed.

Configuration UI

The module administration UI is available in the ModStack console. Use it to install the module and configure system-level features that apply to the entire deployment — registration style (open vs invite-only), mandatory two-factor authentication, password strength requirements, brute force detection thresholds, and OIDC clients. To configure specific tenants or users, use the Runtime API.

The Configure screen is organized into six tabs, each containing one or more sections. Every section has an Update button that applies its changes immediately; changes are executed as a background task and the result is reported inline. The field labels below match the console exactly, so this page doubles as a reference for the in-UI info icons.

Authentication

Control how users sign in to your application — login and session behavior, password strength requirements, brute-force protection, and which events are recorded to the audit log.

Authentication details

Tunes the core behavior of the registration and login flows — what users must provide and verify when they create an account, and how the realm presents itself.

FieldTypeDefaultDescription
Require email verificationToggleOffRequire users to verify their email when they create an account. When enabled, an activation email is sent to the user's address and the account activates only after they follow the link. Requires SMTP: the SMTP settings must be populated first — verification emails cannot be sent without a configured mail server.
Require Two Factor setup during signupToggleOffRequire users to set up two-factor authentication when they create an account. When disabled, users can still enable 2FA themselves after logging in.
Require Two Factor recovery codes setup during signupToggleOffRequire users to configure 2FA recovery codes when they create an account, so they can recover access if they lose their authenticator device.
Require First Name and Last NameToggleOffAdds mandatory first-name and last-name fields to the registration page.
Realm display nameText (1–80 chars)The display name of the authentication realm. Set this to your company name — it is shown, for example, when users add the account to their authenticator app.

Password strength

Sets the complexity policy that user passwords must meet, and how many previous passwords are remembered to prevent reuse. See the Password Strength & History feature overview for how this is enforced.

FieldTypeRangeDescription
Minimum lengthInteger8–50Minimum length of user passwords.
Minimum number of upper case charactersInteger1–10Minimum number of upper-case characters required in user passwords.
Minimum number of special charactersInteger1–10Minimum number of special characters required in user passwords.
Password history to retain to avoid reuse of passwordsInteger10–100Number of previous password hashes retained per user — passwords are never stored in plaintext. A new password is rejected if it matches any of the retained hashes.

Audit log settings

Controls which authentication events are recorded and how long they are retained. The recorded events are viewable on the Audit logs tab.

FieldTypeDefaultDescription
Enable user eventsToggleOnLog user events — login, logout, password change, and similar.
User event expiration (secs)Integer (3,600–2,592,000)604,800 (7 days)User events are kept until this many seconds have elapsed.
Enable admin eventsToggleOffLog administrative events — realm and client configuration changes.
Show detail in the admin eventsToggleOffInclude the full change payload in recorded admin events.
Admin event expiration (secs)Integer (3,600–2,592,000)604,800 (7 days)Admin events are kept until this many seconds have elapsed.

Brute force attack detection

Detects repeated failed logins and slows down or locks out the offending account, protecting your users against password-guessing bots. See the Brute Force Attack Detection feature overview.

FieldTypeDefaultDescription
Enable Brute force attack detectionToggleOnMaster switch for brute-force detection.
Lock user on multiple login failuresToggleOnLock the user account when repeated login failures are detected. Locked users can be unlocked via the Runtime API.
Number of login failures after which to lock accountInteger (3–10)3The account is locked after this many successive failures.
Quick retry window (milliseconds)Integer (500–5,000)1,000Time window in which repeated login attempts are identified as a “quick” retry. Quick retries are throttled to stop rapid automated attempts.
Minimum wait after a quick retry (secs)Integer (5–120)5The minimum delay enforced before another login attempt is allowed after a quick retry.

OAuth clients

Manage the OAuth/OIDC clients that applications use to authenticate against this module. Create one client for each of your applications that integrates with the auth infrastructure — for example one for your web app, one for your CLI, and one for your mobile app. Clients can be added, updated, and deleted from this tab.

What is an OAuth client?

An OAuth client is the registered identity of one of your applications inside the authentication system. When your application sends a user off to log in, it identifies itself with its client ID; after the user authenticates, the module issues tokens to that client and will only redirect the user back to the URLs registered for it. The client therefore defines the contract between your application and the auth infrastructure: what kind of application it is, where users may be sent after login and logout, and which browser origins may call the auth server.

Registering one client per application keeps those contracts separate — your web app, CLI, and mobile app each get their own client ID, their own allowed URLs, and their own login/registration links, so a change (or a compromise) in one application never affects the others.

Client types

To integrate with the Modstack Identity and User Management module, you need to create one or more OIDC client applications. The module supports two types of clients:

Private clients are used with the Authorization Code flow (also called the Standard flow in Keycloak). These clients are capable of securely storing a client secret and are used for standard server-side web applications.

Public clients are used for applications that cannot securely store a secret — typically CLI tools, mobile applications, or single-page applications (SPAs). For SPAs, the recommended flow is the Standard flow with PKCE enabled. For CLI applications, the recommended flow is the Device Grant flow.

Flow Client Type Use Case
Standard flow (Authorization Code) Private Server-side web applications
Standard flow with PKCE Public SPAs and mobile applications
Device Grant Public CLI applications

Client fields

Each client entry carries the following fields. The client type and ID identify the client and cannot be changed after creation; everything else can be updated at any time.

FieldTypeDescription
OAuth client typeDrop-downOne of PrivateStandard, PublicStandardPKCE, or PublicDeviceGrant (see the flow table above). Cannot be changed after creation.
Client IDText (1–80 chars, alphanumeric and dashes)The name used as the ID of your application. Cannot be changed after creation.
DescriptionText (1–80 chars)A human-readable description of the client.
Home page URLURLThe main URL of your application.
Web originsURL list (1–25 entries)Allowed CORS origins for browser-based requests. These origins are permitted to make direct token and API requests to the Keycloak server from client-side JavaScript. Use * to generalize.
Valid redirect URLsURL list (1–25 entries)The list of valid URLs that the user can be redirected to after a successful login. Use * to generalize.
Post logout URLsURL list (1–25 entries)The list of valid URLs that the user can be redirected to after logging out. You can specify a particular URL as part of the logout link to control where the user lands after signing out. Use * to generalize.

Login URLs

Every client row has a Show Login URLs panel with ready-to-use, copyable OIDC URLs for that client:

URLDescription
Registration URLDirect link your application can use to send users to the registration page for this client.
Login URLDirect link your application can use to send users to the login page for this client.
Logout URLDirect link your application can use to log users out of this client.

User Registration

Configure how new users join — self-registration or invite-only — along with email verification and related sign-up behavior.

Registration Style

Chooses the type of registration for your application. With Open registration, any user with the registration URL can sign up. With InviteOnly, only users holding a valid invite link can register — see the Registration Styles feature overview and the Runtime API for generating invites. The mode can be changed at any time and applies immediately.

FieldTypeDefaultDescription
Select Registration StyleDrop-downOpenOpen or InviteOnly. A common pattern is to start with InviteOnly during a private launch and switch to Open when you are ready.

SMTP

Configure the outgoing mail server used to send emails such as verification, password reset, and invitation messages.

SMTP settings

Connection and identity settings for the outgoing email server. This section also carries a Reset action that clears the stored SMTP configuration and returns the section to its defaults.

FieldTypeDefaultDescription
From Email AddressEmailThe From address for any emails sent out by the module.
From Address display nameText (1–255 chars)The display name shown alongside the From address.
SMTP hostHostnameHostname of your SMTP server, e.g. smtp.sendgrid.net.
SMTP portInteger (1–65,536)587Usually 587 for STARTTLS or 465 for implicit TLS.
Auth requiredToggleOnWhether the SMTP server requires authentication.
UserNameText (1–255 chars)Username used to authenticate against the SMTP server.
SMTP passwordSecretPassword or API key used to authenticate against the SMTP server. Stored securely and never displayed back in the UI.
SSLToggleOffWhether SSL is required to connect to the SMTP server.
StartTLSToggleOnWhether StartTLS is required to connect to the SMTP server.

IDP integration

Connect external identity providers such as GitHub and Google so users can sign in with their existing accounts. The tab contains one section per provider — GitHub integration and Google integration — each with the same two fields. Once configured, a “Continue with…” button appears on the login page.

GitHub / Google integration

Allows your users to log in with their GitHub or Google credentials. Create an OAuth App with the provider first, then paste its credentials here. Both fields are optional — leaving them empty disables that provider.

FieldTypeDescription
OAuth clientIdText (max 255 chars)The clientId of the OAuth App created with the provider for this integration.
OAuth client SecretSecret (32–512 chars)The secret of the OAuth App created with the provider. Stored securely and never displayed back in the UI.

Audit logs

Review recent authentication events, logins, failures, and administrative changes for the past 7 days (or your configured retention). Which events are recorded — and for how long — is controlled by the Audit log settings section on the Authentication tab.

The log is a read-only table, paged at 15 entries, with a Download CSV action that exports the full log. Each entry carries:

ColumnDescription
TimeWhen the event occurred.
Event typeThe kind of event — e.g. login, logout, password change, or an admin configuration change.
UserThe user the event applies to.
ClientThe OAuth client through which the event happened.
IP addressSource IP address of the request.
DetailsAdditional event-specific detail.

Runtime API

The Runtime API provides programmatic access to manage users, tenants, invites, and authentication settings at runtime. It is a REST API accessible to other services in the same environment and can be consumed from a client in any language. Authentication for the API is based on client credentials. Refer to the Calling the Runtime API section to understand how to make these calls.

Registration invites

The create invite calls (bulk & single) return an inviteId. To build the registration invite link, append the invite ID as a query parameter to the registration page URL of your OIDC application using the parameter registrationInviteId.

https://<your-auth-domain>/realms/app/protocol/openid-connect/registrations?client_id=<client-id>&registrationInviteId=<invite-id>

Tenant invites

If you are inviting users to a tenant using the inviteusertotenant call, the API call also returns an invite ID. To build the registration invite link, append the invite ID as a query parameter to the registration page URL of your OIDC application using the parameter tenantInviteId.

https://<your-auth-domain>/realms/app/protocol/openid-connect/registrations?client_id=<client-id>&tenantInviteId=<invite-id>

Calling the Runtime API

The Runtime API is accessible to any service running in the same environment. All requests must be authenticated using client credentials — obtain an access token from the OIDC token endpoint using your client ID and secret, then pass it as a Bearer token in the Authorization header. The client ID used for Runtime API calls is modstackapp. The credentials for this client ID can be accessed from the Configuration UI.

The Runtime API is only accessible inside the environment by connecting to the modstackauth-authhelper service on port 80. This service is not exposed externally via ingress.

Calling the API is a two-step flow:

  1. Obtain an access token — POST your client credentials (client_id and client_secret) to the token endpoint of the service realm using the OAuth client_credentials grant. The endpoint replies with JSON of the form {"access_token": "eyJh…", "expires_in": 300, "token_type": "Bearer"} — extract the access_token field. Tokens are short-lived: reuse the same token for subsequent calls and request a new one when expires_in (seconds) elapses or a call answers 401.
  2. Call the Runtime API — send the token in the Authorization: Bearer header. The example below calls GET /v1.0/userstore/tenant, which lists all the tenants in the user store — every other Runtime API endpoint is called the same way.
Obtain a token
# POST the client credentials to the token endpoint. The JSON
# response carries the token in access_token — extracted here
# with jq and kept in a shell variable for the next call.
ACCESS_TOKEN=$(curl -s -X POST https://<your-auth-domain>/realms/service/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=modstackapp" \
  -d "client_secret=<client-secret>" | jq -r '.access_token')
Call an endpoint — list all tenants
# Lists all the tenants in the user store.
curl -X GET http://modstackauth-authhelper/v1.0/userstore/tenant \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Obtain a token
// POST the client credentials to the token endpoint.
const tokenRes = await fetch(
  "https://<your-auth-domain>/realms/service/protocol/openid-connect/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: "modstackapp",
      client_secret: "<client-secret>",
    }),
  },
);

// The JSON response carries the token in access_token.
const { access_token } = await tokenRes.json();
Call an endpoint — list all tenants
// Lists all the tenants in the user store.
const res = await fetch(
  "http://modstackauth-authhelper/v1.0/userstore/tenant",
  { headers: { Authorization: `Bearer ${access_token}` } },
);
const tenants = await res.json();
Obtain a token
import requests

# POST the client credentials to the token endpoint.
token_res = requests.post(
    "https://<your-auth-domain>/realms/service/protocol/openid-connect/token",
    data={
        "grant_type": "client_credentials",
        "client_id": "modstackapp",
        "client_secret": "<client-secret>",
    },
)

# The JSON response carries the token in access_token.
access_token = token_res.json()["access_token"]
Call an endpoint — list all tenants
# Lists all the tenants in the user store.
res = requests.get(
    "http://modstackauth-authhelper/v1.0/userstore/tenant",
    headers={"Authorization": f"Bearer {access_token}"},
)
tenants = res.json()
Obtain a token
HttpClient client = HttpClient.newHttpClient();

//POST the client credentials to the token endpoint.
String form = "grant_type=client_credentials"
        + "&client_id=modstackapp"
        + "&client_secret=" + URLEncoder.encode("<client-secret>", StandardCharsets.UTF_8);

HttpRequest tokenRequest = HttpRequest.newBuilder()
        .uri(URI.create("https://<your-auth-domain>/realms/service/protocol/openid-connect/token"))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .POST(HttpRequest.BodyPublishers.ofString(form))
        .build();

String tokenJson = client.send(tokenRequest, HttpResponse.BodyHandlers.ofString()).body();

//The JSON response carries the token in access_token — extract it
//with your JSON library, e.g. Jackson:
String accessToken = new ObjectMapper().readTree(tokenJson).get("access_token").asText();
Call an endpoint — list all tenants
//Lists all the tenants in the user store.
HttpRequest apiRequest = HttpRequest.newBuilder()
        .uri(URI.create("http://modstackauth-authhelper/v1.0/userstore/tenant"))
        .header("Authorization", "Bearer " + accessToken)
        .GET()
        .build();

String tenants = client.send(apiRequest, HttpResponse.BodyHandlers.ofString()).body();