enhancement
major
minor
defect
critical
major
minor
major
#30041
Self-service accounts: invitations expire and record their acceptance, invitations can be accepted and managed in the React UI, localized mail templates
Related: #29092 (multi-factor authentication and self-service), #30039 (login and password reset defects), #29961 (split of tl-security-selfservice-core, merged before this ticket).
Context
The module tl-security-selfservice lets an administrator invite a person by mail (tl.security.selfservice:Invitation, TL-Script sendInvitation); the invited person opens the link, confirms a code sent by mail and creates the account. A user can also reset the password or the second factor with a code sent by mail. The React view layer (com.top_logic.layout.view.selfservice) offered only the password reset.
An invitation never expired, recorded neither who sent it nor when, and was deleted when it was accepted, so the administration could not tell open, expired and used invitations apart. Its link token had 128 bit. An application with the React UI could neither accept nor manage invitations.
Behavior
Invitation lifecycle
- An invitation has a state - open, expired or accepted - and shows when and by whom it was created (taken from the creating commit, so also for existing invitations), until when it is valid, and - once accepted - when it was accepted and which account was created from it. Accepted invitations are kept as a record instead of being deleted.
- The validity is configured in InvitationModule (option invitation-validity, default 7 days) and counts from the moment the invitation is stored. An expired invitation can no longer be accepted; sending it again gives it a new link and a new validity. An accepted invitation can neither be sent again nor revoked.
- Only open invitations count for the check that an address is invited only once.
- The link token has 32 random bytes (256 bit).
- The authentication device on which an accepted invitation creates the account is configured in InvitationModule (option authentication-device, default dbSecurity).
- The link leads to the page configured in InvitationModule (option invitation-link); its host is the configured public address of the application (tl_host) or, without one, the host of the current request.
- A verification code becomes invalid after three wrong entries; repeated code requests are throttled. Without configured mail, no code is sent (and none is written to the log); the request is refused with a message.
React UI
- The invitation link of an application with tl-layout-view-selfservice leads to the React page accept-invitation.view.xml (an anonymous entry point): it names the reason for an unknown, expired or accepted invitation; for an open one the invited person requests and enters the mail code, then enters name and password. The new account runs through the login steps of the React login (e.g. setting up a required second factor) and is logged in.
- The React administration has a tab "Invitations" (Administration -> Access control): all invitations with state, validity, creation, acceptance and account; invite (creates and sends), send again, revoke.
- tl-demo-react contains the React self-service.
Classic UI
- The classic dialogs accept invitations through the same operations; unknown, expired and accepted links show their reason. The dialog "Invitations" (formerly "Open invitations") lists all invitations with their state and sends them again or revokes them.
Mail templates
- The mail templates (invitation-mail, verification-mail, reset-password-mail, reset-mfa-mail) are localized (subject and text in German and English, in the language of the current user or the default language of the application) and sent as HTML; the typo "Verficationcode" is fixed.
- The verification mail of an invitation reaches the invited address: its script runs without access checks, since nobody is logged in when it is sent (before, it was sent to an empty address).
Small fixes
- CreateLoginHoook is renamed to CreateLoginHook.
- The JavaDoc of MfaRequirement.DISABLED says that the account must not use a second factor and cannot enable one.
- Migration scripts: a module listed in <dropped-dependencies> (#30065) is no longer re-added as a dependency through the module's earlier migrations - neither as a transitive dependency nor as an initial one when the module is present in the application. Before, <dropped-dependencies> had no effect as soon as an earlier migration of the module depended on the dropped module.
Technical notes
- InvitationModule (in tl-security-selfservice-core) offers the UI-independent operations createInvitation, findInvitation / findOpenInvitation / refusal, acceptInvitation (validity check, account on the configured device with its password policy, createCallback, marking accepted - in one transaction), renewInvitation, resendInvitation, invitationLink, sendInvitation, sendVerificationMail. TL-Script: sendInvitation (moved to the core module), invitationRenew, invitationResend, invitationRefusal.
- VerificationCode (core) generates and checks the codes of the invitation and of the classic and React password reset.
- The default of expires is computed in the creating transaction (isComputedInTransaction), so a draft created by a dialog gets its expiry when it is stored.
- LoginFlow.LANDING_PAGE (tl-layout-view): a login entry names the page the window loads after the login; the invitation page uses it, so the new user is not sent back to the invitation link.
- Data migration Ticket_30041_invitation_lifecycle: widens the column of the token and gives existing invitations an expiry date (time of the update plus invitation-validity).
- The developer article authentication/self-service describes the lifecycle, the options and both UIs.
Out of scope
- Resetting the second factor by self-service in the React UI depends on #30039 item 2 (a password reset must not bypass the second factor) and is part of #30039 (item 8).
- Features planned for the self-service and decided on: e-mail code as login factor (#30182) and open self-registration (#30183) are wanted and have tickets of their own. Not wanted: a password reset looking up the account by its mail address (the reset identifies the account by user name), and invitations adding to an existing account (an address that has an account cannot be invited).
- An account with an optional second factor is not offered to set one up after accepting an invitation in the React UI, like after every React password login.
- The React enrolment view mfa-enroll.view.xml already shows the secret for manual entry.
Test
- TestInvitationLifecycle (tl-security-selfservice-core): accepting an expired or accepted invitation is refused and leaves no account; accepting a valid one creates the account on the configured device, marks the invitation accepted and records the account; renew and resend; the token encodes 32 bytes; uniqueness ignores expired and accepted invitations; the link with and without public address; the expiry counts from storing a draft. TestMigrationUtil (com.top_logic): dropped dependencies, also when the module is present.
- tl-demo-react (mail to a local SMTP sink): Administration -> Access control -> Invitations: invite, mail with the link to the React page, accept logged out (code, account, login; with a required second factor its setup), the invitation is then shown as accepted; refused second invitation and invitation of an existing account; send again an expired invitation; revoke. tl-demo: the same in the classic UI (Administration -> Berechtigungen -> Benutzerverwaltung, "Nutzer einladen", "Einladungen").
Migration
- The login hook class com.top_logic.security.selfservice.invitation.CreateLoginHoook is renamed to CreateLoginHook. The module configures it itself; an application that references the class in its own configuration (e.g. in <login-hooks> of MainLayout$GlobalConfig) or in code must use the new name.
- Code that constructs CreateAccountDialog passes no continuation command any more.
- The resource keys ERROR_NO_INVITATION__ID, UPDATED_INVITATION_CODE and DELETED_INVITATION of com.top_logic.security.selfservice.invitation.I18NConstants are removed; an application overriding them drops them.
- An application that changed the classic layouts admin/security/accounts/existingInvitations/* or admin/security/accounts/inviteUsers/dialog.layout.xml in the layout editor keeps its stored copy and re-applies the new columns and commands to it.
- Code that calls sendInvitation for an invitation that is not open gets an error; an expired invitation is sent again with invitationResend.
- Existing invitations are given an expiry date (time of the update plus invitation-validity) automatically.