Single sign-on (OIDC)¶
Any OpenID Connect provider works, several at once: Pocket ID, Authelia, Authentik, Keycloak, Zitadel, Kanidm, Microsoft Entra ID, Google and others. The sign-in page shows a "Continue with ..." button for each.
Setting it up¶
- In the provider, create a client (also called an application) for Punchlist:
- Redirect URI:
https://tasks.example.com/auth/oidc/<name>/callback, where<name>is the name you give the provider below (lowercase). - Type: confidential (with a secret) is usual; a public client also works, since Punchlist always uses PKCE.
- Scopes:
openid profile email, plusgroupsif you will use groups. Punchlist asks forgroupsby itself as soon as any group setting (REQUIRED_GROUPS,ROLE_MAP,GROUP_MAP) is set;OIDC_<NAME>_SCOPESoverrides the list for providers that have no such scope. - Client authentication: the secret goes in the token request's body
(
client_secret_post). Most providers accept it by default; Authelia needs it set. - Set
PUBLIC_URLto the address people open, and the provider's settings:
environment:
PUBLIC_URL: https://tasks.example.com
OIDC_PROVIDERS: pocket
OIDC_POCKET_AUTHORITY: https://id.example.com
OIDC_POCKET_CLIENT_ID: punchlist
OIDC_POCKET_CLIENT_SECRET_FILE: /run/secrets/oidc_secret
OIDC_POCKET_DISPLAY_NAME: Pocket ID
- Restart. Or do all of it from Admin > Settings (or the first-start setup), which shows the redirect URI to register.
Every OIDC_<NAME>_* setting is in all settings.
The Android app¶
The app signs in through the same provider and the same redirect URI, in a browser tab instead of
inside the app (passkeys do not work inside apps, and some providers refuse them). There is
nothing extra to register with the provider. After the provider, the person confirms on a
Punchlist page, and the app receives a one-time code it can only use with a secret it kept to
itself. The app's session is a copy of the browser's, so OIDC_<NAME>_SESSION_HOURS still applies.
Who gets in¶
| Setting | Effect |
|---|---|
OIDC_<NAME>_REQUIRED_GROUPS=punchlist |
Only members of at least one of these groups may sign in. Checked at every sign-in. |
OIDC_<NAME>_AUTO_CREATE=true |
A first sign-in makes an account. Off (the default): the person waits under Admin > Sign-in for approval. |
OIDC_<NAME>_ROLE_MAP=punchlist-admins=admin,it=superadmin |
Roles from groups, set at every sign-in. The highest match wins; no match means user. |
OIDC_<NAME>_GROUP_MAP=family=Family |
Server group membership from provider groups. * mirrors every provider group. |
LOCAL_LOGIN=disabled |
No passwords at all, in the app or the API. An admin gets back in with punchlist admin login-link. |
A new account never takes over an existing one with the same username: it gets a number after the
name, unless you turn on username linking. Usernames come from
preferred_username (OIDC_<NAME>_USERNAME_CLAIM), else the email.
Signing out¶
Signing out of Punchlist ends the Punchlist session only: the provider still knows the person, so
Continue with signs them straight back in. On a shared computer that is not what you want. With
OIDC_<NAME>_SIGN_OUT=true, signing out also ends the provider session (OpenID Connect
RP-initiated logout), and the provider sends the browser back to the sign-in page.
For that, the provider must:
- advertise an
end_session_endpointin its discovery document (Authentik, Keycloak, Pocket ID, Zitadel, Authelia and Entra ID do), and - accept
<PUBLIC_URL>/loginas a post-logout redirect URI. Add it next to the callback address in the client's settings (Keycloak: Valid post logout redirect URIs; Authentik: the provider's redirect URIs).
If the provider cannot be reached, Punchlist still signs out and only its own session ends.
Linking accounts¶
People link a provider to their existing account under Settings > Sign-in, and unlink it as
long as another way to sign in remains. An admin can link one by hand (Admin > Users > Link a
provider account, with the provider and the account's sub), or join a waiting sign-in to an
existing person (Admin > Sign-in > Link to an account).
A first sign-in can also find its account by itself. Punchlist tries these in order, and the first that matches wins:
- A mapping.
OIDC_<NAME>_USER_MAP=ben=bendimit,kim.lee=kimsends the provider user on the left to the Punchlist account on the right, every time. Admins add more on Admin > Sign-in > Mappings (the setting wins when both name the same provider user). A mapping also moves a link: someone already linked to another account lands in the mapped one at their next sign-in. - A migration window, while one is open (see moving to another provider).
- The same username, with
OIDC_<NAME>_LINK_BY_USERNAME: truejoins the account named like the provider's username, if that account has no link to this provider yet, and only when it is a plain user account.alljoins admin accounts too. Use it only when people cannot change their own username at the provider: Pocket ID, for one, lets them by default, which would let anyone sign in as an admin by renaming themselves. Map admins withUSER_MAPinstead.- The same verified email, with
OIDC_<NAME>_LINK_BY_EMAIL=true: the one account that already has that email from another provider. Off by default: a provider that does not truly verify emails would let someone take over an account. - Otherwise a new account (
AUTO_CREATE) or a wait for an admin.
Linking, mapping and moving only change which account a provider account signs into. Tasks, lists and shares never move from one account to another; an account that lost its link keeps everything and can be deleted or handed over from Admin > Users.
Admin > Sign-in (shown when at least one provider is set) lists every account with the provider accounts that sign into it: the provider, the username it sent, and when it was last used. From there an admin maps a provider username to an account, moves a provider account to another account, or unlinks it. Every change goes into the event log.
Provider icons¶
People who signed in through a provider see its icon on their profile picture. Punchlist uses
OIDC_<NAME>_ICON (an https address or a file in the container), else the provider's own
/favicon.ico, and keeps a copy for a day, so browsers never load it from the provider. Without an
image there, no icon shows.
A first sign-in also brings in the person's picture from the provider's picture claim, when their
profile has no photo. Many providers let people set that address themselves, so Punchlist fetches
it only from public internet addresses, or from the host of a configured provider (one on your own
network is fine). Anything else, and redirects to it, is refused and the profile keeps its initials.
Moving to another provider¶
To move everyone from pocket to keycloak without losing accounts:
# 1. Add keycloak to OIDC_PROVIDERS (keep pocket for now) and restart.
# 2. For a day, a first sign-in through keycloak joins the account with the same username.
docker exec punchlist punchlist oidc migration-window keycloak 24
# 3. Detach everyone from the old provider (accounts and data stay).
docker exec punchlist punchlist oidc unbind-all pocket
# 4. People sign in with keycloak. Then close the window and remove pocket.
docker exec punchlist punchlist oidc migration-window keycloak close
A migration window never joins an account that already has a keycloak link, and it matches
usernames only, so check that both providers hand out the same usernames. It never joins an admin
account either (someone could rename themselves at the new provider to take it), unless
OIDC_<NAME>_LINK_BY_USERNAME=all; map admins with USER_MAP instead. Someone left with no way
in gets a one-time link: punchlist admin login-link kim.
Provider notes¶
Create an OIDC client with the callback URL. Groups come with the groups scope; nothing else
to set. Authority: https://id.example.com.
Create an OAuth2/OpenID Provider (confidential, the redirect URI as strict) and an
Application using it. Authority: https://auth.example.com/application/o/<application-slug>/.
Groups arrive in the groups claim; nothing else to set.
Add a client under identity_providers.oidc.clients. Punchlist sends its client secret in the
token request's body, so the client must use client_secret_post (Authelia's default for a
confidential client is client_secret_basic, which fails with invalid_client):
- client_id: punchlist
client_name: Punchlist
client_secret: '$pbkdf2-sha512$...' # the hash of OIDC_<NAME>_CLIENT_SECRET
public: false
authorization_policy: two_factor
require_pkce: true
pkce_challenge_method: S256
redirect_uris:
- https://tasks.example.com/auth/oidc/authelia/callback
scopes: [openid, profile, email, groups]
response_types: [code]
grant_types: [authorization_code]
response_modes: [query]
token_endpoint_auth_method: client_secret_post
Authority: https://auth.example.com.
Create an OpenID Connect client with Client authentication on. Keycloak has no groups
scope: add a Group Membership mapper (token claim name groups, Full group path off) to
the client's dedicated scope, and set OIDC_<NAME>_SCOPES=openid profile email. Authority:
https://keycloak.example.com/realms/<realm>.
Register an app (platform Web, the redirect URI) and give it a client secret. Entra has no
groups scope and refuses a sign-in that asks for one, so set
OIDC_<NAME>_SCOPES=openid profile email whenever you use any group setting. Groups come from
the app's Token configuration > Add groups claim (security groups) instead. The claim holds
group object ids, not names: use the ids in REQUIRED_GROUPS, ROLE_MAP and GROUP_MAP.
preferred_username is the person's sign-in name (kim@contoso.com), so usernames look like
email addresses. Authority: https://login.microsoftonline.com/<tenant-id>/v2.0.
Create an OAuth client (web application) with the redirect URI. Google has no groups, so leave
the group settings out (with one set, Punchlist would ask for a groups scope Google refuses).
Approve people by hand, or turn on AUTO_CREATE with care: any Google account could then sign
in. Google sends no preferred_username, so usernames come from the email address. Authority:
https://accounts.google.com.
Troubleshooting¶
- "redirect_uri mismatch" at the provider:
PUBLIC_URLand the registered URI differ (scheme, host, the provider name in the path, a trailing slash). - Signed in, then "not allowed": the person is in none of
REQUIRED_GROUPS, or the provider sends nogroupsclaim (see the provider notes). - Waiting for approval:
AUTO_CREATEis off; approve them under Admin > Sign-in. - The event log records each provider sign-in and why one was refused.