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