diff --git a/api/types/authentication.go b/api/types/authentication.go index d02a3519288..fbdb0312447 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -55,6 +55,14 @@ var ( ErrPassswordlessLoginBySSOUser = &trace.AccessDeniedError{ Message: "SSO user cannot login using passwordless", } + + // ErrNonExistingRoleAssigned is issued if the user has a role that doesn't exist in + // Teleport. We have a check which prevents directly assigned roles to be deleted by auth, + // but it can be bypassed in some circumstances. E.g. roles generated by a plugin are + // deleted during plugin cleanup. + ErrNonExistingRoleAssigned = &trace.AccessDeniedError{ + Message: "User is assigned one or more roles that no longer exist in Teleport. Ask your cluster admin to verify and remove direct assignments to roles which no longer exist.", + } ) // AuthPreference defines the authentication preferences for a specific diff --git a/lib/auth/userloginstate/generator.go b/lib/auth/userloginstate/generator.go index 400404efb59..de917956797 100644 --- a/lib/auth/userloginstate/generator.go +++ b/lib/auth/userloginstate/generator.go @@ -343,6 +343,9 @@ func (g *Generator) postProcess(ctx context.Context, state *userloginstate.UserL // Make sure all the roles exist. If they don't, error out. for _, role := range state.Spec.Roles { if _, err := g.access.GetRole(ctx, role); err != nil { + if trace.IsNotFound(err) { + return trace.Wrap(types.ErrNonExistingRoleAssigned) + } return trace.Wrap(err) } } diff --git a/lib/client/sso/redirector.go b/lib/client/sso/redirector.go index 40669f725df..f8484ef1e16 100644 --- a/lib/client/sso/redirector.go +++ b/lib/client/sso/redirector.go @@ -69,6 +69,11 @@ const ( // auth connector's callback was encountered. LoginFailedBadCallbackRedirectURL = "/web/msg/error/login/callback" + // LoginFailedBadCallbackMissingRoleRedirectURL is a redirect URL when an auth connector + // callback failed because it couldn't find the user's calculated role (from auth connector + // mapping) in the backend. + LoginFailedBadCallbackMissingRoleRedirectURL = "/web/msg/error/login/callback_missing_role" + // LoginFailedUnauthorizedRedirectURL is a redirect URL for when an SSO authenticates successfully, // but the user has no matching roles in Teleport. LoginFailedUnauthorizedRedirectURL = "/web/msg/error/login/auth" diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 0d78f23ded5..b541c1090a8 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -2312,13 +2312,13 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr req, err := ParseSSORequestParams(r) if err != nil { logger.ErrorContext(r.Context(), "Failed to extract SSO parameters from request", "error", err) - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { logger.ErrorContext(r.Context(), "Failed to parse request remote address", "error", err) - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } response, err := h.cfg.ProxyClient.CreateGithubAuthRequest(r.Context(), types.GithubAuthRequest{ @@ -2331,7 +2331,7 @@ func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httpr }) if err != nil { logger.ErrorContext(r.Context(), "Error creating auth request", "error", err) - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } return response.RedirectURL @@ -2404,10 +2404,10 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr } } if errors.Is(err, auth.ErrGithubNoTeams) { - return client.LoginFailedUnauthorizedRedirectURL + return sso.LoginFailedUnauthorizedRedirectURL } - return client.LoginFailedBadCallbackRedirectURL + return sso.LoginFailedBadCallbackRedirectURL } // if we created web session, set session cookie and redirect to original url @@ -2424,7 +2424,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr if err := SSOSetWebSessionAndRedirectURL(w, r, res, true); err != nil { logger.ErrorContext(r.Context(), "Error setting web session.", "error", err) - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } if dwt := response.Session.GetDeviceWebToken(); dwt != nil { @@ -2443,7 +2443,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr logger.InfoContext(r.Context(), "Callback is redirecting to console login") if len(response.Req.SSHPubKey)+len(response.Req.TLSPubKey) == 0 { logger.ErrorContext(r.Context(), "Not a web or console login request") - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } redirectURL, err := ConstructSSHResponse(AuthParams{ @@ -2459,7 +2459,7 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr }) if err != nil { logger.ErrorContext(r.Context(), "Error constructing ssh response", "error", err) - return client.LoginFailedRedirectURL + return sso.LoginFailedRedirectURL } return redirectURL.String() @@ -3162,6 +3162,11 @@ func (h *Handler) mfaLoginFinishSession(w http.ResponseWriter, r *http.Request, case errors.Is(err, types.ErrPassswordlessLoginBySSOUser): return nil, trace.Wrap(err) + // Return a friendlier error if the user has assigned a role that doesn't exist in the + // backend. + case errors.Is(err, types.ErrNonExistingRoleAssigned): + return nil, trace.Wrap(err) + // Obscure all other errors. case err != nil: // log the actual error. @@ -5051,7 +5056,7 @@ func (h *Handler) WithRedirect(fn redirectHandlerFunc) httprouter.Handle { redirectURL := fn(w, r, p) if !IsValidRedirectURL(redirectURL) { - redirectURL = client.LoginFailedRedirectURL + redirectURL = sso.LoginFailedRedirectURL } http.Redirect(w, r, redirectURL, http.StatusFound) } @@ -5065,7 +5070,7 @@ func (h *Handler) WithMetaRedirect(fn redirectHandlerFunc) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { redirectURL := fn(w, r, p) if !IsValidRedirectURL(redirectURL) { - redirectURL = client.LoginFailedRedirectURL + redirectURL = sso.LoginFailedRedirectURL } err := app.MetaRedirect(w, redirectURL) if err != nil { diff --git a/web/packages/teleport/src/Login/LoginFailed.tsx b/web/packages/teleport/src/Login/LoginFailed.tsx index 3cff3a221fd..50b9dfe758b 100644 --- a/web/packages/teleport/src/Login/LoginFailed.tsx +++ b/web/packages/teleport/src/Login/LoginFailed.tsx @@ -28,6 +28,9 @@ export function LoginFailed() { + + + diff --git a/web/packages/teleport/src/Teleport.tsx b/web/packages/teleport/src/Teleport.tsx index 00c7ae63e28..77bf08bd14f 100644 --- a/web/packages/teleport/src/Teleport.tsx +++ b/web/packages/teleport/src/Teleport.tsx @@ -39,7 +39,7 @@ import { DesktopSessionContainer as DesktopSession } from './DesktopSession'; import { HeadlessRequest } from './HeadlessRequest'; import { Login } from './Login'; import { LoginClose } from './Login/LoginClose'; -import { LoginFailedComponent as LoginFailed } from './Login/LoginFailed'; +import { LoginFailed } from './Login/LoginFailed'; import { LoginSuccess } from './Login/LoginSuccess'; import { LoginTerminalRedirect } from './Login/LoginTerminalRedirect'; import { Main } from './Main'; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index ec781b966eb..a5e8abc1cb1 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -206,6 +206,7 @@ const cfg = { loginErrorLegacy: '/web/msg/error/login_failed', loginError: '/web/msg/error/login', loginErrorCallback: '/web/msg/error/login/callback', + loginErrorCallbackMissingRole: '/web/msg/error/login/callback_missing_role', loginErrorUnauthorized: '/web/msg/error/login/auth', samlSloFailed: '/web/msg/error/slo', userInvite: '/web/invite/:tokenId',