Skip to content

Commit dd874ac

Browse files
Add OIDC authentication support
- Add OIDC configuration to config.go with validation - Add OIDC provider integration with state management and token exchange - Add OIDC login/callback/status API endpoints - Update NewUserParams with IsSSOUser flag for SSO users without passwords - Consolidate CreateOIDCUser into CreateUser with IsSSOUser check - Add OIDC login button to webapp login page - Add OIDC tests and documentation
1 parent 40e555e commit dd874ac

53 files changed

Lines changed: 12235 additions & 12 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apiserver/controllers/controllers.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,127 @@ func (a *APIController) ForceToolsSyncHandler(w http.ResponseWriter, r *http.Req
542542
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
543543
}
544544
}
545+
546+
// swagger:route GET /auth/oidc/status oidc OIDCStatus
547+
//
548+
// Returns the OIDC configuration status (enabled/disabled).
549+
// This endpoint is public and does not require authentication.
550+
//
551+
// Responses:
552+
// 200: OIDCStatusResponse
553+
func (a *APIController) OIDCStatusHandler(w http.ResponseWriter, r *http.Request) {
554+
response := struct {
555+
Enabled bool `json:"enabled"`
556+
}{
557+
Enabled: a.auth.IsOIDCEnabled(),
558+
}
559+
560+
w.Header().Set("Content-Type", "application/json")
561+
if err := json.NewEncoder(w).Encode(response); err != nil {
562+
slog.With(slog.Any("error", err)).ErrorContext(r.Context(), "failed to encode OIDC status response")
563+
}
564+
}
565+
566+
// swagger:route GET /auth/oidc/login oidc OIDCLogin
567+
//
568+
// Initiates OIDC login flow by redirecting to the identity provider.
569+
//
570+
// Responses:
571+
// 302: description:Redirect to OIDC provider
572+
// 400: APIErrorResponse
573+
// 501: APIErrorResponse
574+
func (a *APIController) OIDCLoginHandler(w http.ResponseWriter, r *http.Request) {
575+
ctx := r.Context()
576+
577+
if !a.auth.IsOIDCEnabled() {
578+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC authentication is not enabled"))
579+
return
580+
}
581+
582+
authURL, _, err := a.auth.GetOIDCAuthURL()
583+
if err != nil {
584+
handleError(ctx, w, err)
585+
return
586+
}
587+
588+
http.Redirect(w, r, authURL, http.StatusFound)
589+
}
590+
591+
// swagger:route GET /auth/oidc/callback oidc OIDCCallback
592+
//
593+
// Handles the OIDC callback from the identity provider.
594+
//
595+
// Responses:
596+
// 200: JWTResponse
597+
// 400: APIErrorResponse
598+
// 401: APIErrorResponse
599+
func (a *APIController) OIDCCallbackHandler(w http.ResponseWriter, r *http.Request) {
600+
ctx := r.Context()
601+
602+
if !a.auth.IsOIDCEnabled() {
603+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC authentication is not enabled"))
604+
return
605+
}
606+
607+
// Check for error from OIDC provider first (before checking for code/state)
608+
// When the IdP returns an error (e.g., user not assigned), it won't include a code
609+
if errParam := r.URL.Query().Get("error"); errParam != "" {
610+
errDesc := r.URL.Query().Get("error_description")
611+
slog.With(slog.String("error", errParam), slog.String("description", errDesc)).Error("OIDC provider returned error")
612+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC provider error: %s - %s", errParam, errDesc))
613+
return
614+
}
615+
616+
code := r.URL.Query().Get("code")
617+
state := r.URL.Query().Get("state")
618+
619+
if code == "" || state == "" {
620+
handleError(ctx, w, gErrors.NewBadRequestError("missing code or state parameter"))
621+
return
622+
}
623+
624+
ctx, err := a.auth.HandleOIDCCallback(ctx, code, state)
625+
if err != nil {
626+
handleError(ctx, w, err)
627+
return
628+
}
629+
630+
tokenString, err := a.auth.GetJWTToken(ctx)
631+
if err != nil {
632+
handleError(ctx, w, err)
633+
return
634+
}
635+
636+
// Get user info from context for the cookie
637+
userName := auth.Username(ctx)
638+
if userName == "" {
639+
userName = auth.UserID(ctx)
640+
}
641+
642+
// Set cookies for the webapp
643+
// Token cookie - NOT HttpOnly because the webapp JavaScript needs to read it
644+
// to set it in the API client for authenticated requests
645+
http.SetCookie(w, &http.Cookie{
646+
Name: "garm_token",
647+
Value: tokenString,
648+
Path: "/",
649+
HttpOnly: false,
650+
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
651+
SameSite: http.SameSiteLaxMode,
652+
MaxAge: 86400 * 7, // 7 days
653+
})
654+
655+
// User cookie - accessible to JavaScript for display purposes
656+
http.SetCookie(w, &http.Cookie{
657+
Name: "garm_user",
658+
Value: userName,
659+
Path: "/",
660+
HttpOnly: false,
661+
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
662+
SameSite: http.SameSiteLaxMode,
663+
MaxAge: 86400 * 7, // 7 days
664+
})
665+
666+
// Redirect to the webapp
667+
http.Redirect(w, r, "/ui/", http.StatusFound)
668+
}

apiserver/routers/routers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
203203
authRouter.Handle("/{login:login\\/?}", http.HandlerFunc(han.LoginHandler)).Methods("POST", "OPTIONS")
204204
authRouter.Use(initMiddleware.Middleware)
205205

206+
// OIDC authentication routes (no auth middleware - these initiate/complete auth)
207+
oidcRouter := apiSubRouter.PathPrefix("/auth/oidc").Subrouter()
208+
oidcRouter.Handle("/status/", http.HandlerFunc(han.OIDCStatusHandler)).Methods("GET", "OPTIONS")
209+
oidcRouter.Handle("/status", http.HandlerFunc(han.OIDCStatusHandler)).Methods("GET", "OPTIONS")
210+
oidcRouter.Handle("/login/", http.HandlerFunc(han.OIDCLoginHandler)).Methods("GET", "OPTIONS")
211+
oidcRouter.Handle("/login", http.HandlerFunc(han.OIDCLoginHandler)).Methods("GET", "OPTIONS")
212+
oidcRouter.Handle("/callback/", http.HandlerFunc(han.OIDCCallbackHandler)).Methods("GET", "OPTIONS")
213+
oidcRouter.Handle("/callback", http.HandlerFunc(han.OIDCCallbackHandler)).Methods("GET", "OPTIONS")
214+
206215
//////////////////////////
207216
// Controller endpoints //
208217
//////////////////////////

0 commit comments

Comments
 (0)