Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BED-4624 fix: missing error handling during sso login #1062

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions cmd/api/src/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ func (s authenticator) CreateSSOSession(request *http.Request, response http.Res

// Generate commit ID for audit logging
if commitID, err = uuid.NewV4(); err != nil {
log.Warnf("Error generating commit ID for login: %s", err)
WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusInternalServerError, "audit log creation failure", request), response)
log.Warnf("[SSO] Error generating commit ID for login: %s", err)
RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
return
}

Expand All @@ -378,14 +378,15 @@ func (s authenticator) CreateSSOSession(request *http.Request, response http.Res
if user, err = s.db.LookupUser(requestCtx, principalNameOrEmail); err != nil {
auditLogFields["error"] = err
if !errors.Is(err, database.ErrNotFound) {
HandleDatabaseError(request, response, err)
log.Warnf("[SSO] Error looking up user: %v", err)
RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
} else {
WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusForbidden, "user is not allowed", request), response)
RedirectToLoginURL(response, request, "Your user is not allowed, please contact your Administrator")
}
} else {
if !user.SSOProviderID.Valid || ssoProvider.ID != user.SSOProviderID.Int32 {
auditLogFields["error"] = ErrUserNotAuthorizedForProvider
WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusForbidden, "user is not allowed", request), response)
RedirectToLoginURL(response, request, "Your user is not allowed, please contact your Administrator")
return
}

Expand All @@ -395,7 +396,8 @@ func (s authenticator) CreateSSOSession(request *http.Request, response http.Res
response.Header().Add(headers.Location.String(), locationURL.String())
response.WriteHeader(http.StatusFound)
} else {
WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusInternalServerError, "session creation failure", request), response)
log.Warnf("[SSO] session creation failure %v", err)
RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
}
} else {
auditLogOutcome = model.AuditLogStatusSuccess
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/api/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (

// UserInterfacePath is the static path to the UI landing page
UserInterfacePath = "/ui"
UserLoginPath = "/ui/login"
UserDisabledPath = "/ui/user-disabled"

// Authorization schemes
Expand Down
18 changes: 18 additions & 0 deletions cmd/api/src/api/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
package api

import (
"net/http"
"net/url"
"strings"

"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/src/ctx"
)

func NewJoinedURL(base string, extensions ...string) (string, error) {
Expand Down Expand Up @@ -49,3 +53,17 @@ func URLJoinPath(target url.URL, extensions ...string) url.URL {

return target
}

func RedirectToLoginURL(response http.ResponseWriter, request *http.Request, errorMessage string) {
hostURL := *ctx.FromRequest(request).Host
redirectURL := URLJoinPath(hostURL, UserLoginPath)

// 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)
}
23 changes: 11 additions & 12 deletions cmd/api/src/api/v2/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/mediatypes"
"github.com/specterops/bloodhound/src/api"
"github.com/specterops/bloodhound/src/api/v2"
"github.com/specterops/bloodhound/src/config"
"github.com/specterops/bloodhound/src/ctx"
"github.com/specterops/bloodhound/src/database"
Expand Down Expand Up @@ -161,16 +160,16 @@ func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, reque

if ssoProvider.OIDCProvider == nil {
// SSO misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
} else if state, err := config.GenerateRandomBase64String(77); err != nil {
log.Warnf("[OIDC] Failed to generate state: %v", err)
// Technical issues scenario
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(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 {
log.Warnf("[OIDC] Failed to create OIDC provider: %v", err)
// SSO misconfiguration or technical issue
// Treat this as a misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
} else {
conf := &oauth2.Config{
ClientID: ssoProvider.OIDCProvider.ClientID,
Expand Down Expand Up @@ -206,37 +205,37 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re

if ssoProvider.OIDCProvider == nil {
// SSO misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
} else if len(code) == 0 {
// Don't want to log state but do want to know if state was present
hasState := queryParams.Has(api.QueryParameterState)
queryParams.Del(api.QueryParameterState)
log.Warnf("[OIDC] auth code is missing, has state %t %+v", hasState, queryParams)
// Missing authorization code implies a credentials or form issue
// Not explicitly covered, treat as technical issue
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
} else if pkceVerifier, err := request.Cookie(api.AuthPKCECookieName); err != nil {
log.Warnf("[OIDC] pkce cookie is missing")
// Missing PKCE verifier - likely a technical or config issue
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
} else if len(state) == 0 {
log.Warnf("[OIDC] state parameter is missing")
// Missing state parameter - treat as technical issue
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(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] {
log.Warnf("[OIDC] state cookie does not match %v", err)
// Invalid state - treat as technical issue or misconfiguration
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(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 {
log.Warnf("[OIDC] Failed to create OIDC provider: %v", err)
// SSO misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
} else if claims, err := getOIDCClaims(request.Context(), provider, ssoProvider, pkceVerifier, code[0]); err != nil {
log.Warnf("[OIDC] %v", err)
v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
} else if email, err := getEmailFromOIDCClaims(claims); errors.Is(err, ErrEmailMissing) { // Note email claims are not always present so we will check different claim keys for possible email
log.Warnf("[OIDC] Claims did not contain any valid email address")
v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
} else {
if ssoProvider.Config.AutoProvision.Enabled {
if err := jitOIDCUserCreation(request.Context(), ssoProvider, email, claims, s.db); err != nil {
Expand Down
23 changes: 11 additions & 12 deletions cmd/api/src/api/v2/auth/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,11 @@ func (s ManagementResource) ServeSigningCertificate(response http.ResponseWriter
func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) {
if ssoProvider.SAMLProvider == nil {
// SAML misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")

api.RedirectToLoginURL(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 {
log.Warnf("[SAML] Service provider creation failed: %v", err)
// Technical issues scenario
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
} else {
var (
binding = saml.HTTPRedirectBinding
Expand All @@ -400,14 +399,14 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque
log.Warnf("[SAML] Failed creating SAML authentication request: %v", err)
// SAML misconfiguration or technical issue
// Since this likely indicates a configuration problem, we treat it as a misconfiguration scenario
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(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.Warnf("[SAML] Failed to format a redirect for SAML provider %s: %v", serviceProvider.EntityID, err)
// Likely a technical or configuration issue
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
} else {
response.Header().Add(headers.Location.String(), redirectURL.String())
response.WriteHeader(http.StatusFound)
Expand All @@ -421,13 +420,13 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque
if _, err := response.Write([]byte(fmt.Sprintf(authInitiationContentBodyFormat, authReq.Post("")))); err != nil {
log.Warnf("[SAML] Failed to write response with HTTP POST binding: %v", err)
// Technical issues scenario
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
}

default:
log.Warnf("[SAML] Unhandled binding type %s", binding)
// Treating unknown binding as a misconfiguration
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO Connection failed, please contact your Administrator")
}
}
}
Expand All @@ -437,15 +436,15 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque
func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) {
if ssoProvider.SAMLProvider == nil {
// SAML misconfiguration
v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator")
api.RedirectToLoginURL(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 {
log.Warnf("[SAML] Service provider creation failed: %v", err)
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(response, request, "We’re having trouble connecting. Please check your internet and try again.")
} else if err := request.ParseForm(); err != nil {
log.Warnf("[SAML] Failed to parse form POST: %v", err)
// Technical issues or invalid form data
// This is not covered by acceptance criteria directly; treat as technical issue
v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.")
api.RedirectToLoginURL(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
switch {
Expand All @@ -455,11 +454,11 @@ func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, re
log.Warnf("[SAML] Failed to parse ACS response for provider %s: %v", ssoProvider.SAMLProvider.IssuerURI, err)
}
// SAML credentials issue scenario (authentication failed)
v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
api.RedirectToLoginURL(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.Warnf("[SAML] Failed to lookup user for SAML provider %s: %v", ssoProvider.Name, err)
// SAML credentials issue scenario again
v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
api.RedirectToLoginURL(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator")
} else {
if ssoProvider.Config.AutoProvision.Enabled {
if err := jitSAMLUserCreation(request.Context(), ssoProvider, principalName, assertion, s.db); err != nil {
Expand Down
16 changes: 0 additions & 16 deletions cmd/api/src/api/v2/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import (
"time"

"github.com/gorilla/mux"
"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/src/api"
"github.com/specterops/bloodhound/src/ctx"
"github.com/specterops/bloodhound/src/model"
"github.com/specterops/bloodhound/src/utils"
)
Expand All @@ -39,20 +37,6 @@ func ErrBadQueryParameter(request *http.Request, key string, err error) *api.Err
return api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("query parameter \"%s\" is malformed: %v", key, err), request)
}

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)
}

func ParseIntQueryParameter(params url.Values, key string, defaultValue int) (int, error) {
if param := params.Get(key); param != "" {
return strconv.Atoi(param)
Expand Down
Loading