Skip to content

Commit 78c6a90

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 2f3e558 commit 78c6a90

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
@@ -585,3 +585,127 @@ func (a *APIController) ForceToolsSyncHandler(w http.ResponseWriter, r *http.Req
585585
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
586586
}
587587
}
588+
589+
// swagger:route GET /auth/oidc/status oidc OIDCStatus
590+
//
591+
// Returns the OIDC configuration status (enabled/disabled).
592+
// This endpoint is public and does not require authentication.
593+
//
594+
// Responses:
595+
// 200: OIDCStatusResponse
596+
func (a *APIController) OIDCStatusHandler(w http.ResponseWriter, r *http.Request) {
597+
response := struct {
598+
Enabled bool `json:"enabled"`
599+
}{
600+
Enabled: a.auth.IsOIDCEnabled(),
601+
}
602+
603+
w.Header().Set("Content-Type", "application/json")
604+
if err := json.NewEncoder(w).Encode(response); err != nil {
605+
slog.With(slog.Any("error", err)).ErrorContext(r.Context(), "failed to encode OIDC status response")
606+
}
607+
}
608+
609+
// swagger:route GET /auth/oidc/login oidc OIDCLogin
610+
//
611+
// Initiates OIDC login flow by redirecting to the identity provider.
612+
//
613+
// Responses:
614+
// 302: description:Redirect to OIDC provider
615+
// 400: APIErrorResponse
616+
// 501: APIErrorResponse
617+
func (a *APIController) OIDCLoginHandler(w http.ResponseWriter, r *http.Request) {
618+
ctx := r.Context()
619+
620+
if !a.auth.IsOIDCEnabled() {
621+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC authentication is not enabled"))
622+
return
623+
}
624+
625+
authURL, _, err := a.auth.GetOIDCAuthURL()
626+
if err != nil {
627+
handleError(ctx, w, err)
628+
return
629+
}
630+
631+
http.Redirect(w, r, authURL, http.StatusFound)
632+
}
633+
634+
// swagger:route GET /auth/oidc/callback oidc OIDCCallback
635+
//
636+
// Handles the OIDC callback from the identity provider.
637+
//
638+
// Responses:
639+
// 200: JWTResponse
640+
// 400: APIErrorResponse
641+
// 401: APIErrorResponse
642+
func (a *APIController) OIDCCallbackHandler(w http.ResponseWriter, r *http.Request) {
643+
ctx := r.Context()
644+
645+
if !a.auth.IsOIDCEnabled() {
646+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC authentication is not enabled"))
647+
return
648+
}
649+
650+
// Check for error from OIDC provider first (before checking for code/state)
651+
// When the IdP returns an error (e.g., user not assigned), it won't include a code
652+
if errParam := r.URL.Query().Get("error"); errParam != "" {
653+
errDesc := r.URL.Query().Get("error_description")
654+
slog.With(slog.String("error", errParam), slog.String("description", errDesc)).Error("OIDC provider returned error")
655+
handleError(ctx, w, gErrors.NewBadRequestError("OIDC provider error: %s - %s", errParam, errDesc))
656+
return
657+
}
658+
659+
code := r.URL.Query().Get("code")
660+
state := r.URL.Query().Get("state")
661+
662+
if code == "" || state == "" {
663+
handleError(ctx, w, gErrors.NewBadRequestError("missing code or state parameter"))
664+
return
665+
}
666+
667+
ctx, err := a.auth.HandleOIDCCallback(ctx, code, state)
668+
if err != nil {
669+
handleError(ctx, w, err)
670+
return
671+
}
672+
673+
tokenString, err := a.auth.GetJWTToken(ctx)
674+
if err != nil {
675+
handleError(ctx, w, err)
676+
return
677+
}
678+
679+
// Get user info from context for the cookie
680+
userName := auth.Username(ctx)
681+
if userName == "" {
682+
userName = auth.UserID(ctx)
683+
}
684+
685+
// Set cookies for the webapp
686+
// Token cookie - NOT HttpOnly because the webapp JavaScript needs to read it
687+
// to set it in the API client for authenticated requests
688+
http.SetCookie(w, &http.Cookie{
689+
Name: "garm_token",
690+
Value: tokenString,
691+
Path: "/",
692+
HttpOnly: false,
693+
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
694+
SameSite: http.SameSiteLaxMode,
695+
MaxAge: 86400 * 7, // 7 days
696+
})
697+
698+
// User cookie - accessible to JavaScript for display purposes
699+
http.SetCookie(w, &http.Cookie{
700+
Name: "garm_user",
701+
Value: userName,
702+
Path: "/",
703+
HttpOnly: false,
704+
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
705+
SameSite: http.SameSiteLaxMode,
706+
MaxAge: 86400 * 7, // 7 days
707+
})
708+
709+
// Redirect to the webapp
710+
http.Redirect(w, r, "/ui/", http.StatusFound)
711+
}

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)