What this is about
Someone without an account cannot sign in. For these people the hub has two public pages that are reachable without a session:
- Register at
/register: the form for self-registration. - Verify at
/registration/verify?token=…: the page the link in the mail points to.
The way there is on the hub’s sign-in page: “No account yet? Register”. Keycloak itself does not offer registration in the shipped configuration, see The login pages.
The flow
sequenceDiagram
participant B as Browser
participant F as Hub BFF
participant C as CIAS
participant M as Email
B->>F: GET /api/hub/registration/form
F->>C: GET /cias/registration/flows/SELF_SERVICE/form
C-->>F: field description
F-->>B: fields + challenge (signed timestamp)
Note over B: person fills in, ticks the checkbox
B->>F: POST /api/hub/registration/self
F->>F: check trap, challenge, email
F->>C: POST /cias/registration/self
C->>M: mail with link
C-->>F: 202 accepted
F-->>B: “Check your inbox”
B->>F: opens /registration/verify?token=…
F->>C: POST /cias/registration/verify
C-->>F: 202 accepted
F-->>B: confirmation text
Both pages talk to CIAS without a token. The BFF uses a separate path for this that may only reach addresses under /cias/registration/. Exactly these addresses are open in CIAS without sign-in.
From field to input
GET /cias/registration/flows/SELF_SERVICE/form returns key, type, required flag, maximum length and allowed values for each field. CIAS deliberately does not return a validation pattern (pattern): it stays a server rule.
GET /cias/registration/flows/SELF_SERVICE/form[
{ "key": "email", "type": "EMAIL", "required": true, "maxLength": 255, "allowedValues": [] },
{ "key": "firstName", "type": "STRING", "required": false, "maxLength": 255, "allowedValues": [] },
{ "key": "lastName", "type": "STRING", "required": false, "maxLength": 255, "allowedValues": [] },
{ "key": "company", "type": "STRING", "required": true, "maxLength": 255, "allowedValues": [] },
{ "key": "application", "type": "ENUM", "required": false, "maxLength": 255, "allowedValues": ["hub"] }
]This is the answer with the configuration from hub-backend. The page turns it into:
| In the description | On the page |
|---|---|
type: EMAIL | email field with browser validation |
type: ENUM | select list with the allowed values |
| any other type | plain text field |
required: true | asterisk, submitting only possible once filled in |
maxLength | maximum length in the input field |
ENUM, not required, exactly one allowed value | no field: the page fills in the one value itself. That is how application: hub is sent without asking anyone |
known key (email, firstName, lastName, company, application) | translated label, for email and company with a hint below |
| unknown key | the key itself as the label. A newly configured field appears right away |
Below the fields there is a checkbox to consent to the processing of the details. Without the checkbox the form cannot be submitted. The page does not send empty optional fields. As the language it sends de.
If the field description cannot be loaded, the page shows an error message with “Try again” instead of an empty form.
Submitting
-
BFFTrapIs the invisible field
websiteempty?↳ no Answeraccepted, without asking CIAS -
BFFChallengeDid this server sign the challenge?↳ no Answer
accepted, without asking CIAS -
BFFWaiting timeIs the form older than 2 seconds and younger than 2 hours?↳ no 429 “too fast” or 400 “open too long”
-
BFFAddressIs an email address given?↳ no 400
-
CIASCIASFields valid, flow switched on, throttle not reached?↳ no 400, 503, 429 or 404
- 202 accepted, the page shows “Check your inbox”
Three terms from the picture:
- The trap (a honeypot) is a field no person sees: outside the visible area, not reachable by tab, hidden from screen readers. A program that fills in all fields fills in this one too. The BFF then answers exactly like a real registration, so the program gets no signal.
- The challenge is a timestamp with a signature that the BFF hands out when the form is loaded. Whoever posts straight at the address without loading the form has no valid one.
- The throttle in CIAS counts attempts per address and per client. The BFF determines the client itself: it sets
X-Forwarded-Forto the entry its own proxy wrote and does not pass on any value from the browser. See Protecting the public endpoints.
The variants
When: Someone opens /register without a session.
The page shows “Loading form …”, fetches field description and challenge and builds the form.
Result: Form with an intro: the address is also the sign-in, the person sets the password later via the link in the mail.
When: CIAS accepts the attempt, whether the address is new, already has an account or joins another tenant.
The page always shows the same: “Check your inbox” with the entered address and a hint about the spam folder. What really happened is only in the mail.
Result: The page does not reveal whether the address already has an account.
When: Less than 2 seconds or more than 2 hours between loading and submitting.
“That was too fast” asks to submit again. For “open too long”, the page reloads the form with a new challenge. The entries stay.
Result: A second click goes through.
When: CIAS refuses.
The text comes from the error key: 400 “The details were refused”, 429 “Too many attempts in a short time”, 503 “Registration is not enabled on this installation” (the flow is not set up). If registration is switched off in CIAS entirely, CIAS answers 404, as if the addresses did not exist.
Result: The entries stay.
When: The invisible field has a value, or the challenge is not from this server.
The BFF answers accepted but does not call CIAS.
Result: The page shows “Check your inbox”, no mail is sent.
When: Someone with a valid session opens /register.
The page forwards to the hub's start page right away.
Result: No form.
The Verify page
The path /registration/verify?token=… is not set by the hub but by CIAS: it builds every link in its mails as {base address}/registration/verify?token=…. The base address is the address a person reaches in the browser, for the hub the address of the hub frontend. That is why invitation links land on this page too. See Verify the email.
When opened, the page redeems the token right away, without a button. CIAS answers every success the same way with 202, even for a link that was already used. So the page cannot read from the answer which sentence to show next. Instead it asks for the installation’s rule: GET /api/hub/registration/policy says whether a self-registration still needs an approval after the verification.
| Redeemed successfully? | Approval required? | What the page shows |
|---|---|---|
| yes | no | “Welcome aboard”: the account is set up, how to continue with the password is in the welcome mail |
| yes | yes | “Address confirmed”: the registration is being reviewed, a message follows |
| yes | unknown | “Address confirmed”: how it continues is in the message |
| no | – | “The link is no longer valid”, plus “To sign-in” and “Register again” |
“Unknown” means: the rule could not be loaded. The page then uses the sentence that is true in both cases. If the token is missing from the address entirely, it shows “The link is incomplete” without asking CIAS. An unknown link and an expired link are the same for the page: CIAS deliberately does not tell them apart.
The value for “Approval required?” comes from the hub frontend’s configuration (CIAS_SELF_SERVICE_APPROVAL, at runtime NUXT_REGISTRATION_APPROVAL_REQUIRED). It is the same switch that hub-backend reads for SELF_SERVICE. Empty means: no approval.
Pitfalls
Next
- Self-registration
- Verify the email
- Approval by an administrator
- The admin interface (queue of registrations)