feat(caddy): send Forgejo's sign-in link straight to Kanidm #290

Sammanfogat
supernaut sammanfogade 1 incheckning från feat/login-straight-to-oidc in i main 2026-07-31 20:38:58 +00:00
Ägare

Answers the operator's question: yes, it is possible — but only in the reverse proxy.

Why not in Forgejo

There is no app.ini setting. forgejo#732 requests exactly this and is unmerged, because an instance may have several auth sources so "the" provider is not well defined in general. Upstream's own suggestion is a reverse-proxy rewrite.

With ENABLE_INTERNAL_SIGNIN = false there is no local form, so /user/login renders a heading and a single button pointing at /user/oauth2/Bitborg%20Auth. Every web login costs a click on a page with nothing to decide.

The real risk was that the OIDC start route would drop redirect_to, so someone clicking "sign in to comment on this issue" would land on the dashboard instead. Forgejo's login page stashes that parameter in a cookie; the question was whether /user/oauth2/<source> does the same.

It does:

GET /user/oauth2/Bitborg%20Auth?redirect_to=%2Fexplore%2Frepos
  → set-cookie: redirect_to=%2Fexplore%2Frepos; Path=/; HttpOnly; Secure

GET /user/oauth2/Bitborg%20Auth          (control)
  → no redirect_to cookie at all

The parameter is genuinely read and persisted, and the control proves it is not an unconditional cookie. Passing {query} through preserves it.

Had this gone the other way I would have recommended against the change rather than shipping a silent regression on every deep link.

Two escape hatches

This redirect sits on the documented break-glass path — your app.ini comment names re-enabling ENABLE_INTERNAL_SIGNIN as the recovery route when OIDC login is broken. A blanket redirect would bounce you into the broken OIDC in exactly that emergency. So:

  1. Templated out when forgejo_enable_internal_signin is true, so re-enabling local sign-in through Ansible removes the redirect with it.
  2. /user/login?local=1 is never redirected, which still works if app.ini was edited by hand on the host without running Ansible — the likely case when OIDC is down and Ansible is not the tool you reach for.

GET only, so nothing POSTing to /user/login is affected.

Verification

--syntax-check clean; ansible-lint roles/caddy/ passes on the production profile.

Dry-run --check --diff --tags caddy: changed=2 — the Caddyfile render and the consequent Caddy restart. Renders as:

@forgejo_login_page {
	method GET
	path /user/login
	not query local=1
}
redir @forgejo_login_page /user/oauth2/Bitborg%20Auth?{query}

Safety net: the role's existing Validate the rendered Caddyfile task runs caddy validate and fails before the config is used, so a matcher-syntax mistake aborts the play instead of taking down all four public hosts. It is a command, so --check skips it and the apply is its first real exercise.

Post-apply checks

  1. https://git.gitborg.se/user/login → lands on Kanidm, not on the button page.
  2. /user/login?redirect_to=%2Fexplore%2Frepos → after signing in, you end up on /explore/repos, not the dashboard. This is the one worth actually doing.
  3. /user/login?local=1 → still shows Forgejo's page. The break-glass hatch.

Independent of bitborg-infra#289, which fixes a different fault: the Kanidm dashboard tile pointed at Forgejo's logged-out root and signed nobody in. That was a dead link; this is a working link with a redundant click.

Answers the operator's question: yes, it is possible — but only in the reverse proxy. ## Why not in Forgejo There is no `app.ini` setting. [forgejo#732](https://codeberg.org/forgejo/forgejo/issues/732) requests exactly this and is unmerged, because an instance may have several auth sources so "the" provider is not well defined in general. Upstream's own suggestion is a reverse-proxy rewrite. With `ENABLE_INTERNAL_SIGNIN = false` there is no local form, so `/user/login` renders a heading and a single button pointing at `/user/oauth2/Bitborg%20Auth`. Every web login costs a click on a page with nothing to decide. ## Deep links survive — verified before building it The real risk was that the OIDC start route would drop `redirect_to`, so someone clicking "sign in to comment on this issue" would land on the dashboard instead. Forgejo's login page stashes that parameter in a cookie; the question was whether `/user/oauth2/<source>` does the same. It does: ``` GET /user/oauth2/Bitborg%20Auth?redirect_to=%2Fexplore%2Frepos → set-cookie: redirect_to=%2Fexplore%2Frepos; Path=/; HttpOnly; Secure GET /user/oauth2/Bitborg%20Auth (control) → no redirect_to cookie at all ``` The parameter is genuinely read and persisted, and the control proves it is not an unconditional cookie. Passing `{query}` through preserves it. Had this gone the other way I would have recommended against the change rather than shipping a silent regression on every deep link. ## Two escape hatches This redirect sits on the documented break-glass path — your `app.ini` comment names re-enabling `ENABLE_INTERNAL_SIGNIN` as the recovery route when OIDC login is broken. A blanket redirect would bounce you into the broken OIDC in exactly that emergency. So: 1. **Templated out** when `forgejo_enable_internal_signin` is true, so re-enabling local sign-in through Ansible removes the redirect with it. 2. **`/user/login?local=1` is never redirected**, which still works if `app.ini` was edited by hand on the host without running Ansible — the likely case when OIDC is down and Ansible is not the tool you reach for. `GET` only, so nothing POSTing to `/user/login` is affected. ## Verification `--syntax-check` clean; `ansible-lint roles/caddy/` passes on the production profile. Dry-run `--check --diff --tags caddy`: `changed=2` — the Caddyfile render and the consequent Caddy restart. Renders as: ```caddyfile @forgejo_login_page { method GET path /user/login not query local=1 } redir @forgejo_login_page /user/oauth2/Bitborg%20Auth?{query} ``` **Safety net:** the role's existing `Validate the rendered Caddyfile` task runs `caddy validate` and fails before the config is used, so a matcher-syntax mistake aborts the play instead of taking down all four public hosts. It is a `command`, so `--check` skips it and the apply is its first real exercise. ## Post-apply checks 1. `https://git.gitborg.se/user/login` → lands on Kanidm, not on the button page. 2. `/user/login?redirect_to=%2Fexplore%2Frepos` → after signing in, you end up on **/explore/repos**, not the dashboard. This is the one worth actually doing. 3. `/user/login?local=1` → still shows Forgejo's page. The break-glass hatch. ## Related Independent of bitborg-infra#289, which fixes a different fault: the Kanidm dashboard tile pointed at Forgejo's logged-out root and signed nobody in. That was a dead link; this is a working link with a redundant click.
supernaut lade till 1 incheckning 2026-07-31 20:14:10 +00:00
feat(caddy): send Forgejo's sign-in link straight to Kanidm
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m31s
3b7e644d19
With ENABLE_INTERNAL_SIGNIN false there is no local login form, so /user/login
renders a heading and a single button linking to /user/oauth2/<source>. Every web
login therefore costs an extra click on a page with nothing to decide.

Forgejo cannot skip that page itself. Upstream issue 732 asks for exactly this and
is unmerged, because an instance may have several auth sources and picking one is
not well defined in general. Upstream's own suggestion is a reverse-proxy rewrite,
which is what this is.

VERIFIED BEFORE BUILDING IT that deep links survive. Forgejo's login page stashes
?redirect_to= in a cookie so you land where you were heading; the concern was that
the OIDC start route would drop it, sending someone who clicked "sign in to comment
on this issue" to the dashboard instead. It does not:

  GET /user/oauth2/Gitborg%20Auth?redirect_to=%2Fexplore%2Frepos
    → set-cookie: redirect_to=%2Fexplore%2Frepos
  GET /user/oauth2/Gitborg%20Auth
    → no redirect_to cookie at all

So the parameter is genuinely read and persisted, and passing {query} through
preserves it.

Two escape hatches, because this redirect sits on the documented break-glass path
and could otherwise block the emergency it is most likely to be needed in:

  1. The whole block is templated out when forgejo_enable_internal_signin is true,
     so re-enabling local sign-in through Ansible removes the redirect with it.
  2. /user/login?local=1 is never redirected, which still works if app.ini was
     edited by hand on the host without running Ansible — the likely case when OIDC
     is broken and Ansible is not the tool reaching for.

GET only, so nothing POSTing to /user/login is affected.

The role's existing "Validate the rendered Caddyfile" task is the safety net for the
matcher syntax: it runs caddy validate and fails before the config is used, so a
mistake aborts the play rather than taking down all four public hosts. That task is a
command, so --check skips it and the apply is its first real exercise.
supernaut sammanfogade incheckning 068c25ddd6 till main 2026-07-31 20:38:58 +00:00
supernaut tog bort grenen feat/login-straight-to-oidc 2026-07-31 20:38:59 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra!290
Ingen beskrivning angiven.