Skip to content

Account & Security — End-User Guide

Overview

Every application scaffolded on Pramnos Framework ships a complete, self-service account area — no code required beyond branding. This guide is written for the end user of such an application: it explains what each account page does and how to use it.

The account area lives under a single base path (shown here as /account — your application may brand it differently) and groups everything a signed-in user needs to manage their identity, security and privacy:

Area Pages
Profile View / edit profile and profile picture, dashboard
Security Change password, Two-Factor Authentication, Passkeys, Active Sessions
Privacy Privacy settings, Export my data, Delete account

All pages share one navigation sidebar and breadcrumb, so the layout is the same whichever theme (plain-CSS, Bootstrap, Tailwind) the application uses.


Profile picture

Account → Profile has a Profile Picture card. Choose a JPEG, PNG, GIF or WebP picture. Where the project has Cropper.js (project:install cropperjs), a dialog opens to crop it to a square, turn or flip it. The picture is made at most profile_photo_upload_size pixels (512 by default) in the browser, and sent as soon as it is cropped (see the Image Upload Guide). Without Cropper.js, press Upload. Whatever the format, it is stored as JPEG, so every client can show it; a transparent picture gets a white background. The picture shown is a square of 256 pixels cropped from the centre, and it replaces the previous one. The original is kept beside it, at most 1024 pixels on its longer side. A file over 10 MB, or one that declares more than 40 million pixels, is refused. A phone photo is turned upright from its EXIF orientation when PHP has the exif extension. Without the extension it is stored as the camera wrote it, which can be sideways. Remove takes it away. Until one is uploaded, the application shows its own default if it has one (the defaultAvatarUrl setting), or no picture.

The same picture appears wherever the application shows the account, with nothing more to do:

  • GET /me returns it as avatarurl, an absolute URL;
  • the OpenID Connect picture claim carries it to the applications you have signed in to;
  • the administration's user list and every screen that reads the account's avatarurl show it.

How it is stored. The picture is kept through the framework's media library. users.photo holds its media usage, and avatarurl is worked out from that usage when the account loads. A replaced or removed picture is released, and its files are deleted once nothing else uses them. The same image uploaded twice is stored once.

Your data. Export my data includes the picture's address under Profile picture. Delete my account deletes the picture with the account.

From a sign-in provider. An application whose users sign in through Google, Microsoft or another provider can copy the provider's picture the first time, for a user who has none of their own. It does so only when the profile_photo_from_provider setting is on. The picture is copied, never linked, because a provider's picture address expires and a page that allows images only from itself would not show it. The sign-in callback calls:

\Pramnos\User\ProfilePhoto::adoptFromProvider($user, $profile['picture'] ?? '');

From Gravatar, when the user asks. The card has a Use my Gravatar button, with a line saying what it does. Pressing it asks Gravatar for the picture of the account's address and keeps a copy, converted like an upload. Gravatar is told only about the person who pressed the button, at that moment, as a SHA-256 of their address. Nobody else's address is sent, and nothing is sent unless somebody presses it. The server makes the request, so Gravatar sees neither the visitor's own address nor when their pages are read, and the picture does not depend on Gravatar afterwards. An address with no Gravatar picture gets "There is no Gravatar picture for your email address", not Gravatar's generic placeholder. An installation that wants no contact with Gravatar at all sets profile_photo_gravatar to 0, and the button is not shown. A privacy policy that lists every third party should still name Gravatar as an option the user can choose.

Gravatar is never asked for a picture on its own initiative: that would send every address's hash to a third party without anyone asking.

For an application's own code: ProfilePhoto::set($user, $_FILES['photo']) and ProfilePhoto::fromGravatar($user) return null or the reason they refused, ProfilePhoto::remove($user) removes the picture, and ProfilePhoto::url($user->photo) gives the absolute address.

Creating an account

If the server has self-service registration switched on, Create Account takes you to /register. You choose a username and give an email address and a password of at least 8 characters containing a digit and a symbol.

Many installations do not offer this — accounts are created by an administrator, or by whatever system owns the people using it. On those, the page tells you registration is closed rather than disappearing.

Invited? An invitation mail carries a link that opens the form for your address — it is filled in and cannot be changed, because the invitation is for you. The link works once and expires after two days unless the server says otherwise; ask whoever invited you for a new one.

Asked to confirm your address? Some servers accept only addresses at certain domains, or ask every new account to confirm its address. Then you are mailed a link after registering, and you cannot sign in until you open it. Signing in before that tells you so, and after ten minutes a new link is sent.

Creating an account does not sign you in: you are sent to the sign-in page, so the first sign-in goes through the same checks as every other one.

Signing in

The login screen accepts your username or email plus your password. Depending on how the application is configured you may also see:

  • "Sign in with a passkey" — passwordless login using a device passkey (see Passkeys below).
  • A two-factor prompt after your password, if you have 2FA enabled.

If you enter the wrong password several times, the account is temporarily locked (brute-force protection). Each failed attempt — and any lockout it triggers — is recorded in your activity log.

Staying signed in

Ticking "Remember me" keeps you signed in on that device. You can review and end remembered sessions from Active Sessions.


Changing your password

Security → Change Password.

Enter your current password, then your new one twice. The new password must:

  • be at least 8 characters,
  • contain at least one digit, and
  • contain at least one special character.

The form checks these rules as you type. On success your password is updated and a password_changed entry is added to your activity log.

Requiring a change on first sign-in

A client application can make users change the password they were given before it lets them in. It needs two things from the server.

When the password last changed. GET /me carries password_changed_at: ISO 8601, or null if the user has never changed their password. User::getPasswordChangeDate() returns the same thing as a Unix timestamp. Only a change counts. User::setPassword() dates it when an account that already has a password is given a different one. A first password (a new account, or the one an administrator sets on an account that had none) is not dated, and neither is setting the same password again. Every way the framework writes a password goes through setPassword(): this page, the reset page, the admin user form and pramnos user:password. An administrator or operator who sets a different password on an existing account therefore counts as a change. The date is kept in the passwordchangedate userdetails field. For a change made before that field was kept, the newest password_changed entry in the activity log answers instead.

A way back. The change-password page takes returnUrl, and optionally client_id (the application's apikey), and sends the user there after a successful change:

https://auth.example/Account/changepassword?returnUrl=https%3A%2F%2Fclient.example%2Fwelcome&client_id=<apikey>

The allowed addresses are the ones the applications themselves declare on their edit screen. There is no separate list to keep in step:

  • each active application's (status = 1) URL,
  • every entry of its callbacks (commas or whitespace between them),
  • every CORS origin in its settings.

With a client_id, only that application's addresses count. An address that no application declares is ignored, and the page returns to the security screen as usual.

Where nothing is declared, any http(s) address is accepted. That applies to the application a client_id names, or, without one, to every active application. Declaring an address is what narrows it. A client_id that names no active application accepts nothing; otherwise a made-up one would unlock every address.

  • Scheme and host must match a declared address exactly. The port must match only when the declared address names one, so a declared http://localhost admits any local port.
  • Only http and https are ever accepted.
  • The address and client_id survive a failed attempt: the error redirect back to the form carries both. They also survive the sign-in detour, because the login page returns to the full URL.
  • The form page adds the address's origin to its CSP form-action. Without that, Chrome silently cancels the cross-origin redirect after the POST.
  • The form page keeps both in the session, so a copy of the view made before it carried them still returns. The view also receives the address as $this->returnUrl.

The client's flow is then: read /me; while password_changed_at is null, send the user to changepassword?returnUrl=<back to the client>; on return, read /me again.


Two-Factor Authentication (2FA)

Security → Two-Factor Authentication.

Two-factor authentication adds a one-time code (TOTP) on top of your password.

  1. Set up: the page shows a QR code — scan it with an authenticator app (Google Authenticator, Authy, 1Password, …) or enter the secret manually.
  2. Confirm: type the 6-digit code the app shows to verify the pairing.
  3. Backup codes: you are given one-time backup codes — store them somewhere safe. Each can be used once if you lose access to your authenticator.

Once enabled, you will be asked for a code (or a backup code) after your password on every sign-in. You can regenerate backup codes or disable 2FA from the same page (disabling requires your password).


Passkeys

Security → Passkeys.

A passkey is a phishing-resistant credential stored on your device (Face ID / Touch ID / Windows Hello / a hardware security key). It can be used instead of your password or as a second factor.

  • Add a passkey: click Add passkey, give it a name (e.g. "MacBook"), and follow your browser/device prompt.
  • Rename / remove: manage each passkey from the list; removing one takes effect immediately.
  • Sign in: on the login screen choose Sign in with a passkey.

Only passkey metadata (its name, when it was added, last used) is ever stored server-side — never anything that could be used to impersonate you.


Active Sessions

Security → Active Sessions.

This lists the devices/browsers currently signed in to your account, with the approximate location (IP), browser and last-seen time. If you see a session you do not recognise, click Sign out next to it — that device is signed out on its next request. Your current session is always kept.


Recent Account Activity

The Security page shows your recent account activity — sign-ins, sign-outs, failed sign-ins, password changes, 2FA/passkey changes, application authorizations, privacy changes and data-export requests — each with a timestamp and the IP/browser it came from. Use it to spot anything you did not do.


Authorized Applications

Security → Authorized Applications.

If you have granted third-party applications access to your account (via "Sign in with …", or by connecting an assistant), they are listed here. That includes one you approved whose sessions have since expired, because your approval would let it back in without asking. Revoke removes an application's access, invalidates its tokens immediately, and withdraws your approval, so it has to ask again. An application the site has since deactivated can be revoked too.


Privacy settings

Privacy.

Toggle privacy preferences such as usage-analytics sharing and marketing emails. Changes are saved immediately and recorded in your activity log.

Each opt-in mailing list the site runs — a newsletter, say — appears here too, unticked until you join it. With a confirmed address, ticking it subscribes you at once; otherwise you are sent a link to confirm, and the screen says it is waiting. Unticking is the same as the unsubscribe link in the mail.


Export my data (GDPR)

Privacy → Export my data.

You can download a complete copy of the personal data the application holds about you. The confirmation page lists exactly which sections the export contains (profile, authorized applications, OAuth consents, passkeys, two-factor status, active sessions, tokens, token activity, account details, privacy settings, activity log, mailing-list subscriptions and profile picture — plus any application-specific data).

Confirm to download a single JSON file. For your safety the export never contains secrets — no passwords, token values, 2FA secrets/backup codes, passkey keys, or raw request bodies.


Delete my account (GDPR)

Privacy → Delete account.

This permanently erases your personal data. To prevent accidents you must:

  1. re-enter your password, and
  2. type DELETE in the confirmation field.

Deletion removes your data across all account tables. This cannot be undone — export your data first if you want a copy.


If a screen renders blank

View::display() returns the markup — it does not echo it. So an action that renders must either return that value or add it to the document:

// An action declared to return
return $view->display('profile');

// An action declared `: void`
\Pramnos\Framework\Factory::getDocument()->addContent(
    (string) $view->display('profile')
);

Calling $view->display(...) as a bare statement from a void action throws the markup away. The response is still a 200, with the theme, the layout and the correct <title> — everything except the page.