makallio85 / rocksoftware-cakephp-users
CakePHP 5 plugin for everything about signing in to a Rock Software application: Rock Software ID (OpenID Connect), password sign-in, sessions, sign-in history, the users page and the user menu.
Package info
github.com/makallio85/rocksoftware-cakephp-users
Type:cakephp-plugin
pkg:composer/makallio85/rocksoftware-cakephp-users
Requires
- php: >=8.3
- cakephp/authentication: ^3.2
- cakephp/cakephp: ^5.1
- cakephp/migrations: ^4.0 || ^5.0
- firebase/php-jwt: ^6.10 || ^7.0
Requires (Dev)
- cakephp/cakephp-codesniffer: ^5.0
- phpunit/phpunit: ^11.5 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-dev
- v3.1.1
- v3.1.0
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.0.1
- v2.0.0
- v1.6.1
- v1.6.0
- v1.5.0
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.1
- v1.3.0
- v1.2.0
- v1.1.0
- v1.0.1
- v1.0.0
- dev-master
- dev-real-client-ip-and-browser
- dev-plugin-3.1-followups
- dev-stale-email-conflict
- dev-echo-pushed-active
- dev-app-events-before-early-endpoints
- dev-fit-name-to-column
- dev-issue-33-scim-lifecycle
- dev-clear-schema-cache-after-migrate
- dev-rename-rocksoftware-cakephp-users
- dev-password-form-application
- dev-issue-24-sign-in-users-menu
- dev-issue-21-reconcile-support
- dev-issue-18-password-sign-in-not-available
- dev-issue-15-end-password-sessions
- dev-issue-12-password-sign-in-switch
- dev-issue-9-session-check-own-sessions
- dev-issue-6-provision
- dev-issue-4-plug-and-play
- dev-issue-1-app-pieces
This package is auto-updated.
Last update: 2026-10-18 11:47:35 UTC
README
CakePHP 5 plugin for everything about signing in to a Rock Software application. It
signs users in through the central Rock Software identity provider
(id.rocksoftware.fi, Authentik) with OpenID Connect, and provides password sign-in,
session limits, the sign-in history, the users page and the user menu. No application
handles sign-in itself.
Formerly makallio85/cakephp-sso (plugin Sso, routes /sso/*). 2.0 renamed
everything; an application moving over reconnects to Rock Software ID (below).
| Name | |
|---|---|
| Composer package | makallio85/rocksoftware-cakephp-users |
| CakePHP plugin, namespace | RockSoftware/Users, RockSoftware\Users\ |
| Routes | /auth/* |
| Settings, events, session keys | RockSoftwareUsers.* |
| Tables | rock_software_users_* |
| Translation domain | rock_software_users |
| Commands | bin/cake rock_software_users … |
Rock Software ID owns a person's whole lifecycle: who they are, when their access
starts and ends, and which applications they may use. It pushes people to the
application through SCIM before their first day; the plugin keeps them in the
application's users table, whose people columns it owns, and refuses every way in
outside their access. An application manages only permissions. No user is ever
deleted: access is revoked, and the row, its history and its permissions stay.
Installing it is three steps and no code.
Design: makallio85/rocksoftware-identity#5; the lifecycle: #33.
What it does
| Route | Purpose |
|---|---|
GET /auth?redirect=/path |
The sign-in page: "Sign in with Rock Software ID", the email and password form while password sign-in is on, and the reason for a failed sign-in |
GET /auth/start?redirect=/path |
Starts the authorization code flow with PKCE |
POST /auth/password |
The sign-in page's email and password form |
GET /auth/callback |
Redirect URI registered at the provider |
GET|POST /auth/sign-out |
Signs out here and at the provider |
POST /auth/backchannel-logout |
Back-channel logout URI registered at the provider |
GET|POST /auth/emergency/{token} |
Single-use sign-in from bin/cake rock_software_users emergency_sign_in |
GET /auth/settings, POST /auth/settings/update, POST /auth/settings/test |
Settings › Identity provider: issuer, client ID, write-only secret, required group. Save checks the server first and stores nothing if it does not answer |
POST /auth/settings/password-sign-in, POST /auth/settings/sessions |
The password sign-in switch; the session limits and the sign-in history's retention |
GET /auth/users, GET /auth/users/{id}, POST …/end-sessions, GET|POST …/emergency-link |
The users page, read-only: people, their access, access and sign-in history |
GET /auth/users/held-removals, POST …/approve, POST …/reject |
Removals the mass-removal stop held |
/auth/scim/v2/* |
SCIM 2.0: Rock Software ID pushes people and groups (bearer token) |
Loading the plugin wires it into the application: its middleware hook places the
back-channel logout before CSRF, provides session authentication when the application
has none, adds SessionCheckMiddleware (signs out a session whose user is gone or
without access now, or which started before a back-channel logout) and requires sign-in for
every route except the sign-in routes and RockSoftwareUsers.publicPaths. Every text has a Finnish
translation (resources/locales/fi/rock_software_users.po, domain rock_software_users).
- ID and logout tokens are verified against the provider's JWKS with
firebase/php-jwt; only asymmetric algorithms are accepted. Issuer, audience, nonce, state and PKCE are checked by the plugin. - The link between a user and the provider lives in the plugin's
rock_software_users_identitiestable (user_id,external_id,synced_at,last_login_at,sessions_valid_from), so the application'suserstable needs no columns for it. - People arrive through SCIM (below). A sign-in still finds a user through their
identity, else links one on a verified email address, else creates one, for an
application SCIM does not reach yet. Email and name are rewritten on every sign-in.
A new row's NOT NULL
passwordgets an unusable value. - Access (
UserAccess):is_activeoff,access_fromin the future oraccess_untilpassed refuses Rock Software ID, password and emergency sign-in, and ends a running session on its next request. - A required provider group (
rock_software_users_settings.required_group) gates access. - The connection lives in
rock_software_users_settings; the client secret is encrypted with a key derived fromSecurity.salt. - After every
bin/cake migrations migrateorrollback, the plugin empties the schema cache (cacheMetadata) of the application's connections, so a migration that changes a table (moving over dropsusers.password, for one) never leaves queries reading the old layout.
Authorization stays in the application. The plugin only establishes who the user is.
Installing in an application
The application creates its users table (id, email) in its own migrations. The
plugin adopts it: its migration adds the columns it owns when they are missing —
name, phone, is_active, access_from, access_until — and a unique index on
email. It never drops or changes an application's column, ids stay, and it stops
before changing anything when two users share an address. The plugin ships
RockSoftware\Users\Model\Table\UsersTable; an application's own UsersTable may
extend it, and one without its own class gets it.
- Composer.
composer require makallio85/rocksoftware-cakephp-users:^3.0(Packagist; releases are tags onmaster). - Plugin.
$this->addPlugin(\RockSoftware\Users\UsersPlugin::class);inApplication::bootstrap(). - Migrations. The application's first, then the plugin's:
bin/cake migrations migrate && bin/cake migrations migrate -p RockSoftware/Users(in the deploy entrypoint and intests/bootstrap.php).
That is all. Then connect it to the provider (below) and sign in at /auth.
Settings
Every setting has a default; override them under RockSoftwareUsers in config/app.php. None is a
secret.
| Setting | Default | |
|---|---|---|
appName |
null |
Shown on the plugin's sign-in page |
signInRedirect |
'/' |
Where a person lands after signing in |
publicPaths |
[] |
fnmatch patterns reachable without signing in, e.g. '/health', '/public-api/*' |
settingsAccess |
null |
Who may open /auth/settings: fn($identity, $request): bool. Null means anyone signed in, who is already in the required group |
usersAccess |
null |
Who may open /auth/users, the same kind of callable. Null means whoever may open the settings page |
layout |
'RockSoftware/Users.sign_in' |
Layout of the sign-in and emergency pages; name the application's own to restyle them |
systemUsers |
[] |
Ids of users rows the application runs jobs as (konekallio-erp's R2D2): SCIM never touches them |
alertWebhookUrl |
null |
An https address that also receives every alert as JSON (an IT monitoring application) |
timezone |
'Europe/Helsinki' |
The zone access dates and link expiry are shown in |
createUsers |
true |
Create a user on first sign-in when none matches |
authentication |
'auto' |
Provide authentication when the application has no AuthenticationMiddleware; true/false force it |
requireSignIn |
'auto' |
Require sign-in everywhere when the plugin provides authentication; true/false force it |
signInUrl |
'/auth' |
The sign-in page |
cacheConfig |
'default' |
Cache for the provider's discovery document and keys |
passwordSignIn |
false |
true when people may also sign in with a password from the users table: the sign-in page shows the form and /auth/settings the switch (see below) |
provisioning |
see below | POST /auth/provision; false switches it off |
An application with its own AuthenticationMiddleware keeps it, and keeps deciding which
of its pages are public; point its unauthenticatedRedirect at /auth.
Optional
-
Menu. Link to
/auth/settings(['plugin' => 'RockSoftware/Users', 'controller' => 'Settings', 'action' => 'index']) and/auth/users('controller' => 'Users'). Their controllers extend the application's ownApp\Controller\AppController, so they render in the application's layout. Put the user menu in the layout (below). -
Look. Override
templates/plugin/RockSoftware/Users/SignIn/index.php(sign-in page),templates/plugin/RockSoftware/Users/Settings/index.phportemplates/plugin/RockSoftware/Users/Users/*.php(Bootstrap 5 markup by default). -
Events.
Event Data Typical use RockSoftwareUsers.beforeUserSaveuser,claims,isNewFill columns the plugin does not know RockSoftwareUsers.afterSignInuser,claims,methodAudit log RockSoftwareUsers.beforeSignOutuserAudit log RockSoftwareUsers.backchannelLogoutuserAudit log RockSoftwareUsers.provisionedissuer,clientId,requiredGroupAudit log; tell the administrators RockSoftwareUsers.passwordSignInChangeduser,enabledAudit log RockSoftwareUsers.sessionLimitsChangeduser,limitsAudit log RockSoftwareUsers.sessionsEndeduser,actorAudit log RockSoftwareUsers.accessGranteduser,actorUserIdA person got access (again) RockSoftwareUsers.accessRevokeduser,revocation,actorUserIdA person lost access, e.g. list their open work RockSoftwareUsers.alertRaisedalert,message,user,countSend it on with the application's mailer RockSoftwareUsers.userListColumnscolumns(ArrayObject),usersAdd columns to the users page: ['label' => …, 'value' => fn($user): string]RockSoftwareUsers.userActionsactions(ArrayObject)A button per user on the list and the user's page: ['label' => …, 'url' => fn($user): string|array|null], e.g. Manage permissionsRockSoftwareUsers.userViewSectionssections(ArrayObject),userAdd a section to a user's page: ['title' => …, 'element' => …, 'data' => [...]], rendered with the application's element and$user -
Own second factor. Skip it when
\RockSoftware\Users\Session\SignInSession::isProviderSession($session)is true: the provider already required a second factor. A plugin password session is not one; an application with its own second factor keeps its own password form.
Moving to 3.0
composer require makallio85/rocksoftware-cakephp-users:^3.0.- Run the application's migrations before the plugin's (entrypoint and
tests/bootstrap.php). - A column with another name than the plugin's (
full_name, say) is the application's to rename in its own migration;userModelandfieldsare gone. - Listeners on
userDeactivated/userActivatedgo: access changes in Rock Software ID. Listen toaccessGranted/accessRevokedinstead. - Profile pages show name, email and phone read-only: Rock Software ID owns them.
- Emergency links are written to a file, never printed (below).
- Deploy, press Allow Rock Software ID to connect again, then
new-id-app.sh --update --rotate-secretto deliver the SCIM token.
Moving from cakephp-sso 1.x
composer remove makallio85/cakephp-sso && composer require makallio85/rocksoftware-cakephp-users:^2.0.addPlugin('RockSoftware/Users'); migrations-p RockSoftware/Users; settings move fromSsotoRockSoftwareUsers(loginUrl→signInUrl,loginRedirect→signInRedirect); events and links per the table above.- Drop the old
sso_*tables andsso_phinxlogwhen convenient; nothing reads them. - Deploy, then reconnect:
new-id-app.sh --update --rotate-secret(it reads/auth/client-metadataand pushes to/auth/provision). Each person's account is linked again on their next sign-in by verified email address.
Connecting to the provider
new-id-app.sh in infra-docs registers the application in Rock Software ID and pushes
the connection to POST /auth/provision, so nothing is typed into the application. The
design is in rocksoftware-identity#5,
Connecting applications automatically.
The request carries the issuer, client ID, client secret and required group, and a bearer token the provider issued through token exchange for that client. The plugin accepts it only when:
- the application has no connection yet, or an administrator pressed Allow Rock
Software ID to connect again on
/auth/settingswithin the last hour; every provisioning closes it again; - the issuer is directly under
provisioning.issuerPrefix(https://id.rocksoftware.fi/application/o/), checked before anything is fetched, and the provider's keys are on the same host; - the token is signed with that issuer's key, its
issandaudequal the issuer and client ID it delivers, it was issued toprovisioning.subject(onboarding) at mostprovisioning.maxAge(300) seconds ago, and itsjtiwas never used before.
Any refusal is a bare 401 (400 for a malformed request, 429 over
provisioning.rateLimit per provisioning.rateWindow), with the reason in the log and
never the request body. Each provisioning is kept in rock_software_users_provisionings.
By hand, register the application in Authentik's admin UI (the steps are in
rocksoftware-identity#5),
then enter the issuer, client ID, secret and required group on /auth/settings, or in
the application container:
bin/cake rock_software_users configure \
--issuer https://id.rocksoftware.fi/application/o/<slug>/ \
--client-id <client id> \
--required-group <app>-staff \
--enable
# the client secret is asked for interactively
Password sign-in
With passwordSignIn set to true, the sign-in page shows an email and password form
under the Rock Software ID button, checked against the users table's password column
(fields.password) with CakePHP's default hasher, so existing passwords keep working;
an outdated hash is replaced on the next sign-in. Refused attempts are recorded in the
sign-in history and throttled from it: 5 per address or 20 per client address within
15 minutes. There is no self-service reset; an administrator sets a password with
bin/cake rock_software_users set_password person@example.com # reads the password from standard input
or the person signs in through Rock Software ID.
Settings › Identity provider shows Turn password sign-in off / on. The switch is stored
in rock_software_users_settings.password_sign_in_enabled, with who changed it and when. Off hides the
form and ends open password sessions on their next request: the plugin's own, and any
session an application's own password form established. Provider sessions, emergency
links and token requests are untouched.
It is on until someone turns it off, and always on before the first connection. Turning it off needs a session signed in through the provider (not a password session, not an emergency link) and a connection that answers, so nobody locks themselves out; turning it on works from any session allowed on the page.
An application that still has a password form of its own (while it moves over; applications do not handle sign-in themselves) enforces the switch there:
if (!$this->fetchTable('RockSoftware/Users.Settings')->passwordSignInAllowed()) { // hide the form, and refuse a password POST like a wrong password }
People from Rock Software ID (SCIM)
Rock Software ID's SCIM provider pushes the application's people and groups to
/auth/scim/v2 with a bearer token it delivered through /auth/provision (only its
SHA-256 is kept; no token means every SCIM request is refused).
- Created in advance. A person is pushed when they are added in Rock Software ID,
with
access_from(their first day), so permissions can be set before they start. Matched byexternalId(= the OIDCsub), else by email to an unlinked row; the SCIMidisusers.id. - All addresses. The address for this application's company is
users.email; the person's other addresses (another company's, the account address) come along and are kept inrock_software_users_user_emails, shown on the users page and searched, so one person with two addresses is recognised as one. - Kept current. Name, company email, phone (E.164) and the access dates
(
urn:rocksoftware:params:scim:schemas:extension:access:2.0:User) come with every push. An address another user already has is refused (409) and shown as Email conflict. - Revoked, never deleted.
active: false,DELETE, and leaving the last of the application's groups (sent at once as a membership change) turnis_activeoff. A lateractive: true, or coming back into a group after leaving, gives the same row access again.RockSoftwareUsers.systemUsersare never touched. - Mass-removal stop. When the removals within 15 minutes reach half of the
application's users, they are held and an alert raised (
alertRaised, andalertWebhookUrl) until a person approves or rejects them on the users page or withbin/cake rock_software_users revocations [list|approve|reject]. With one user, that user's own removal always waits. - The way back in.
bin/cake rock_software_users restore_access <email>gives one person access back from the server when a mistake took it. The next push overrules it.
Alerts by email. GET /auth/scim/v2/RockSoftwareAlerts (the same token) lists
what needs a person here: removals on hold and current email conflicts, each with an
id that stays while it lasts. Rock Software ID's hourly job reads it from every
application and emails each new alert once, so no application needs its own mail
set-up for them.
Before switching SCIM on for an application that already has users,
bin/cake rock_software_users scim_precheck (the people JSON from Rock Software ID on
stdin or --file; id-scim-precheck.sh in infra-docs does both ends) shows what SCIM
would do with each user, with the same matching rules, and what to fix first: rows
that would be duplicated, addresses taken by another linked user, linked users who
would lose access. It changes nothing.
Every change is in the access history (rock_software_users_access_changes, typed by
rock_software_users_access_change_types and rock_software_users_revocation_types).
Users page and sign-in history
/auth/users is read-only: people come from Rock Software ID and the application sets
their permissions on its own pages. It lists name, email, phone, access (Has access,
Starts …, Ended …, Access removed, Email conflict, Not in Rock Software ID)
and the last sign-in, filtered by access (default: has access or starting), as cards at
phone width. A user's page shows their details, the access history and the sign-in
history (successful and refused sign-ins, sign-outs, back-channel logouts, with the
client's IP address), and offers:
- the application's buttons (
RockSoftwareUsers.userActions), e.g. Manage permissions; - Emergency sign-in link: a single-use link for this person, shown once, for when Rock Software ID is down; only for someone with access now;
- End sessions: every session of the user in this application ends on its next request.
Roles stay the application's, added to the page through
RockSoftwareUsers.userListColumns, RockSoftwareUsers.userViewSections and
RockSoftwareUsers.userActions.
The history lives in rock_software_users_session_events (types in rock_software_users_session_event_types and
rock_software_users_sign_in_method_types) and is kept for 180 days unless Settings › Identity provider
says otherwise.
Sessions
Settings › Identity provider › Sessions sets the longest session (hours) and the idle time after which a person is signed out (minutes). Both are off by default. They apply to every session the plugin established.
User menu
The signed-in person's menu, for the application's layout. A ready dropdown (no script,
colours from --rsu-menu-* CSS variables):
<?= $this->element('RockSoftware/Users.user_menu') ?>
Or the same entries as data, to render in the application's own menu:
$this->loadHelper('RockSoftware/Users.UserMenu'); foreach ($this->UserMenu->items() as $item) { // header (label, detail) · app (label, url, current) · link (label, url) // · sign_out (label, url, method = post: render with $this->Form->postLink()) }
The applications listed are the ones the person may open, from the provider's apps
claim (rocksoftware-identity's profile scope mapping), kept in the session at sign-in.
Nothing is listed after a password or emergency sign-in.
Emergency sign-in
When the provider is unavailable, someone with a shell in the application container runs:
bin/cake rock_software_users emergency_sign_in person@example.com # one person bin/cake rock_software_users emergency_sign_in --all # everyone with access now
Links are never printed. The command asks where to write them (or takes
--file <path>), creates the file with mode 0600 and never overwrites one. Each link
works once and expires after --valid (default 4h, at most 24h); issuing it is
recorded in the person's access history. People without access get none. The users
page makes one for one person too.
--create also creates the user when no one has the address, for an empty
development database such as a pull request preview. It is refused when the
application runs with debug off.
For automation
| What | Use |
|---|---|
GET /auth/client-metadata |
The addresses the identity provider must register (callback, sign-out return paths from signInUrl, back-channel logout, provisioning) and the plugin version. Public, answered before the application's authentication; new-id-app.sh reads it |
bin/cake rock_software_users status |
The stored connection as JSON (configured, enabled, issuer, client ID, required group), never the secret |
bin/cake rock_software_users restore_access <email> |
Give one person access back from the server |
bin/cake rock_software_users revocations [list|approve|reject] |
The removals the mass-removal stop held |
bin/cake rock_software_users scim_precheck [--file people.json] |
What switching SCIM on would do; changes nothing |
Development
composer install vendor/bin/phpunit composer cs-check
Tests use SQLite and a fake provider (tests/TestCase/FakeProvider.php) served through
CakePHP's HTTP client mocks, so no live provider is needed.