What this is about
Every project has its own rules for registration: allow only addresses of certain companies, choose the tenant by domain, give a person extra roles. So that nobody has to change CIAS’s code for this, there are two kinds of extension points:
- A hook runs inside the registration. It may change values or reject the registration.
- An event is sent afterwards. Whoever receives it learns what happened, but can no longer change it.
- Bean that implements
RegistrationHook - may reject (
RegistrationRejectedException→ 422) - may change tenant, initial roles, email
- order via
getOrder(), lower first
- Spring
@EventListeneronRegistrationEvent - can no longer prevent anything
- never contains a token
- sent right after saving
The hook points on the timeline
-
1Hook
onResolveTenant(context, decision)– change the tenant, not withFROM_CALLER -
2Hook
beforeValidation(context)– before the fields are checked -
3CIASchecks the fields
-
4Hook
afterValidation(context)– the usual place for a veto -
5Hook
beforeIdentityProvisioning/afterIdentityProvisioning– only for a new account, around the creation in Keycloak -
6Hook
beforeNotification(mail, context)– before every email, may change the email -
7User→CIASclicks the link
-
8Hook
onVerified(context)– after verification -
9Hook
onAssignInitialRoles(context, roles)– change the initial roles -
10Hook
onCompleted(context)– at the end of provisioning -
11Hook
onFailed(context, error)– when provisioning fails -
12Hook
onCompensate(context, accountRemoved)– when an administrator discards the failed registration; in reverse order, before CIAS takes back tenant, organization and account
All methods have an empty default implementation. A hook implements only the ones it needs.
The variants
When: Only addresses of partner companies should be allowed to register.
The hook throws a RegistrationRejectedException with its own key in afterValidation. CIAS then creates no account and stores nothing.
Result: 422 { "error": "<own key>", "message": "the registration was refused" }
When: Addresses with @nordbau.example should always end up in nordbau.
onResolveTenant returns a new TenantDecision, for example JOIN_EXISTING with key nordbau, and a reason. null means: unchanged. Several hooks run one after another, and each sees the result of the previous one.
Result: See Where the tenant comes from.
When: People from a certain company should also get report-read.
onAssignInitialRoles gets the roles from the rule set and returns a new set. CIAS then checks that each role exists.
Result: See Initial roles as a rule set.
When: Invitation by a tenant administrator.
onResolveTenant is not called at all. The tenant from the inviting person's token cannot be redirected.
When: A record should be created in another system.
onCompleted still runs inside provisioning. If it throws an exception, the registration ends in FAILED, and retry runs it again. If an error there should not hold up the registration, an event is the better place.
The events
| Event | is sent when |
|---|---|
Initiated(id, flow) | a registration was accepted and the email was sent |
Verified(id) | the address is verified, by click or by hand |
Completed(id, externalUserId, tenantKey) | provisioning is done |
Rejected(id, reason) | an administrator rejected it |
Failed(id, reason) | provisioning failed |
Expired(id, state, identityRemoved) | a registration expired, was replaced, or was discarded |
Events are sent in the same thread, right after CIAS has saved the new state. A listener that takes long delays the response. CIAS’s audit listens to the same events, see One trail for everything.