Authentication
In server mode Countersign does not keep user accounts or passwords. People, their names and their roles come from your identity provider at sign-in. This page covers the four sign-in methods, sessions, the re-authentication behind an electronic signature, what the audit trail records about identity, and the error messages you may meet.
The Microsoft Entra ID and Keycloak steps below could not be tested against real tenants while writing this page. They describe what must be achieved at each step; names of portal screens are given "as of writing" and may have moved. Test the result in your own environment, in particular the points marked verify.
Contents
- Methods at a glance
- How roles reach Countersign
- Microsoft Entra ID
- Generic OpenID Connect
- Authenticating reverse proxy
- Demo sign-in
- Sessions
- Electronic signatures and re-authentication
- What the audit trail records about identity
- Troubleshooting
Methods at a glance
COUNTERSIGN_AUTH |
Who authenticates the person | Session | Re-authentication for approvals | Use |
|---|---|---|---|---|
entra (default) |
Microsoft Entra ID, OpenID Connect | Signed cookie | Yes, default 15 minutes | Production with Microsoft 365 / Azure |
oidc |
Any OpenID Connect provider (Keycloak, Okta, ADFS, …) | Signed cookie | Yes, default 15 minutes | Production with another provider |
proxy |
A reverse proxy in front of the application; it passes identity in headers | None in Countersign | No (the proxy's business) | Environments that already authenticate at the edge |
demo |
Nobody: the visitor picks a person from a list | Signed cookie | No | Evaluation only |
Without COUNTERSIGN_STORAGE there is no sign-in at all: that is the browser-only demo.
Both OpenID Connect methods use the authorization code flow with PKCE (S256), state and nonce. The ID token is
verified with the provider's published keys: signature, issuer, audience (the client id), expiry (60 seconds of clock
tolerance) and nonce. Countersign uses only the ID token; it calls no user-info or directory API.
How roles reach Countersign
Countersign needs three things about a person, all taken from claims of the ID token (or from proxy headers):
| What | Setting | Default with entra |
Default with oidc |
Fallback |
|---|---|---|---|---|
| A stable id | COUNTERSIGN_ID_CLAIM |
oid |
sub |
none: without the claim the sign-in is refused |
| A display name | COUNTERSIGN_NAME_CLAIM |
name |
name |
preferred_username, then email, then the id |
| Roles | COUNTERSIGN_ROLE_CLAIM |
roles |
groups |
none: the person can read and nothing else |
The roles claim may be a JSON array of strings or one string with values separated by spaces or commas. It must be a
top-level claim: a nested claim such as Keycloak's default realm_access.roles is not read.
Each value is used as a role id of the configuration (coordinator, qa, …; see
Configuration). Where the provider's values are something else, translate
them:
COUNTERSIGN_ROLE_MAP='{"SG-Fleet-QA":"qa","SG-Fleet-Coordinators":"coordinator","6f1c2a9e-…":"steward"}'
A value found in the map is replaced; a value not in the map is used as it is; values that match no role of the configuration are ignored by the engine. Several provider values may map to the same role, and a person may hold several roles.
Roles are read once, at sign-in, and stay in the session until it ends. A role granted or withdrawn at the provider applies from the person's next sign-in.
Microsoft Entra ID
What you will end up with:
COUNTERSIGN_STORAGE=postgres
COUNTERSIGN_AUTH=entra # the default
COUNTERSIGN_ENTRA_TENANT_ID=<directory (tenant) id, a GUID>
COUNTERSIGN_ENTRA_CLIENT_ID=<application (client) id, a GUID>
COUNTERSIGN_ENTRA_CLIENT_SECRET=<client secret value>
COUNTERSIGN_SESSION_SECRET=<at least 32 random characters>
COUNTERSIGN_PUBLIC_URL=https://records.example.com
The issuer is built from the tenant id: https://login.microsoftonline.com/<tenant id>/v2.0. Use the tenant's GUID,
not a domain name and not common or organizations: the tokens name the tenant by its GUID, and the issuer must
match exactly.
1. Register the application
In the Entra admin centre (as of writing: Identity › Applications › App registrations › New registration), create a registration for Countersign.
- Supported account types: accounts in this organisational directory only (single tenant).
- Redirect URI: platform Web, value
<COUNTERSIGN_PUBLIC_URL>/auth/callback, for examplehttps://records.example.com/auth/callback. It must match exactly, including scheme and path. For local development addhttp://localhost:3000/auth/callback.
Note the Application (client) ID and the Directory (tenant) ID from the overview.
You do not need the implicit grant or "ID tokens" under implicit and hybrid flows: Countersign receives the ID token from the token endpoint in exchange for the code.
2. Create a client secret
Under the registration's certificates and secrets, add a client secret and copy its value (not its id) into
COUNTERSIGN_ENTRA_CLIENT_SECRET. Secrets expire: put the expiry date in your calendar, because sign-in stops when it
passes (The identity provider did not issue a token: …). Countersign sends the secret in the body of the token
request and does not support certificate credentials.
3. Define app roles whose Value equals the role ids
Under the registration's app roles, create one app role per role of your configuration:
| Field | Content |
|---|---|
| Display name | Anything readable, such as Quality Assurance |
| Allowed member types | Users/Groups |
| Value | Exactly the role's id in the configuration: qa, coordinator, system-owner, … |
| Description | What the role may do |
The Value is what arrives in the token's roles claim, and Countersign compares it with the configuration's role ids
character by character. If you cannot use the ids as values, set COUNTERSIGN_ROLE_MAP.
4. Assign people or groups to the roles
Roles are assigned on the enterprise application that belongs to the registration (as of writing: Identity › Applications › Enterprise applications › your application › Users and groups › Add user/group): choose a user or a group, then one of the app roles. Assigning groups to app roles needs an Entra ID licence level that supports it; without it, assign users one by one.
A person assigned to several roles gets all of them in the roles claim. A person with no role can sign in (unless
you require assignment, next step) and can read only.
5. Require assignment
On the enterprise application's properties, set "Assignment required?" to Yes. Then only people who hold at least one app role can sign in at all; everyone else is turned away by Entra before Countersign sees them. Because every signed-in person can read every record, this is the control over who can read. Recommended.
6. Claims
oidis the person's object id in the tenant: stable, never reused, the same in every application of the tenant. Countersign uses it as the person's id in the audit trail. LeaveCOUNTERSIGN_ID_CLAIMat its default;subis different per application andpreferred_usernameoremailcan change.nameis delivered with theprofilescope, which Countersign requests by default (COUNTERSIGN_OIDC_SCOPES=openid profile email).rolesis present when the person holds an app role.auth_time: verify. Countersign readsauth_timefrom the ID token to know when the person last proved their identity. In Entra this is an optional claim that, as far as we know, must be added to the ID token under the registration's token configuration. If the claim is absent, Countersign falls back to the token's issue time, which is the time of the sign-in redirect, not of the last real authentication; see the section on signatures for why that matters and how to test it. Do not take this on trust: sign in, decode the ID token or look at an approval's audit entry, and compare.
Using groups instead of app roles
If your organisation manages access by security groups and does not want app roles:
Add the groups claim to the ID token (token configuration › add groups claim). The claim then carries the groups' object ids.
Set
COUNTERSIGN_ROLE_CLAIM=groups COUNTERSIGN_ROLE_MAP='{"<object id of the QA group>":"qa","<object id of the coordinators group>":"coordinator"}'
Be aware of the group overage: for people in very many groups Entra leaves the groups claim out of the token and
expects the application to ask Microsoft Graph instead. Countersign does not do that, so such a person would arrive
without roles. Restricting the claim to groups assigned to the application avoids it. App roles do not have this
problem and are the recommended way.
Check the result
- Open the application and sign in.
GET /api/bootstrapshows who you are and with which roles:"user": {"id": "<oid>", "name": "…", "roles": ["qa"]}. - Approve something and look at the entry:
auth.methodisentra,auth.issueris your tenant's issuer,auth.authTimeis the time you last authenticated. - Wait longer than
COUNTERSIGN_SIGN_MAX_AGE_MINUTES, approve again, and confirm that Entra asks for your credentials and thatauth.authTimein the new entry is the time of that prompt.
Generic OpenID Connect
COUNTERSIGN_AUTH=oidc
COUNTERSIGN_OIDC_ISSUER=https://id.example.com/realms/company # exactly as the tokens' "iss" claim
COUNTERSIGN_OIDC_CLIENT_ID=countersign
COUNTERSIGN_OIDC_CLIENT_SECRET=<secret> # optional: leave out for a public client with PKCE
COUNTERSIGN_ROLE_CLAIM=groups # the default for oidc
COUNTERSIGN_SESSION_SECRET=<at least 32 random characters>
COUNTERSIGN_PUBLIC_URL=https://records.example.com
Requirements on the provider:
- A discovery document at
<issuer>/.well-known/openid-configurationwithauthorization_endpoint,token_endpointandjwks_uri, reachable from the server. - The
issclaim of its ID tokens equalsCOUNTERSIGN_OIDC_ISSUERexactly, character by character. The setting is used as you write it: if the provider's issuer ends with a slash (Auth0'shttps://<tenant>.auth0.com/), write the slash; if it does not, leave it out. Only the address of the discovery document is built without a doubled slash. - Authorization code flow with PKCE (S256).
- If a client secret is set, the token endpoint must accept it in the request body (
client_secret_post). - Redirect URI to register:
<COUNTERSIGN_PUBLIC_URL>/auth/callback. - An ID token with a stable subject, a name and a top-level roles or groups claim.
- For signatures: support for
max_age=0orprompt=login, and anauth_timeclaim in the ID token.
Keycloak example
In a realm (here company):
Client. Create an OpenID Connect client with client id
countersign. Turn client authentication on (a confidential client) and keep the standard flow; the other flows are not needed. Valid redirect URI:https://records.example.com/auth/callback(andhttp://localhost:3000/auth/callbackfor development). Copy the client secret from the client's credentials.Roles or groups in the ID token. Either
- groups: add a mapper of type Group Membership to the client (its dedicated client scope), token claim name
groups, full group path off, added to the ID token. Create groups named after the configuration's role ids (qa,coordinator, …) and put people in them; or - realm roles: add a mapper of type User Realm Role, token claim name
roles, multivalued, added to the ID token, and setCOUNTERSIGN_ROLE_CLAIM=roles. Create realm roles named after the role ids.
A mapper is needed in both cases, because Keycloak's default places roles in the nested claim
realm_access.roles, which Countersign does not read.- groups: add a mapper of type Group Membership to the client (its dedicated client scope), token claim name
Settings.
COUNTERSIGN_AUTH=oidc COUNTERSIGN_OIDC_ISSUER=https://id.example.com/realms/company COUNTERSIGN_OIDC_CLIENT_ID=countersign COUNTERSIGN_OIDC_CLIENT_SECRET=<from the credentials tab> COUNTERSIGN_ROLE_CLAIM=groups
Keycloak includes auth_time in ID tokens and honours max_age and prompt=login. sub is the user's id in the
realm and is stable. A local Keycloak for development is described in
Developer setup.
Other providers
| Provider | Notes |
|---|---|
| Okta | Issuer is the authorisation server's issuer (the org URL or …/oauth2/<server id>). Add a groups claim to the ID token (a claim named groups with a filter) and map group names with COUNTERSIGN_ROLE_MAP if they are not the role ids. |
| Auth0 | The issuer ends with a slash: set COUNTERSIGN_OIDC_ISSUER with it. Roles need a custom, namespaced claim added by an Action; set COUNTERSIGN_ROLE_CLAIM to the claim's full name. |
| ADFS 2019 or newer, Google, others | Any provider that meets the requirements above. A provider that can deliver no roles or groups claim (Google) leaves everyone read-only; put an authenticating proxy that adds groups in front instead. |
These notes are generic; consult the provider's documentation for the current steps.
Authenticating reverse proxy
With COUNTERSIGN_AUTH=proxy, Countersign authenticates nobody itself. A reverse proxy in front of it signs people in
and passes who they are in request headers.
COUNTERSIGN_AUTH=proxy
COUNTERSIGN_PROXY_SECRET=<at least 16 random characters>
# optional, shown with their defaults:
COUNTERSIGN_PROXY_USER_HEADER=x-forwarded-user
COUNTERSIGN_PROXY_NAME_HEADER=x-forwarded-preferred-username
COUNTERSIGN_PROXY_ROLES_HEADER=x-forwarded-groups
| Header | Content |
|---|---|
X-Countersign-Proxy-Secret |
The value of COUNTERSIGN_PROXY_SECRET. Compared in constant time on every request. Without it, or with a wrong value, the request is treated as not signed in (HTTP 401 Sign in first.). |
| the user header | The person's stable id. Required. |
| the name header | The display name. If absent, the id is used. |
| the roles header | Role ids or group names, separated by commas. COUNTERSIGN_ROLE_MAP applies. |
The shared secret is what makes this safe: anyone who can reach the application directly could otherwise send
X-Forwarded-User: someone-else. Therefore:
- The proxy must set all four headers itself on every request and so overwrite anything the client sent.
- Only the proxy may be able to reach the application's port (network policy, a private network, or a loopback listener).
- Treat the proxy secret like a password and rotate it together on both sides.
No session cookie is involved; identity is read from the headers of each request, so role changes apply at once. The
audit entry records auth.method: "proxy" without an authentication time or issuer. The re-authentication check for
approvals is not applied with this method: if your procedures require a fresh authentication for a signature,
enforce it at the proxy or use entra or oidc.
Example: oauth2-proxy with nginx
oauth2-proxy authenticates against your identity provider and answers nginx's auth_request; nginx passes the result
on. Adapt to your versions; this sketch has not been run as part of writing this page.
server {
listen 443 ssl;
server_name records.example.com;
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
location = /oauth2/auth {
internal;
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
# Monitoring may reach the health endpoint without signing in; restrict by network instead.
location = /api/health {
proxy_pass http://127.0.0.1:3000;
}
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $name $upstream_http_x_auth_request_preferred_username;
auth_request_set $groups $upstream_http_x_auth_request_groups;
# Always set all four, so that nothing the client sent gets through.
proxy_set_header X-Forwarded-User $user;
proxy_set_header X-Forwarded-Preferred-Username $name;
proxy_set_header X-Forwarded-Groups $groups;
proxy_set_header X-Countersign-Proxy-Secret "<the proxy secret>";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:3000;
}
}
oauth2-proxy must run with --set-xauthrequest so that it returns the X-Auth-Request-* headers, and with a provider
configuration that yields groups. Choose as the user header a value that is stable (a subject or object id rather
than an e-mail address).
Demo sign-in
COUNTERSIGN_AUTH=demo
COUNTERSIGN_CONFIG=preset:fleet
COUNTERSIGN_DEMO_DATA=1 # optional
The sign-in page lists the people of the preset (Alice the coordinator, Kai from QA, …) and anyone can become any of
them with one click: POST /auth/demo {"id": "kai"}. There is no password. Entries are recorded with
auth.method: "demo".
Evaluation only. Never use it for real records.
docker compose upuses it so that you can try the server mode without an identity provider.By default the list comes from the preset whose id equals the configuration's id (
fleet,property,clinic).For a configuration of your own, or to replace a preset's list, name the people in
COUNTERSIGN_DEMO_USERS: a JSON list of objects withid,nameandroles(role ids of the configuration):COUNTERSIGN_CONFIG=./examples/config/lab-equipment.json COUNTERSIGN_DEMO_USERS='[{"id":"mia","name":"Mia Keller","roles":["metrologist"]},{"id":"noor","name":"Noor Haddad","roles":["lab-manager"]}]'Remember four eyes: list at least two people for everything you want to see approved. Without the variable and without a preset the list is empty and nobody can sign in. A value that is not such a list stops the server:
COUNTERSIGN_DEMO_USERS must be JSON such as [{"id":"ann","name":"Ann Lee","roles":["qa"]}].GET /api/bootstrapreturns the list asdemoPeopleand the configuration asdemoConfig, without sign-in, so that the sign-in page can show who holds which role.The endpoint answers 404
Demo sign-in is off.with every other method.
Sessions
With entra, oidc and demo, a successful sign-in sets one cookie:
| Property | Value |
|---|---|
| Name | countersign_session |
| Content | <base64url(JSON)>.<base64url(HMAC-SHA256)>: the person's id, name and roles, the sign-in method, issuer and authentication time, and an expiry. Signed with COUNTERSIGN_SESSION_SECRET; not encrypted (it holds nothing the person does not know about themselves). |
| Attributes | HttpOnly, SameSite=Lax, Path=/, Max-Age = the lifetime; Secure when the public address is HTTPS |
| Lifetime | COUNTERSIGN_SESSION_HOURS, default 8. Fixed from sign-in; it is not extended by activity. |
Consequences of keeping no session state on the server:
- Any number of servers can serve the same person, as long as they share
COUNTERSIGN_SESSION_SECRET. - Changing
COUNTERSIGN_SESSION_SECRETsigns everyone out at once, because no existing cookie verifies any more. That is also the way to end all sessions in an emergency. There is no way to end one person's session from the server before it expires; disable the person at the identity provider (which prevents the next sign-in) and, if needed, rotate the secret. POST /auth/logoutdeletes the cookie in the browser. It does not sign the person out of the identity provider, and a copy of the cookie taken earlier would remain valid until its expiry.- Name and roles are those at sign-in. Shorten
COUNTERSIGN_SESSION_HOURSif role changes must take effect faster.
During sign-in a second cookie, countersign_signin, holds the state, nonce and PKCE verifier for at most ten
minutes; it is deleted when the sign-in completes or fails.
The secret must be at least 32 characters; the server refuses to serve otherwise. Generate one with
openssl rand -base64 48.
Electronic signatures and re-authentication
An approval is the electronic signature in Countersign: a named person, at a recorded time, states what their signature means, on a version someone else wrote. To tie the signature to the person at the keyboard, an approval requires a recent authentication:
- Setting:
COUNTERSIGN_SIGN_MAX_AGE_MINUTES. Default 15 withentraandoidc.0turns the check off. - It applies to the
approvecommand, withentraandoidconly. Withproxyanddemothe check is never applied, whatever the setting.
How it works:
The server compares the authentication time in the session with its clock. If the sign-in is older than the limit, it refuses the approval with HTTP 401:
{ "error": "A signature needs a sign-in from the last 15 minutes. Sign in again, then approve.", "code": "reauth_required", "loginUrl": "/auth/login?fresh=1" }The application sends the person to that address. With
fresh=1, Countersign addsmax_age=0andprompt=loginto the authorization request: both tell the provider to authenticate the person now instead of reusing its own single sign-on session.max_ageis the standard parameter;prompt=loginis understood by providers that ignoremax_age.The person enters their credentials (and second factor, if the provider asks) and comes back with a new session whose authentication time is now. If the provider answers a
fresh=1sign-in with a token whoseauth_timeis more than five minutes old, it evidently did not ask again, and Countersign refuses the sign-in:The identity provider did not ask you to sign in again (its authentication time is old).The person approves; the entry records how and when they were authenticated:
"auth": { "method": "entra", "issuer": "https://login.microsoftonline.com/<tenant>/v2.0", "authTime": "2026-11-11T09:14:03.000Z" }
Where the authentication time comes from
authTime is the ID token's auth_time claim: the moment the provider last actually authenticated the person. If
the provider does not put auth_time in the ID token, Countersign uses the token's issue time (iat) instead. That
fallback is weaker:
- The issue time is the time of the redirect through the provider. A provider that silently reuses its own session issues a token "now" without the person having typed anything.
- With the fallback, an ordinary sign-in therefore always looks fresh, and an approval within the limit after it passes the check even if the person last entered a password hours ago.
- The forced re-authentication of step 2 then relies entirely on the provider honouring
max_age=0/prompt=login: the five-minute check of step 3 can only be made whenauth_timeis present.
So, for every provider and in particular for Entra, verify in your own tenant:
- that the ID token carries
auth_time(for Entra: the optional claim may need to be configured, see above); - that a sign-in with
fresh=1really prompts for credentials; - that
auth.authTimein the resulting approval entry is the time of that prompt and not merely the time of the redirect.
Record the result in your validation documents (see the validation pack). If your provider
cannot deliver auth_time, describe the fallback in your risk assessment or choose a shorter session lifetime.
What the signature consists of
| Element | Where it is recorded |
|---|---|
| Who | actor (the stable id), actorName, actorRoles of the entry |
| When | at, the server's clock, UTC |
| Meaning | after.meaning and command.meaning: the text the approver gave |
| On what | recordId, v; the version's data is in the entries that wrote it |
| How authenticated | auth.method, auth.issuer, auth.authTime |
| That it was a second person | Enforced by the engine; visible as different actor values on the version's entries |
| Integrity | The entry's place in the hash chain |
Countersign does not ask for a password itself and stores none; the "two components" of a signature are checked by the identity provider. Whether that meets the regulations you work under is for your validation to establish.
What the audit trail records about identity
Every entry holds, at the moment of the action:
| Field | Content |
|---|---|
actor |
The stable id: Entra oid, the OIDC subject, the proxy's user header, a demo person's id, or what --actor-id said on the command line. |
actorName |
The display name at that time. |
actorRoles |
The role ids at that time, after COUNTERSIGN_ROLE_MAP. |
auth.method |
entra, oidc, proxy, demo, cli, or system for the initial configuration. |
auth.issuer |
The identity provider's issuer; for cli the operating-system account and host (jdoe@ops-01). |
auth.authTime |
When the person authenticated (see above); for cli and demo the time of the command or sign-in. |
These values are written by the server from the session or the proxy headers, never from the request body, and they are part of the hashed entry. A later change of someone's name or roles does not alter old entries. There is no user table: the list of people the application shows is everyone who appears in the trail, with the name and roles of their latest entry.
No e-mail address, password, token or IP address is stored.
Troubleshooting
Sign-in failures are shown on a page titled "Sign-in failed" (HTTP 400) or returned as JSON by the API.
| Message | Where | Cause and remedy |
|---|---|---|
The server is not configured completely. with a list such as COUNTERSIGN_ENTRA_TENANT_ID is required. |
any endpoint, HTTP 503 | A required variable is missing. The list names each one; see Deployment. |
COUNTERSIGN_SESSION_SECRET must be at least 32 characters (for example: openssl rand -base64 48). |
in that list | Set a longer secret. |
COUNTERSIGN_PROXY_SECRET (at least 16 characters) is required with proxy sign-in: … |
in that list | Set the proxy secret. |
Sign-in could not be started: The identity provider's discovery document could not be read (HTTP 404). |
/auth/login, HTTP 502 |
Wrong issuer or tenant id. Open <issuer>/.well-known/openid-configuration yourself. |
Sign-in could not be started: fetch failed |
/auth/login, HTTP 502 |
The server cannot reach the provider: DNS, firewall, outbound proxy, TLS trust. |
Sign-in could not be started: The identity provider's discovery document is incomplete. |
/auth/login, HTTP 502 |
The document lacks an authorization endpoint, token endpoint or key set. |
| A provider page complaining about the redirect URI (Entra: AADSTS50011) | at the provider | The registered redirect URI is not exactly <COUNTERSIGN_PUBLIC_URL>/auth/callback. Behind a proxy, set COUNTERSIGN_PUBLIC_URL. |
This sign-in was not started in this browser, or it took longer than ten minutes. |
/auth/callback |
The countersign_signin cookie is missing: cookies blocked, another browser or host name, or a slow sign-in. Also happens when the callback's host differs from the one the sign-in started on (www. vs. none, http vs. https). |
The sign-in took too long; start again. |
/auth/callback |
More than ten minutes between start and return. |
The identity provider refused the sign-in: <description> |
/auth/callback |
The provider returned an error: the person cancelled, is not assigned to the application (AADSTS50105 when assignment is required), or consent is missing. |
The sign-in response does not belong to this browser (state mismatch). |
/auth/callback |
An old or foreign response, often from the back button or two sign-ins in parallel. Start again. |
The sign-in response has no code. |
/auth/callback |
The provider did not use the code flow. Check the client's allowed flows. |
The identity provider did not issue a token: <description> |
/auth/callback |
The code exchange failed: wrong or expired client secret (AADSTS7000215, AADSTS7000222), redirect URI mismatch at the token endpoint, a client that does not accept the secret in the request body. |
The ID token does not belong to this sign-in (nonce mismatch). |
/auth/callback |
A replayed response. Start again. |
The ID token has no "oid" claim. (or the configured claim) |
/auth/callback |
The claim named by COUNTERSIGN_ID_CLAIM is not in the ID token. There is no fallback to another claim: a person's id must not silently change its source. Make the provider deliver the claim, or set the variable to the claim it does deliver. |
The identity provider did not ask you to sign in again (its authentication time is old). |
/auth/callback, after a sign-in started with fresh=1 |
The provider returned an auth_time more than five minutes old: it reused its session instead of authenticating the person. Check that the provider honours max_age=0 and prompt=login for this client. |
A message about an unexpected iss or aud claim, or a failed signature |
/auth/callback |
Raised by the token verification. iss: the issuer setting does not equal the token's issuer (domain instead of tenant GUID, a missing or extra trailing slash, a different realm). aud: the client id is wrong. |
Sign in first. with "code": "signin_required" |
API, HTTP 401 | No valid session: not signed in, expired, the session secret changed, or the servers do not share one secret. With proxy: the secret header is missing or wrong, or the user header is empty. |
A signature needs a sign-in from the last 15 minutes. Sign in again, then approve. with "code": "reauth_required" |
/api/act, HTTP 401 |
Normal: re-authenticate. If it repeats right after re-authenticating, the server's clock is wrong. |
The request is missing the X-Countersign-Request header. |
writes, HTTP 403 | The caller did not send X-Countersign-Request: 1; see API. |
The request comes from another site. |
writes, HTTP 403 | The request's Origin is not the application's address. Behind a proxy: set COUNTERSIGN_PUBLIC_URL, or pass X-Forwarded-Proto and X-Forwarded-Host. |
Demo sign-in is off. |
/auth/demo, HTTP 404 |
COUNTERSIGN_AUTH is not demo. |
Unknown demo person. |
/auth/demo, HTTP 400 |
The id is not in the list: the preset's people, or COUNTERSIGN_DEMO_USERS. With a configuration of your own and no COUNTERSIGN_DEMO_USERS the list is empty. |
Signed in, but every action says As someone without a role you can't … |
the application | No role arrived, or none matches a role id. Look at user.roles in /api/bootstrap: empty means the claim is missing or named differently (COUNTERSIGN_ROLE_CLAIM); values that are not role ids need COUNTERSIGN_ROLE_MAP. |
| Signed out again after every sign-in, over HTTP in development | the browser | The cookie is marked Secure because COUNTERSIGN_PUBLIC_URL starts with https:// while you browse over http://. |