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',