Skip to content

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

  1. In the provider, create a client (also called an application) for Punchlist:
  2. Redirect URI: https://tasks.example.com/auth/oidc/<name>/callback, where <name> is the name you give the provider below (lowercase).
  3. Type: confidential (with a secret) is usual; a public client also works, since Punchlist always uses PKCE.
  4. Scopes: openid profile email, plus groups if you will use groups. Punchlist asks for groups by itself as soon as any group setting (REQUIRED_GROUPS, ROLE_MAP, GROUP_MAP) is set; OIDC_<NAME>_SCOPES overrides the list for providers that have no such scope.
  5. Client authentication: the secret goes in the token request's body (client_secret_post). Most providers accept it by default; Authelia needs it set.
  6. Set PUBLIC_URL to 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
  1. 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_endpoint in its discovery document (Authentik, Keycloak, Pocket ID, Zitadel, Authelia and Entra ID do), and
  • accept <PUBLIC_URL>/login as 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:

  1. A mapping. OIDC_<NAME>_USER_MAP=ben=bendimit,kim.lee=kim sends 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.
  2. A migration window, while one is open (see moving to another provider).
  3. The same username, with OIDC_<NAME>_LINK_BY_USERNAME:
  4. true joins 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.
  5. all joins 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 with USER_MAP instead.
  6. 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.
  7. 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_URL and 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 no groups claim (see the provider notes).
  • Waiting for approval: AUTO_CREATE is off; approve them under Admin > Sign-in.
  • The event log records each provider sign-in and why one was refused.