Add support for SSO / OpenID Connect (OIDC) (#996)

* ent re-generation

* add oidc integration

* document oidc integration

* go fmt

* address backend linter findings

* run prettier on index.vue

* State cookie domain can mismatch when Hostname override is used (breaks CSRF check). Add SameSite.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Delete state cookie with matching domain and MaxAge; add SameSite.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix endpoint path in comments and error to include /api/v1.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Also use request context when verifying the ID token.

* Do not return raw auth errors to clients (user-enumeration risk).

* consistently set cookie the same way across function

* remove baseURL after declaration

* only enable OIDC routes if OIDC is enabled

* swagger doc for failure

* Only block when provider=local; move the check after parsing provider

* fix extended session comment

* reduce pii logging

* futher reduce pii logging

* remove unused DiscoveryDocument

* remove unused offline_access from default oidc scopes

* remove offline access from AuthCodeURL

* support host from X-Forwarded-Host

* set sane default claim names if unset

* error strings should not be capitalized

* Revert "run prettier on index.vue"

This reverts commit aa22330a23.

* Add timeout to provider discovery

* Split scopes robustly

* refactor hostname calculation

* address frontend prettier findings

* add property oidc on type APISummary

* LoginOIDC: Normalize inputs, only create if not found

* add oidc email verification

* oidc handleCallback: clear state cookie before each return

* add support for oidc nonce parameter

* Harden first-login race: handle concurrent creates gracefully and fix log key.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* support email verified claim as bool or string

* fail fast on empty email

* PKCE verifier

* fix: add timing delay to attachment test to resolve CI race condition

The attachment test was failing intermittently in CI due to a race condition
between attachment creation and retrieval. Adding a small 100ms delay after
attachment creation ensures the file system and database operations complete
before the test attempts to verify the attachment exists.

* Revert "fix: add timing delay to attachment test to resolve CI race condition"

This reverts commit 4aa8b2a0d829753e8d2dd1ba76f4b1e04e28c45e.

* oidc error state, use ref

* rename oidc.force to oidc.authRedirect

* remove hardcoded oidc error timeout

* feat: sub/iss based identity matching and userinfo endpoint collection

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
This commit is contained in:
Jeff Rescignano
2025-12-06 10:16:05 -05:00
committed by GitHub
parent bfc5ffa76b
commit f36756d98e
89 changed files with 9589 additions and 7863 deletions

View File

@@ -2,6 +2,7 @@ package v1
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
@@ -106,6 +107,11 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
provider = "local"
}
// Block local only when disabled
if provider == "local" && !ctrl.config.Options.AllowLocalLogin {
return validate.NewRequestError(fmt.Errorf("local login is not enabled"), http.StatusForbidden)
}
// Get the provider
p, ok := providers[provider]
if !ok {
@@ -114,8 +120,8 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
newToken, err := p.Authenticate(w, r)
if err != nil {
log.Err(err).Msg("failed to authenticate")
return server.JSON(w, http.StatusInternalServerError, err.Error())
log.Warn().Err(err).Msg("authentication failed")
return validate.NewUnauthorizedError()
}
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
@@ -247,3 +253,65 @@ func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
Path: "/",
})
}
// HandleOIDCLogin godoc
//
// @Summary OIDC Login Initiation
// @Tags Authentication
// @Produce json
// @Success 302
// @Router /v1/users/login/oidc [GET]
func (ctrl *V1Controller) HandleOIDCLogin() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// Forbidden if OIDC is not enabled
if !ctrl.config.OIDC.Enabled {
return validate.NewRequestError(fmt.Errorf("OIDC is not enabled"), http.StatusForbidden)
}
// Check if OIDC provider is available
if ctrl.oidcProvider == nil {
log.Error().Msg("OIDC provider not initialized")
return validate.NewRequestError(errors.New("OIDC provider not available"), http.StatusInternalServerError)
}
// Initiate OIDC flow
_, err := ctrl.oidcProvider.InitiateOIDCFlow(w, r)
return err
}
}
// HandleOIDCCallback godoc
//
// @Summary OIDC Callback Handler
// @Tags Authentication
// @Param code query string true "Authorization code"
// @Param state query string true "State parameter"
// @Success 302
// @Router /v1/users/login/oidc/callback [GET]
func (ctrl *V1Controller) HandleOIDCCallback() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// Forbidden if OIDC is not enabled
if !ctrl.config.OIDC.Enabled {
return validate.NewRequestError(fmt.Errorf("OIDC is not enabled"), http.StatusForbidden)
}
// Check if OIDC provider is available
if ctrl.oidcProvider == nil {
log.Error().Msg("OIDC provider not initialized")
return validate.NewRequestError(errors.New("OIDC provider not available"), http.StatusInternalServerError)
}
// Handle callback
newToken, err := ctrl.oidcProvider.HandleCallback(w, r)
if err != nil {
log.Err(err).Msg("OIDC callback failed")
http.Redirect(w, r, "/?oidc_error=oidc_auth_failed", http.StatusFound)
return nil
}
// Set cookies and redirect to home
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
http.Redirect(w, r, "/home", http.StatusFound)
return nil
}
}