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 PeckList:
- 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 PeckList always uses PKCE.
- Scopes:
openid profile email, plusgroupsif you will use groups. PeckList 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.
- Redirect URI:
- 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: pecklist
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
PeckList 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=pecklist | 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=pecklist-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 pecklist 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 PeckList ends the PeckList 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, PeckList 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. PeckList 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 PeckList 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. PeckList 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 PeckList 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 pecklist pecklist oidc migration-window keycloak 24
# 3. Detach everyone from the old provider (accounts and data stay).
docker exec pecklist pecklist oidc unbind-all pocket
# 4. People sign in with keycloak. Then close the window and remove pocket.
docker exec pecklist pecklist 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: pecklist admin login-link kim.
Provider notes
- Pocket ID
- Authentik
- Authelia
- Keycloak
- Microsoft Entra ID
"Continue with Google" for anyone with a Google account, with no groups. Name the provider
google and only the client id and secret are needed: the authority
(https://accounts.google.com), the button and its label come filled in. A provider with
another name gets the same with OIDC_<NAME>_PRESET=google.
OIDC_PROVIDERS: google
OIDC_GOOGLE_CLIENT_ID: 123-abc.apps.googleusercontent.com
OIDC_GOOGLE_CLIENT_SECRET_FILE: /run/secrets/google_client_secret
OIDC_GOOGLE_AUTO_CREATE: 'true' # or let an admin approve each new person
In the Google Cloud console: set up the
OAuth consent screen (External, with your server's address as an authorized domain), then
Create credentials > OAuth client ID > Web application, with the callback URL above as an
authorized redirect URI. Google has no username claim: a new account takes the part of the
email before the @. Google has no groups, so leave the group settings out (with one set,
PeckList would ask for a groups scope Google refuses). The Android app signs in through the
browser, which Google requires.
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. PeckList 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: pecklist
client_name: PeckList
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 ([email protected]), so usernames look like
email addresses. Authority: https://login.microsoftonline.com/<tenant-id>/v2.0.
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.