WIP: Collar SSO #9

Draft
kritdass wants to merge 1 commit from feat/oidc-auth into main
Member
No description provided.
docs: plan Collar SSO
Some checks failed
nixbot/nix-eval evaluation succeeded in 0s (1 warning)
nixbot/nix-build 0 attributes built
kennel/build skipped: no services or static sites defined
CI / check (pull_request) Failing after 5m45s
CI / package (pull_request) Successful in 6m18s
3372a32e39
Author
Member

@anish Could you please review this plan and make sure it makes sense?

@anish Could you please review this plan and make sure it makes sense?
docs: compare Forgejo OAuth and Keycloak broker access
Some checks failed
nixbot/nix-eval evaluation succeeded in 0s (1 warning)
nixbot/nix-build 0 attributes built
CI / package (pull_request) Successful in 17s
kennel/build skipped: no services or static sites defined
CI / check (pull_request) Failing after 3m53s
2a4d97e168
Owner

@kritdass wrote in #9 (comment):

@anish Could you please review this plan and make sure it makes sense?

mostly looks good, as for forgejo authentication i had a conversation with claude on the ideal approach:

Here’s a comment you can paste into the PR:

Suggestion: pick the Keycloak broker option for Forgejo

I think we should go with the broker. It costs more infrastructure, but we get these properties:

One sign-in. collar auth login only talks to Keycloak, and Forgejo access comes from it. There’s no Forgejo approval on each new machine. Each user links cmu-dev once, ever.
No long-lived Forgejo credential on laptops. Machines only ever hold short-lived Forgejo access tokens. The Forgejo refresh token stays in Keycloak. With direct OAuth, every keychain holds a Forgejo refresh token, and since Forgejo OAuth tokens have no scopes, a stolen one is a full credential.
One place to revoke. Ending a user’s Keycloak sessions or disabling the account cuts off both Grafana and Forgejo at the next refresh. With direct OAuth, Forgejo grants keep working until someone also revokes them in Forgejo.
One session policy. Keycloak alone decides how long CLI sessions last, for both services.

Requirements:

Keycloak with working V2 refresh. You’re right that 26.8.1’s V2 path returns the stored token without refreshing it, so the broker would stop working about an hour after linking. Upstream has fixed this. We’d run a Keycloak build that includes the fix, and roll out only after testing expired access tokens and rotated refresh tokens.
Strict checks in the relay. Verify the token’s signature and require aud: collar-broker and azp: collar. Use the token’s subject as the user. Allow only cmu-dev. Return only the access token and its expiry. The relay can’t get a Forgejo token without a valid user token, but it can use any user token it receives, so these checks are what keep it safe.
Linking flow. If the relay reports that cmu-dev isn’t linked, collar should send the user to Keycloak’s account-linking page instead of failing. This replaces direct OAuth’s per-machine approval with a single approval per user.
CLI session length. The proposal currently says “normal refresh sessions without offline access or realm timeout changes”. The realm uses Keycloak’s defaults (30 min idle, 10 h max), and a client can’t extend them. Once collar’s Keycloak session expires, the relay can’t fetch Forgejo tokens either, so users would have to sign in again after half an hour of inactivity. I’d request offline_access for collar or raise the realm’s idle timeout.

Keep direct OAuth (git-credential-oauth) as a fallback for users who haven’t linked cmu-dev yet, and for when Keycloak or the relay is down.

Some things stay the same either way. Forgejo tokens remain unscoped, and full offboarding still means disabling the Forgejo account, because web sessions and personal access tokens don’t go through Keycloak.

The comment doesn’t cover the redirect URI bug. http://127.0.0.1/callback should be http://127.0.0.1:*, so it may be worth a separate comment on that line of the doc.
@kritdass wrote in https://git.cmu.dev/ScottyLabs/collar/pulls/9#issuecomment-20111: > @anish Could you please review this plan and make sure it makes sense? mostly looks good, as for forgejo authentication i had a conversation with claude on the ideal approach: ``` Here’s a comment you can paste into the PR: Suggestion: pick the Keycloak broker option for Forgejo I think we should go with the broker. It costs more infrastructure, but we get these properties: One sign-in. collar auth login only talks to Keycloak, and Forgejo access comes from it. There’s no Forgejo approval on each new machine. Each user links cmu-dev once, ever. No long-lived Forgejo credential on laptops. Machines only ever hold short-lived Forgejo access tokens. The Forgejo refresh token stays in Keycloak. With direct OAuth, every keychain holds a Forgejo refresh token, and since Forgejo OAuth tokens have no scopes, a stolen one is a full credential. One place to revoke. Ending a user’s Keycloak sessions or disabling the account cuts off both Grafana and Forgejo at the next refresh. With direct OAuth, Forgejo grants keep working until someone also revokes them in Forgejo. One session policy. Keycloak alone decides how long CLI sessions last, for both services. Requirements: Keycloak with working V2 refresh. You’re right that 26.8.1’s V2 path returns the stored token without refreshing it, so the broker would stop working about an hour after linking. Upstream has fixed this. We’d run a Keycloak build that includes the fix, and roll out only after testing expired access tokens and rotated refresh tokens. Strict checks in the relay. Verify the token’s signature and require aud: collar-broker and azp: collar. Use the token’s subject as the user. Allow only cmu-dev. Return only the access token and its expiry. The relay can’t get a Forgejo token without a valid user token, but it can use any user token it receives, so these checks are what keep it safe. Linking flow. If the relay reports that cmu-dev isn’t linked, collar should send the user to Keycloak’s account-linking page instead of failing. This replaces direct OAuth’s per-machine approval with a single approval per user. CLI session length. The proposal currently says “normal refresh sessions without offline access or realm timeout changes”. The realm uses Keycloak’s defaults (30 min idle, 10 h max), and a client can’t extend them. Once collar’s Keycloak session expires, the relay can’t fetch Forgejo tokens either, so users would have to sign in again after half an hour of inactivity. I’d request offline_access for collar or raise the realm’s idle timeout. Keep direct OAuth (git-credential-oauth) as a fallback for users who haven’t linked cmu-dev yet, and for when Keycloak or the relay is down. Some things stay the same either way. Forgejo tokens remain unscoped, and full offboarding still means disabling the Forgejo account, because web sessions and personal access tokens don’t go through Keycloak. The comment doesn’t cover the redirect URI bug. http://127.0.0.1/callback should be http://127.0.0.1:*, so it may be worth a separate comment on that line of the doc. ```
Author
Member

@anish does this split make sense? Broker as the default, direct OAuth as an explicit fallback:

  • governance: add redirect_uris and audiences overrides to OidcClientFeature (crates/governance-schema/src/team.rs) and its generator (crates/governance-tfgen/src/generators/keycloak.rs), including schema/docs/tests. Register Collar in data/teams/devops.yaml as a public PKCE client with audiences grafana and collar-broker.
  • infrastructure / Keycloak: pin a build with the V2 refresh fix and enable identity-brokering-api:v2 in keycloak/server.nix. In keycloak/providers.nix, provision confidential client collar-broker, allow token retrieval only for cmu-dev, and store its secret in OpenBao. Both files are under modules/hosts/scottylabs/infra-01/.
  • collar / relay: add a separate server binary; deploy it through an infrastructure NixOS module. Validate the user token's signature, issuer, expiry, aud: collar-broker, and azp: collar. Use its subject, with no caller-supplied user or provider. Retrieve from Keycloak's V2 cmu-dev endpoint and return only the Forgejo access token and expiry. Refresh tokens stay in Keycloak.
  • infrastructure / Grafana: enable JWT auth in modules/hosts/scottylabs/infra-01/observability/grafana.nix, checking issuer, audience, and client. Keep browser OAuth and the existing role mapping; verify CLI and browser login resolve to the same account.
  • collar / CLI: update src/auth.rs and src/oauth.rs for Keycloak login/refresh/logout and relay access. Open Keycloak account linking during login if cmu-dev is missing or revoked. Keep direct OAuth opt-in. Add deployment-log queries through Grafana in src/kennel.rs; no Kennel API changes.

Before rollout, test expired access tokens, refresh rotation, revoked links, cross-user denial, and wrong-client/provider requests.

For session length, do you prefer bounded offline_access or longer normal sessions? Offline sessions need separate revocation from browser logout. Issued access tokens can remain valid until expiry; full offboarding still includes disabling Forgejo. Direct OAuth credentials also need separate revocation.

The callback can stay http://127.0.0.1/callback: Keycloak 26.8.1 ignores the port for loopback matching, so http://127.0.0.1:* isn't needed.

@anish does this split make sense? Broker as the default, direct OAuth as an explicit fallback: - **governance:** add `redirect_uris` and `audiences` overrides to `OidcClientFeature` (`crates/governance-schema/src/team.rs`) and its generator (`crates/governance-tfgen/src/generators/keycloak.rs`), including schema/docs/tests. Register Collar in `data/teams/devops.yaml` as a public PKCE client with audiences `grafana` and `collar-broker`. - **infrastructure / Keycloak:** pin a build with the V2 refresh fix and enable `identity-brokering-api:v2` in `keycloak/server.nix`. In `keycloak/providers.nix`, provision confidential client `collar-broker`, allow token retrieval only for `cmu-dev`, and store its secret in OpenBao. Both files are under `modules/hosts/scottylabs/infra-01/`. - **collar / relay:** add a separate server binary; deploy it through an infrastructure NixOS module. Validate the user token's signature, issuer, expiry, `aud: collar-broker`, and `azp: collar`. Use its subject, with no caller-supplied user or provider. Retrieve from Keycloak's V2 `cmu-dev` endpoint and return only the Forgejo access token and expiry. Refresh tokens stay in Keycloak. - **infrastructure / Grafana:** enable JWT auth in `modules/hosts/scottylabs/infra-01/observability/grafana.nix`, checking issuer, audience, and client. Keep browser OAuth and the existing role mapping; verify CLI and browser login resolve to the same account. - **collar / CLI:** update `src/auth.rs` and `src/oauth.rs` for Keycloak login/refresh/logout and relay access. Open Keycloak account linking during login if `cmu-dev` is missing or revoked. Keep direct OAuth opt-in. Add deployment-log queries through Grafana in `src/kennel.rs`; no Kennel API changes. Before rollout, test expired access tokens, refresh rotation, revoked links, cross-user denial, and wrong-client/provider requests. For session length, do you prefer bounded `offline_access` or longer normal sessions? Offline sessions need separate revocation from browser logout. Issued access tokens can remain valid until expiry; full offboarding still includes disabling Forgejo. Direct OAuth credentials also need separate revocation. The callback can stay `http://127.0.0.1/callback`: [Keycloak 26.8.1 ignores the port for loopback matching](https://github.com/keycloak/keycloak/blob/26.8.1/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java), so `http://127.0.0.1:*` isn't needed.
Owner

@kritdass wrote in #9 (comment):

@anish does this split make sense? Broker as the default, direct OAuth as an explicit fallback:

* **governance:** add `redirect_uris` and `audiences` overrides to `OidcClientFeature` (`crates/governance-schema/src/team.rs`) and its generator (`crates/governance-tfgen/src/generators/keycloak.rs`), including schema/docs/tests. Register Collar in `data/teams/devops.yaml` as a public PKCE client with audiences `grafana` and `collar-broker`.

* **infrastructure / Keycloak:** pin a build with the V2 refresh fix and enable `identity-brokering-api:v2` in `keycloak/server.nix`. In `keycloak/providers.nix`, provision confidential client `collar-broker`, allow token retrieval only for `cmu-dev`, and store its secret in OpenBao. Both files are under `modules/hosts/scottylabs/infra-01/`.

* **collar / relay:** add a separate server binary; deploy it through an infrastructure NixOS module. Validate the user token's signature, issuer, expiry, `aud: collar-broker`, and `azp: collar`. Use its subject, with no caller-supplied user or provider. Retrieve from Keycloak's V2 `cmu-dev` endpoint and return only the Forgejo access token and expiry. Refresh tokens stay in Keycloak.

* **infrastructure / Grafana:** enable JWT auth in `modules/hosts/scottylabs/infra-01/observability/grafana.nix`, checking issuer, audience, and client. Keep browser OAuth and the existing role mapping; verify CLI and browser login resolve to the same account.

* **collar / CLI:** update `src/auth.rs` and `src/oauth.rs` for Keycloak login/refresh/logout and relay access. Open Keycloak account linking during login if `cmu-dev` is missing or revoked. Keep direct OAuth opt-in. Add deployment-log queries through Grafana in `src/kennel.rs`; no Kennel API changes.

Before rollout, test expired access tokens, refresh rotation, revoked links, cross-user denial, and wrong-client/provider requests.

For session length, do you prefer bounded offline_access or longer normal sessions? Offline sessions need separate revocation from browser logout. Issued access tokens can remain valid until expiry; full offboarding still includes disabling Forgejo. Direct OAuth credentials also need separate revocation.

The callback can stay http://127.0.0.1/callback: Keycloak 26.8.1 ignores the port for loopback matching, so http://127.0.0.1:* isn't needed.

  • let's just do broker without direct oauth fallback, linking happens during login anyway + forgejo login already goes through keycloak so the fallback wouldn't help if keycloak is down (also it would defeat the goal of trying to keep long lived forgejo tokens off devices)
  • for session length, the realm is set to 24h idle / 7d max already, so normal sessions are fine
  • for the "neither option is picked", kennel has an api for deploy state now (GET /projects/{project}/branches/{branch}) that i modified collar to use, so the logs section should get project/branch/commit from there instead of the kennel/deploy status
  • for v2 refresh, the fix is in https://github.com/keycloak/keycloak/pull/53256, we can use an overlay to include it in our keycloak build until it's in a release
@kritdass wrote in https://git.cmu.dev/ScottyLabs/collar/pulls/9#issuecomment-20377: > @anish does this split make sense? Broker as the default, direct OAuth as an explicit fallback: > > * **governance:** add `redirect_uris` and `audiences` overrides to `OidcClientFeature` (`crates/governance-schema/src/team.rs`) and its generator (`crates/governance-tfgen/src/generators/keycloak.rs`), including schema/docs/tests. Register Collar in `data/teams/devops.yaml` as a public PKCE client with audiences `grafana` and `collar-broker`. > > * **infrastructure / Keycloak:** pin a build with the V2 refresh fix and enable `identity-brokering-api:v2` in `keycloak/server.nix`. In `keycloak/providers.nix`, provision confidential client `collar-broker`, allow token retrieval only for `cmu-dev`, and store its secret in OpenBao. Both files are under `modules/hosts/scottylabs/infra-01/`. > > * **collar / relay:** add a separate server binary; deploy it through an infrastructure NixOS module. Validate the user token's signature, issuer, expiry, `aud: collar-broker`, and `azp: collar`. Use its subject, with no caller-supplied user or provider. Retrieve from Keycloak's V2 `cmu-dev` endpoint and return only the Forgejo access token and expiry. Refresh tokens stay in Keycloak. > > * **infrastructure / Grafana:** enable JWT auth in `modules/hosts/scottylabs/infra-01/observability/grafana.nix`, checking issuer, audience, and client. Keep browser OAuth and the existing role mapping; verify CLI and browser login resolve to the same account. > > * **collar / CLI:** update `src/auth.rs` and `src/oauth.rs` for Keycloak login/refresh/logout and relay access. Open Keycloak account linking during login if `cmu-dev` is missing or revoked. Keep direct OAuth opt-in. Add deployment-log queries through Grafana in `src/kennel.rs`; no Kennel API changes. > > > Before rollout, test expired access tokens, refresh rotation, revoked links, cross-user denial, and wrong-client/provider requests. > > For session length, do you prefer bounded `offline_access` or longer normal sessions? Offline sessions need separate revocation from browser logout. Issued access tokens can remain valid until expiry; full offboarding still includes disabling Forgejo. Direct OAuth credentials also need separate revocation. > > The callback can stay `http://127.0.0.1/callback`: [Keycloak 26.8.1 ignores the port for loopback matching](https://github.com/keycloak/keycloak/blob/26.8.1/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java), so `http://127.0.0.1:*` isn't needed. * let's just do broker without direct oauth fallback, linking happens during login anyway + forgejo login already goes through keycloak so the fallback wouldn't help if keycloak is down (also it would defeat the goal of trying to keep long lived forgejo tokens off devices) * for session length, the realm is set to 24h idle / 7d max already, so normal sessions are fine * for the "neither option is picked", kennel has an api for deploy state now (`GET /projects/{project}/branches/{branch}`) that i modified collar to use, so the logs section should get project/branch/commit from there instead of the kennel/deploy status * for v2 refresh, the fix is in https://github.com/keycloak/keycloak/pull/53256, we can use an overlay to include it in our keycloak build until it's in a release
kritdass force-pushed feat/oidc-auth from 2a4d97e168
Some checks failed
nixbot/nix-eval evaluation succeeded in 0s (1 warning)
nixbot/nix-build 0 attributes built
CI / package (pull_request) Successful in 17s
kennel/build skipped: no services or static sites defined
CI / check (pull_request) Failing after 3m53s
to 9bbe296042
Some checks reported errors
CI / checks-1 (pull_request) Failing after 16s
CI / checks (pull_request) Failing after 0s
nixbot/nix-eval evaluation succeeded in 1m 37s (1 warning)
kennel/deploy deployment healthy
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.clippy cancelled
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.test cancelled
nixbot/nix-build 2 cancelled, 3 succeeded
2026-11-10 21:54:42 +00:00
Compare
Author
Member

@anish Collar's SSO implementation is now on this PR. Could you handle these platform prerequisites before rollout?

  • Governance: update the existing public Collar client for loopback PKCE login, with grafana and collar-broker access-token audiences.
  • Keycloak and infrastructure: enable V2 brokering; provision the confidential collar-broker client for cmu-dev external-token retrieval only; deploy the Rust broker behind HTTPS with its secret server-side; update the infrastructure Collar input. Include the refresh fix from keycloak/keycloak#53256 in the Keycloak build (or use a release containing it).
  • Grafana: enable JWT authentication for Collar while retaining browser OAuth and the existing role mapping; ensure both login methods identify the same user by email.

Once those are deployed, we can verify university sign-in, account linking, expired and rotated tokens, revocation, account matching, and live deployment logs. The CLI uses normal sessions and broker-only Forgejo access, as you requested. Details are in docs/design/oidc.md.

@anish Collar's SSO implementation is now on this PR. Could you handle these platform prerequisites before rollout? - Governance: update the existing public Collar client for loopback PKCE login, with grafana and collar-broker access-token audiences. - Keycloak and infrastructure: enable V2 brokering; provision the confidential collar-broker client for cmu-dev external-token retrieval only; deploy the Rust broker behind HTTPS with its secret server-side; update the infrastructure Collar input. Include the refresh fix from keycloak/keycloak#53256 in the Keycloak build (or use a release containing it). - Grafana: enable JWT authentication for Collar while retaining browser OAuth and the existing role mapping; ensure both login methods identify the same user by email. Once those are deployed, we can verify university sign-in, account linking, expired and rotated tokens, revocation, account matching, and live deployment logs. The CLI uses normal sessions and broker-only Forgejo access, as you requested. Details are in docs/design/oidc.md.
Owner

Kennel Deployments

Service URL
docs https://collar-docs-pr-9.scottylabs.net

Last updated for commit b787257.

<!-- kennel-deployment --> ### Kennel Deployments | Service | URL | | --- | --- | | docs | https://collar-docs-pr-9.scottylabs.net | Last updated for commit `b787257`.
kritdass force-pushed feat/oidc-auth from 9bbe296042
Some checks reported errors
CI / checks-1 (pull_request) Failing after 16s
CI / checks (pull_request) Failing after 0s
nixbot/nix-eval evaluation succeeded in 1m 37s (1 warning)
kennel/deploy deployment healthy
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.clippy cancelled
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.test cancelled
nixbot/nix-build 2 cancelled, 3 succeeded
to c6ca5349b9
Some checks reported errors
nixbot/nix-eval evaluation succeeded in 6s (1 warning)
CI / checks-1 (pull_request) Successful in 17s
CI / checks (pull_request) Successful in 0s
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.test cancelled
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.clippy cancelled
nixbot/nix-build 2 cancelled, 3 succeeded
2026-11-10 21:59:13 +00:00
Compare
kritdass force-pushed feat/oidc-auth from c6ca5349b9
Some checks reported errors
nixbot/nix-eval evaluation succeeded in 6s (1 warning)
CI / checks-1 (pull_request) Successful in 17s
CI / checks (pull_request) Successful in 0s
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.test cancelled
nixbot/nix-build gitea:ScottyLabs/collar#checks.x86_64-linux.clippy cancelled
nixbot/nix-build 2 cancelled, 3 succeeded
to b787257656
All checks were successful
nixbot/nix-eval evaluation succeeded in 7s (1 warning)
CI / checks-1 (pull_request) Successful in 17s
CI / checks (pull_request) Successful in 0s
kennel/deploy deployment healthy
nixbot/nix-build 5 attributes built
2026-11-10 22:00:14 +00:00
Compare
All checks were successful
nixbot/nix-eval evaluation succeeded in 7s (1 warning)
Required
Details
CI / checks-1 (pull_request) Successful in 17s
Required
Details
CI / checks (pull_request) Successful in 0s
Required
Details
kennel/deploy deployment healthy
Required
nixbot/nix-build 5 attributes built
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 'feat/oidc-auth:feat/oidc-auth'
git switch 'feat/oidc-auth'
Sign in to join this conversation.
No description provided.