> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/identities/self-registration.md).

# Configure self-registration

Build a multi-step self-registration workflow with a page template per step, split the shipped template so internal fields are not exposed publicly, and replace the accept-everybody sponsor validator.

## What this is, and when you need it

Self-registration lets an unauthenticated person create their own user record. In current OpenIAM it is not a single form but a **workflow of steps**, and one insight makes the whole area tractable:

> **A self-registration step is a business rule.** Its conditions are business rule conditions and its outcome is a business rule action.

Once you see it that way, "how do I branch contractors to a different form?" answers itself: a condition on a rule.

## At a glance

|                          |                                                                                                                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | A public registration form that collects what you asked for and creates a user, with or without approval                                                                |
| **Where**                | **Administration → Self Registration Configuration**, `/webconsole/sr/`                                                                                                 |
| **You need first**       | A page template of type `USER_TEMPLATE` per step, a content provider to attach the workflow to, and `org.openiam.self.register.enabled` set to `true` (it ships `true`) |
| **Takes**                | An hour for a two-step workflow, most of it in the page templates                                                                                                       |
| **Reversible**           | Partly — configuration reverses, but a field that has already been rendered on the public form has already been exposed                                                 |

## Build the workflow

The worked example: step 1 collects the person's own details, step 2 collects a sponsor's email address, and an approval creates the user.

{% stepper %}
{% step %}

### Split the shipped template before you add a single field

{% hint style="danger" %}
**The shipped New User Template is bound to two URI patterns at once — the authenticated SelfService wildcard `/selfservice/*` and the public, unauthenticated registration form `/selfservice/selfRegistration.html`. Editing it for one changes the other.**

The dangerous direction is adding a field for internal use — a cost centre, an internal ID, a classification, a drop-down whose valid values come from a data model script — and discovering it is now rendered on an **unauthenticated public form**. Anything on the registration pattern is visible and submittable by anyone who can reach the host, and any data model script backing it runs for anonymous callers.

Do the split **before** you add your first field. A field that has already been rendered publicly has already been exposed, and removing it does not un-disclose it. The mechanics are under [Why the shipped template must be split](#why-the-shipped-template-must-be-split).
{% endhint %}

Create `SELF_REG_STEP1_TEMPLATE` and `SELF_REG_STEP2_TEMPLATE`, both of type `USER_TEMPLATE`. Move `SELFREG_PATTERN` off `NEW_USER_TEMPLATE` and onto the step-1 template. Put the sponsor field on the step-2 template only.
{% endstep %}

{% step %}

### Create the workflow

`/webconsole/sr/edit`. Set the workflow active, pick the content provider it applies to, and set the first step's template to `SELF_REG_STEP1_TEMPLATE`. A new workflow is created with an empty first step already in place.
{% endstep %}

{% step %}

### Give step 1 a business rule with a `TRANSITION` action

Set the step's target type to `SELF_REGISTRATION` — that is what decides which actions the **Action** drop-down offers — and give it one action of type `TRANSITION`, labelled **Proceed to Next Step**, pointing at step 2.

To branch, give the step two `TRANSITION` actions with different conditions. Conditions are expressed over the business rule expression attributes; see [Business rule expression attributes](/administration-guide/reference/enums.md#business-rule-expression-attributes).
{% endstep %}

{% step %}

### Give step 2 an action that actually creates the user

Only three of the 27 business rule actions apply to self-registration, and one of the two creating actions has to be on your final step:

| Action                            | Label on screen                | What it does                                                                      |
| --------------------------------- | ------------------------------ | --------------------------------------------------------------------------------- |
| `TRANSITION`                      | **Proceed to Next Step**       | Moves the registrant to another step. This is how you branch                      |
| `TRIGGER_CREATE_WITHOUT_APPROVAL` | **Save User**                  | Ends the workflow and creates the user immediately                                |
| `TRIGGER_CREATE_WITH_APPROVAL`    | **Initiate New Hire Workflow** | Ends the workflow and raises an approval request; the user is created on approval |

For this example use `TRIGGER_CREATE_WITH_APPROVAL`.

{% hint style="warning" %}
The labels read nothing like the constants. **Save User** is `TRIGGER_CREATE_WITHOUT_APPROVAL` and **Initiate New Hire Workflow** is `TRIGGER_CREATE_WITH_APPROVAL`. Translate in that direction when reading a log or an export.

A step with **no** action configured produces an error rather than a silent stop, and a workflow whose last step has only `TRANSITION` actions can never complete. Every step needs at least one action, and every terminal step needs one of the two `TRIGGER_CREATE_*` actions.
{% endhint %}
{% endstep %}

{% step %}

### Replace the sponsor validator

{% hint style="danger" %}
**The shipped sponsor validator accepts everybody.** Both the base implementation and the shipped Groovy override return `true` unconditionally, so any existing user whose email address the registrant can guess is a valid sponsor out of the box — including a terminated user, a service account, or someone with no authority to sponsor anybody.

**A script that fails to instantiate is also treated as a pass.** The failure is logged as "Can't instantiate groovy to validate sponsor. Set valid by default." and the sponsor is accepted. A typo in your script does not fail closed; it fails **open**. So does leaving `org.openiam.selfregistration.sponsor.validator` blank, which skips the check entirely.
{% endhint %}

Store a real validator through the Groovy manager and point `org.openiam.selfregistration.sponsor.validator` at its ID. A working script is under [The sponsor validator](#the-sponsor-validator) below.
{% endstep %}

{% step %}

### Test both directions

Register with a valid sponsor: must complete and reach approval. Register with the email address of a user who is **not** a sponsor: must be refused with "Sponsor is invalid". Register with a malformed address: must be refused with "Sponsor's email address is not valid".

The middle test is the one that proves your script is being called at all. Without it you cannot distinguish a working validator from a validator that failed open.
{% endstep %}
{% endstepper %}

## Verification

| What you did                        | Evidence                                                                                                                           |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Enabled self-registration           | `/selfservice/selfRegistration` renders the form rather than reporting not found                                                   |
| Submitted a step                    | Audit action `SELF_REGISTRATION`, described "Self registration form submitted"                                                     |
| Completed without approval          | Audit `CREATE_USER`; the user exists with the expected user type and status                                                        |
| Completed with approval             | An approval request of type `SELF_REGISTRATION` is pending. See [Approval flows](/administration-guide/requests/approval-flows.md) |
| Split the templates                 | The **URI Patterns** section of both templates. `SELFREG_PATTERN` must appear on exactly **one** of them                           |
| Replaced the sponsor validator      | An unauthorised sponsor is refused with "Sponsor is invalid". If everything is accepted, the script is not being reached           |
| Want to see abandoned registrations | They are indexed separately as `incomplete-self-registration-user`                                                                 |

## When it goes wrong

| Symptom                                                                        | Cause                                                                                                                                                                                                                                                            | Fix                                                                                                           |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `/selfservice/selfRegistration` reports not found                              | `org.openiam.self.register.enabled` is `false`                                                                                                                                                                                                                   | Set it to `true`. The same guard governs submitting the form, so the page and the submit fail together        |
| The public form shows internal fields                                          | `NEW_USER_TEMPLATE` is bound to both the SelfService wildcard and `SELFREG_PATTERN`                                                                                                                                                                              | Split the template. The field has already been exposed — treat that as a disclosure, not a bug to fix quietly |
| The form shows nothing, or the wrong fields                                    | No template is bound to `SELFREG_PATTERN`, or a wildcard template is winning                                                                                                                                                                                     | Open each candidate template and read its **URI Patterns** section to see which one holds the pattern         |
| Every sponsor email is accepted                                                | The shipped validator returns `true` unconditionally                                                                                                                                                                                                             | Replace the script                                                                                            |
| Sponsors are still accepted after you replaced the script                      | Instantiation failed and was swallowed. The log line is "Can't instantiate groovy to validate sponsor. Set valid by default."                                                                                                                                    | Fix the script; the class must extend `DefaultSponsorValidator`                                               |
| A sponsor is rejected as "Sponsor is invalid" although the person exists       | The email search is an exact match, and the address must exist as an email record on that user in OpenIAM                                                                                                                                                        | Correct the address, or add the email to the user                                                             |
| A sponsor is rejected as "Sponsor's email address is not valid"                | The value failed the pattern in `org.openiam.email.validation.regexp`; the underlying error is `INVALID_SPONSOR_EMAIL_ADDRESS`                                                                                                                                   | Correct the address, or the pattern                                                                           |
| Registration errors out at a step with no explanation                          | That step has no action configured                                                                                                                                                                                                                               | Give every step at least one action, and every terminal step a `TRIGGER_CREATE_*` action                      |
| Registration restarts from step 1                                              | Step state is keyed on the HTTP session ID and the session expired                                                                                                                                                                                               | Shorten the workflow, or raise the session timeout                                                            |
| Phone or email OTP verification blocks the submit                              | A phone or email field is on the step and `org.openiam.ui.otp.sms.phone.require.on.create` or `org.openiam.ui.otp.email.require.on.create` is on with a provider that supports it — the submit returns `CODE_NOT_EQUAL` until the code is verified               | Supply the verification code, or remove the field from that step                                              |
| The sponsor validator property has no label on the System Configuration screen | The shipped label keys are `org.openiam.self.registration.sponsor.validator.*` — with a dot between "self" and "registration" — while the property OpenIAM reads is `org.openiam.selfregistration.sponsor.validator`. The two do not match, so no label resolves | Search for the property ID, not the label                                                                     |

## Roll it back

**Workflow configuration** — reversible. Set the workflow inactive to fall back to the non-workflow path; the objects survive.

**Template bindings** — reversible, and the fastest way back: rebind `SELFREG_PATTERN` to the previous template.

**Sponsor validator** — reversible; repoint the property at the previous script ID.

**An exposed field** — **not** reversible as an exposure. Removing the field stops future submissions but does not un-disclose what a public form already rendered. Split the template before you add fields.

## Why the shipped template must be split

A page template applies to a URI pattern through a cross-reference, and the single shipped template `NEW_USER_TEMPLATE` is bound to **two** patterns from a fresh 4.0.0.0 install onwards:

| Pattern                   | Matches                                                 | Authentication                                                                                                   |
| ------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `DEF_CP_SELSERVICE_SLASH` | `/selfservice/*` — every authenticated SelfService page | Required                                                                                                         |
| `SELFREG_PATTERN`         | `/selfservice/selfRegistration.html`                    | **None.** Its authentication level grouping is `NONE`, which is what makes the form reachable without signing in |

They are one template on two patterns, so a field added for either surface appears on both.

The fix is to stop sharing the template:

1. Create a second `USER_TEMPLATE` page template for self-registration only.
2. Remove `SELFREG_PATTERN` from `NEW_USER_TEMPLATE`'s **URI Patterns** and add it to the new one.
3. Keep `DEF_CP_SELSERVICE_SLASH` on `NEW_USER_TEMPLATE`.
4. Verify both surfaces: load `/selfservice/selfRegistration` unauthenticated and confirm the field sets differ.

One second-order effect. Because `DEF_CP_SELSERVICE_SLASH` is a wildcard over the whole SelfService context, a **narrower** pattern is what distinguishes one SelfService page's template from another's. Bind a new template to the most specific pattern that matches, then reopen the wildcard template's **URI Patterns** section to confirm you have not left both bindings in place.

## The sponsor validator

Self-registration can require a sponsor: the registrant types a sponsor's **email address** into the supervisor field and OpenIAM resolves it to an existing user. Three checks run in order, all conditional on the template carrying the sponsor field.

| Order | Check                                                   | Error on failure                  | English message                        |
| ----- | ------------------------------------------------------- | --------------------------------- | -------------------------------------- |
| 1     | The field is required and non-empty                     | `USER_SELECT_SUPERVISOR_REQUIRED` | —                                      |
| 2     | The value matches `org.openiam.email.validation.regexp` | `INVALID_SPONSOR_EMAIL_ADDRESS`   | "Sponsor's email address is not valid" |
| 3     | An exact email search returns a user                    | `INVALID_SPONSOR`                 | "Sponsor is invalid"                   |

Only if a user was found is your Groovy validator called. When check 3 fails, the log records "Can't find such sponsor".

| Property                                         | Shipped default                                        | Category |
| ------------------------------------------------ | ------------------------------------------------------ | -------- |
| `org.openiam.selfregistration.sponsor.validator` | `selfservice/selfregistration/SponsorValidator.groovy` | `UI`     |

The shipped `selfservice/selfregistration/SponsorValidator_1.0.groovy` has its real validation commented out and an unconditional `return true`. Your replacement extends `DefaultSponsorValidator` and overrides `isValidSponsor` — the base returns `true` for everybody, so extending it without overriding that method accepts every sponsor.

### A validator that actually validates

This one refuses a non-active sponsor and requires an `is_sponsor` attribute on the sponsoring user. The class declaration, the method signature, the `context.getBean(...)` idiom and the attribute access are all taken from the shipped file's own commented example.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
import org.openiam.common.beans.mq.UserRabbitMQService
import org.openiam.dto.User
import org.openiam.idm.searchbeans.UserSearchBean
import org.openiam.idm.srvc.user.dto.UserCollection
import org.openiam.idm.srvc.user.dto.UserStatusEnum
import org.openiam.ui.groovy.selfservice.DefaultSponsorValidator
import org.springframework.context.ApplicationContext

class SponsorValidator extends DefaultSponsorValidator {
    protected ApplicationContext context;
    private UserRabbitMQService userDataWebService

    @Override
    boolean isValidSponsor(User sponsor, String sponsorEmailFromForm) {
        if (sponsor == null) {
            return false
        }
        // Only an ACTIVE user may sponsor.
        if (UserStatusEnum.ACTIVE != sponsor.getStatus()) {
            return false
        }
        // And only one carrying the is_sponsor attribute.
        userDataWebService = context.getBean(UserRabbitMQService.class)
        UserSearchBean userSearchBean = new UserSearchBean()
        userSearchBean.addKey(sponsor.getId())
        def beans = userDataWebService.findBeans(userSearchBean,
                [UserCollection.ATTRIBUTES] as UserCollection[], 0, 1)
        if (beans == null || beans.getList() == null || beans.getList().isEmpty()) {
            return false
        }
        def value = beans.getList().get(0).getAttribute("is_sponsor")?.getFirstValue()
        return "true".equalsIgnoreCase(value as String)
    }
}
```

{% endcode %}

Store it through the Groovy manager and point the property at its ID. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md).

### Where the accepted sponsor ends up

When a sponsor is accepted, OpenIAM adds them to the registrant as a supervisor of the metadata type named by `org.openiam.sponsor.supervisor.type.id`, which falls back to `SPONSOR_SUP_TYPE`. That type ships from 4.2.1.2 named **Sponsor** in the `SUPERVISOR_TYPE` grouping; confirm it under **Administration → Metadata Types**.

**That default cannot be read from the Webconsole.** The key has no row on the System Configuration screen, because the fallback lives in the service's own configuration rather than in the property table. Setting the key creates the row and makes it visible; until then, ask the deployment owner to confirm the effective value — or read the consequence instead: register with a sponsor and look at the new user's **Supervisors**, where the entry's type is the one in force.

## The screens, and the audit action each one leaves

| Purpose                           | Route                              | Audit action on completion                                                  |
| --------------------------------- | ---------------------------------- | --------------------------------------------------------------------------- |
| The public self-registration form | `/selfservice/selfRegistration`    | `SELF_REGISTRATION`, described "Self registration form submitted", per step |
| New hire, no approval             | `/selfservice/newUser`             | `CREATE_USER`, described "Create new user, new hire no approval"            |
| New hire, with approval           | `/selfservice/newUserWithApprover` | `CREATE_USER`, described "Create new user, new hire with approval"          |
| List the registration workflows   | `/webconsole/sr/`                  | —                                                                           |
| List the steps                    | `/webconsole/sr/steps`             | —                                                                           |
| Create a workflow                 | `/webconsole/sr/edit`              | —                                                                           |
| Edit a workflow                   | `/webconsole/sr/edit?id=<ID>`      | —                                                                           |

Those three public forms are the three flows, and the audit action is how you tell which one a given registration came through — the two new-hire flows both record `CREATE_USER` and differ only in the description.

State across steps lives in the HTTP session, keyed by session ID, so progress is lost on session expiry. Partially completed registrations are indexed separately as `incomplete-self-registration-user`.

### What a workflow and a step are made of

A **workflow** carries whether it is active, the content provider it applies to, and its first step. A **step** carries a description, the page template rendered for it, the business rule evaluated after it is submitted, and the target holding the actions taken when the rule matches.

The target's own type is what gates which actions are offered, and it has only two possible values: `PROVISIONING` and `SELF_REGISTRATION`. A self-registration step's target must be `SELF_REGISTRATION`.

The step editor is handed four vocabularies, which is exactly the set it needs: business rule expression attributes, expression operations, condition types and user statuses. See [Enumerated values](/administration-guide/reference/enums.md) and [Business rule reference](/administration-guide/access/business-rules/business-rule-reference.md).

### The four outcomes of submitting a step

Submitting a step resolves to exactly one of four outcomes: **complete**, **complete with approval**, **transition** to the step whose ID came back, or **error** — the last being what a step with no action produces, and what you get when nothing came back at all.

## Related

* [Build a page template](/administration-guide/identities/page-templates-and-custom-fields.md) — the templates each step renders
* [Business rule reference](/administration-guide/access/business-rules/business-rule-reference.md) — all 27 action types, and which are offered where
* [Approval flows](/administration-guide/requests/approval-flows.md) — what **Initiate New Hire Workflow** hands off to

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/identities/self-registration.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
