From a0c2fc3df4fb23af27262815728073d656aeb8e7 Mon Sep 17 00:00:00 2001 From: Justin Wu Date: Sat, 7 Dec 2024 21:43:40 -0800 Subject: [PATCH] feat: added error redirection logic to ui for failed saml and sso logons --- cmd/api/src/api/v2/auth/oidc.go | 49 ++++++++++++++++++++++++--------- cmd/api/src/api/v2/auth/saml.go | 39 +++++++++++++++++--------- cmd/api/src/api/v2/auth/sso.go | 28 +++++++++---------- 3 files changed, 76 insertions(+), 40 deletions(-) diff --git a/cmd/api/src/api/v2/auth/oidc.go b/cmd/api/src/api/v2/auth/oidc.go index 76bd695ccd..c9c137ade3 100644 --- a/cmd/api/src/api/v2/auth/oidc.go +++ b/cmd/api/src/api/v2/auth/oidc.go @@ -18,6 +18,7 @@ package auth import ( "fmt" + "github.com/specterops/bloodhound/log" "net/http" "time" @@ -96,11 +97,17 @@ func getRedirectURL(request *http.Request, provider model.SSOProvider) string { func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { if ssoProvider.OIDCProvider == nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) + // SSO misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if state, err := config.GenerateRandomBase64String(77); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + log.Errorf("[OIDC] Failed to generate state: %v", err) + // Technical issues scenario + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if provider, err := oidc.NewProvider(request.Context(), ssoProvider.OIDCProvider.Issuer); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) + log.Errorf("[OIDC] Failed to create OIDC provider: %v", err) + // SSO misconfiguration or technical issue + // Treat this as a misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { conf := &oauth2.Config{ ClientID: ssoProvider.OIDCProvider.ClientID, @@ -135,17 +142,25 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re ) if ssoProvider.OIDCProvider == nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) + // SSO misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if len(code) == 0 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "missing code", request), response) + // Missing authorization code implies a credentials or form issue + // Not explicitly covered, treat as technical issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if pkceVerifier, err := request.Cookie(api.AuthPKCECookieName); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "missing pkce verifier", request), response) + // Missing PKCE verifier - likely a technical or config issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if len(state) == 0 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "missing state", request), response) + // Missing state parameter - treat as technical issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if stateCookie, err := request.Cookie(api.AuthStateCookieName); err != nil || stateCookie.Value != state[0] { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "bad state", request), response) + // Invalid state - treat as technical issue or misconfiguration + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if provider, err := oidc.NewProvider(request.Context(), ssoProvider.OIDCProvider.Issuer); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) + log.Errorf("[OIDC] Failed to create OIDC provider: %v", err) + // SSO misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { var ( oidcVerifier = provider.Verifier(&oidc.Config{ClientID: ssoProvider.OIDCProvider.ClientID}) @@ -157,11 +172,16 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re ) if token, err := oauth2Conf.Exchange(request.Context(), code[0], oauth2.VerifierOption(pkceVerifier.Value)); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + log.Errorf("[OIDC] Token exchange failed: %v", err) + // SAML credentials issue equivalent for OIDC authentication + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else if rawIDToken, ok := token.Extra("id_token").(string); !ok { // Extract the ID Token from OAuth2 token - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "missing id token", request), response) + // Missing ID token - credentials issue + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else if idToken, err := oidcVerifier.Verify(request.Context(), rawIDToken); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "invalid id token", request), response) + log.Errorf("[OIDC] ID token verification failed: %v", err) + // Credentials issue scenario + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { // Extract custom claims var claims struct { @@ -172,7 +192,10 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re Verified bool `json:"email_verified"` } if err := idToken.Claims(&claims); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) + log.Errorf("[OIDC] Failed to parse claims: %v", err) + // Technical or credentials issue + // Not explicitly covered; treat as a technical issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else { s.authenticator.CreateSSOSession(request, response, claims.Email, ssoProvider) } diff --git a/cmd/api/src/api/v2/auth/saml.go b/cmd/api/src/api/v2/auth/saml.go index 2afcaee546..d04738e413 100644 --- a/cmd/api/src/api/v2/auth/saml.go +++ b/cmd/api/src/api/v2/auth/saml.go @@ -302,13 +302,13 @@ func (s ManagementResource) ServeSigningCertificate(response http.ResponseWriter // HandleStartAuthFlow is called to start the SAML authentication process. func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { if ssoProvider.SAMLProvider == nil { - //api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) - //response.Header().Add(headers.Location.String(), "http://bhe.localhost/ui/login") - //response.WriteHeader(http.StatusFound) - redirectToLoginPage(response, request, "SSO Provider not found") + // SAML misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if serviceProvider, err := auth.NewServiceProvider(*ctx.Get(request.Context()).Host, s.config, *ssoProvider.SAMLProvider); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) + log.Errorf("[SAML] Service provider creation failed: %v", err) + // Technical issues scenario + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else { var ( binding = saml.HTTPRedirectBinding @@ -322,13 +322,16 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque // TODO: add actual relay state support - BED-5071 if authReq, err := serviceProvider.MakeAuthenticationRequest(bindingLocation, binding, saml.HTTPPostBinding); err != nil { log.Errorf("[SAML] Failed creating SAML authentication request: %v", err) - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + // SAML misconfiguration or technical issue + // Since this likely indicates a configuration problem, we treat it as a misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { switch binding { case saml.HTTPRedirectBinding: if redirectURL, err := authReq.Redirect("", &serviceProvider); err != nil { log.Errorf("[SAML] Failed to format a redirect for SAML provider %s: %v", serviceProvider.EntityID, err) - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + // Likely a technical or configuration issue + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { response.Header().Add(headers.Location.String(), redirectURL.String()) response.WriteHeader(http.StatusFound) @@ -341,11 +344,14 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque if _, err := response.Write([]byte(fmt.Sprintf(authInitiationContentBodyFormat, authReq.Post("")))); err != nil { log.Errorf("[SAML] Failed to write response with HTTP POST binding: %v", err) + // Technical issues scenario + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } default: log.Errorf("[SAML] Unhandled binding type %s", binding) - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + // Treating unknown binding as a misconfiguration + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } } } @@ -354,13 +360,18 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque // HandleStartAuthFlow is called to start the SAML authentication process. func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { if ssoProvider.SAMLProvider == nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) + // SAML misconfiguration scenario + redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if serviceProvider, err := auth.NewServiceProvider(*ctx.Get(request.Context()).Host, s.config, *ssoProvider.SAMLProvider); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) + log.Errorf("[SAML] Service provider creation failed: %v", err) + // Technical issues scenario + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else { if err := request.ParseForm(); err != nil { log.Errorf("[SAML] Failed to parse form POST: %v", err) - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "form POST is malformed", request), response) + // Technical issues or invalid form data + // This is not covered by acceptance criteria directly; treat as technical issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else { if assertion, err := serviceProvider.ParseResponse(request, nil); err != nil { var typedErr *saml.InvalidResponseError @@ -370,10 +381,12 @@ func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, re default: log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v", ssoProvider.SAMLProvider.IssuerURI, err) } - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusUnauthorized, api.ErrorResponseDetailsAuthenticationInvalid, request), response) + // SAML credentials issue scenario (authentication failed) + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else if principalName, err := ssoProvider.SAMLProvider.GetSAMLUserPrincipalNameFromAssertion(assertion); err != nil { log.Errorf("[SAML] Failed to lookup user for SAML provider %s: %v", ssoProvider.Name, err) - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "session assertion does not meet the requirements for user lookup", request), response) + // SAML credentials issue scenario again + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { s.authenticator.CreateSSOSession(request, response, principalName, ssoProvider) } diff --git a/cmd/api/src/api/v2/auth/sso.go b/cmd/api/src/api/v2/auth/sso.go index eda3d726e0..f313ffb983 100644 --- a/cmd/api/src/api/v2/auth/sso.go +++ b/cmd/api/src/api/v2/auth/sso.go @@ -196,20 +196,6 @@ func (s ManagementResource) UpdateSSOProvider(response http.ResponseWriter, requ } } -func redirectToLoginPage(response http.ResponseWriter, request *http.Request, errorMessage string) { - hostURL := *ctx.FromRequest(request).Host - redirectURL := api.URLJoinPath(hostURL, "ui/login") - - // Optionally, include the error message as a query parameter or in session storage - query := redirectURL.Query() - query.Set("error", errorMessage) - redirectURL.RawQuery = query.Encode() - - // Redirect to the login page - response.Header().Add(headers.Location.String(), redirectURL.String()) - response.WriteHeader(http.StatusFound) -} - func (s ManagementResource) SSOLoginHandler(response http.ResponseWriter, request *http.Request) { ssoProviderSlug := mux.Vars(request)[api.URIPathVariableSSOProviderSlug] @@ -243,3 +229,17 @@ func (s ManagementResource) SSOCallbackHandler(response http.ResponseWriter, req } } } + +func redirectToLoginPage(response http.ResponseWriter, request *http.Request, errorMessage string) { + hostURL := *ctx.FromRequest(request).Host + redirectURL := api.URLJoinPath(hostURL, api.UserInterfacePath) + + // Optionally, include the error message as a query parameter or in session storage + query := redirectURL.Query() + query.Set("error", errorMessage) + redirectURL.RawQuery = query.Encode() + + // Redirect to the login page + response.Header().Add(headers.Location.String(), redirectURL.String()) + response.WriteHeader(http.StatusFound) +}