@@ -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+ }
0 commit comments