Skip to content

Commit 951d332

Browse files
authored
fix: Display error when a user connected with OIDC token from another… (#4646)
### Improve OIDC error message when user has an active session for different account When users try to login to one Twake instance while having an active OIDC session for a different instance, they now see a helpful message, "To connect to X, please disconnect first from Y" instead of the generic "The authentication has failed". This helps users understand why login failed and what action to take. <img width="785" height="178" alt="image" src="https://github.com/user-attachments/assets/a633bfc8-d17a-4c9b-b294-bf4a9fbe9945" />
2 parents b292bdf + ef68298 commit 951d332

File tree

8 files changed

+10618
-10520
lines changed

8 files changed

+10618
-10520
lines changed

assets/locales/en.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,12 @@ msgstr "The authentication has failed"
482482
msgid "the FranceConnect authentication has failed"
483483
msgstr "The FranceConnect authentication has failed"
484484

485+
msgid "OIDC Domain Mismatch %s %s"
486+
msgstr "To connect to %s, please disconnect first from %s"
487+
488+
msgid "Disconnect"
489+
msgstr "Disconnect"
490+
485491
msgid "Instance Blocked Login"
486492
msgstr "The Twake was blocked because of too many login attempts"
487493

assets/locales/fr.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,12 @@ msgstr "L'authentification n'a pu aboutir"
550550
msgid "the FranceConnect authentication has failed"
551551
msgstr "Le compte FranceConnect utilisé ne correspond pas à votre compte Twake."
552552

553+
msgid "OIDC Domain Mismatch %s %s"
554+
msgstr "Pour vous connecter à %s, veuillez d'abord vous déconnecter de %s"
555+
556+
msgid "Disconnect"
557+
msgstr "Déconnexion"
558+
553559
msgid "Instance Blocked Login"
554560
msgstr "Le Twake a été bloqué à cause de trop nombreux essais de connexion"
555561

assets/locales/ru.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,12 @@ msgstr "Аутентификация не удалась"
533533
msgid "the FranceConnect authentication has failed"
534534
msgstr "Используемый аккаунт FranceConnect не соответствует вашему аккаунту Twake."
535535

536+
msgid "OIDC Domain Mismatch %s %s"
537+
msgstr "Чтобы подключиться к %s, сначала отключитесь от %s"
538+
539+
msgid "Disconnect"
540+
msgstr "Отключиться"
541+
536542
msgid "Instance Blocked Login"
537543
msgstr "Twake был заблокирован из-за слишком многих попыток входа"
538544

assets/locales/vi.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,12 @@ msgstr "Xác thực không thành công"
484484
msgid "the FranceConnect authentication has failed"
485485
msgstr "Xác thực bằng FranceConnect không thành công"
486486

487+
msgid "OIDC Domain Mismatch %s %s"
488+
msgstr "Để kết nối với %s, vui lòng đăng xuất khỏi %s trước"
489+
490+
msgid "Disconnect"
491+
msgstr "Ngắt kết nối"
492+
487493
msgid "Instance Blocked Login"
488494
msgstr "Twake đã bị khóa do có quá nhiều lần đăng nhập không thành công"
489495

assets/templates/error.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828
{{if .Illustration}}<img src="{{asset .Domain .Illustration}}" alt="" class="illustration mb-3" />{{end}}
2929
<h1 class="h4 h2-md mb-3 text-center">{{if .ErrorTitle}}{{t .ErrorTitle}}{{else}}{{t "Error Title"}}{{end}}</h1>
3030
<div class="mb-2 mb-md-3">
31-
{{$err := t .Error}}
31+
{{$err := ""}}
32+
{{if .ErrorArgs}}
33+
{{$err = tArgs .Error .ErrorArgs}}
34+
{{else}}
35+
{{$err = t .Error}}
36+
{{end}}
3237
{{$arr := split $err "\n"}}
3338
{{range $i, $p := $arr}}
3439
{{if ne $p ""}}<p class="text-center mb-2">{{$p}}</p>{{end}}

web/oidc/oidc.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ var (
4343
ErrIdentityProvider = errors.New("error from the identity provider")
4444
)
4545

46+
// DomainMismatchError is returned when the user tries to connect to an
47+
// instance but has an active OIDC session for a different instance.
48+
type DomainMismatchError struct {
49+
ExpectedDomain string // The instance the user is trying to access
50+
ActualDomain string // The instance from the OIDC token
51+
}
52+
53+
// TranslationKey returns the i18n key for translating this error.
54+
func (e *DomainMismatchError) TranslationKey() string {
55+
return "OIDC Domain Mismatch %s %s"
56+
}
57+
58+
// TranslationArgs returns the arguments for the translation.
59+
func (e *DomainMismatchError) TranslationArgs() []interface{} {
60+
return []interface{}{e.ExpectedDomain, e.ActualDomain}
61+
}
62+
63+
func (e *DomainMismatchError) Error() string {
64+
return fmt.Sprintf("OIDC Domain Mismatch %s %s", e.ExpectedDomain, e.ActualDomain)
65+
}
66+
4667
// extractSessionID extracts the session ID (sid) from an id_token.
4768
func extractSessionID(idToken string) string {
4869
if idToken == "" {
@@ -367,7 +388,18 @@ func Login(c echo.Context) error {
367388
}
368389

369390
if err := checkDomainFromUserInfo(conf, inst, token); err != nil {
370-
return renderError(c, inst, http.StatusBadRequest, err.Error())
391+
extras := map[string]interface{}{}
392+
errMsg := err.Error()
393+
var dmErr *DomainMismatchError
394+
if errors.As(err, &dmErr) {
395+
extras["ErrorArgs"] = dmErr.TranslationArgs()
396+
errMsg = dmErr.TranslationKey()
397+
if logoutURL := getOIDCLogoutURL(inst.ContextName); logoutURL != "" {
398+
extras["Button"] = "Disconnect"
399+
extras["ButtonURL"] = logoutURL
400+
}
401+
}
402+
return renderError(c, inst, http.StatusBadRequest, errMsg, extras)
371403
}
372404
}
373405

@@ -443,7 +475,17 @@ func TwoFactor(c echo.Context) error {
443475
return renderError(c, inst, http.StatusBadRequest, "No OpenID Connect is configured.")
444476
}
445477
if err := checkDomainFromUserInfo(conf, inst, accessToken); err != nil {
446-
return renderError(c, inst, http.StatusBadRequest, err.Error())
478+
extras := map[string]interface{}{}
479+
errMsg := err.Error()
480+
if dmErr, ok := err.(*DomainMismatchError); ok {
481+
extras["ErrorArgs"] = dmErr.TranslationArgs()
482+
errMsg = dmErr.TranslationKey()
483+
if logoutURL := getOIDCLogoutURL(inst.ContextName); logoutURL != "" {
484+
extras["Button"] = "Disconnect"
485+
extras["ButtonURL"] = logoutURL
486+
}
487+
}
488+
return renderError(c, inst, http.StatusBadRequest, errMsg, extras)
447489
}
448490

449491
if inst.ValidateTwoFactorTrustedDeviceSecret(c.Request(), trustedDeviceToken) {
@@ -1054,7 +1096,7 @@ func checkDomainFromUserInfo(conf *Config, inst *instance.Instance, token string
10541096
}
10551097
if domain != inst.Domain {
10561098
logger.WithNamespace("oidc").Errorf("Invalid domains: %s != %s", domain, inst.Domain)
1057-
return ErrAuthenticationFailed
1099+
return &DomainMismatchError{ExpectedDomain: inst.Domain, ActualDomain: domain}
10581100
}
10591101
return nil
10601102
}
@@ -1259,15 +1301,15 @@ func loadKey(raw *jwKey) (interface{}, error) {
12591301
return &key, nil
12601302
}
12611303

1262-
func renderError(c echo.Context, inst *instance.Instance, code int, msg string) error {
1304+
func renderError(c echo.Context, inst *instance.Instance, code int, msg string, extras ...map[string]interface{}) error {
12631305
if inst == nil {
12641306
inst = &instance.Instance{
12651307
Domain: c.Request().Host,
12661308
ContextName: config.DefaultInstanceContext,
12671309
Locale: consts.DefaultLocale,
12681310
}
12691311
}
1270-
return c.Render(code, "error.html", echo.Map{
1312+
params := echo.Map{
12711313
"Domain": inst.ContextualDomain(),
12721314
"ContextName": inst.ContextName,
12731315
"Locale": inst.Locale,
@@ -1276,7 +1318,22 @@ func renderError(c echo.Context, inst *instance.Instance, code int, msg string)
12761318
"Illustration": "/images/generic-error.svg",
12771319
"Error": msg,
12781320
"SupportEmail": inst.SupportEmailAddress(),
1279-
})
1321+
}
1322+
// Merge any extra params (Button, ButtonURL, etc.)
1323+
for _, extra := range extras {
1324+
for k, v := range extra {
1325+
params[k] = v
1326+
}
1327+
}
1328+
return c.Render(code, "error.html", params)
1329+
}
1330+
1331+
func getOIDCLogoutURL(contextName string) string {
1332+
a := config.GetConfig().Authentication
1333+
delegated, _ := a[contextName].(map[string]interface{})
1334+
oidc, _ := delegated["oidc"].(map[string]interface{})
1335+
u, _ := oidc["logout_url"].(string)
1336+
return u
12801337
}
12811338

12821339
// Routes setup routing for OpenID Connect routes.

web/statik/handler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func NewDirRenderer(assetsPath string) (AssetRenderer, error) {
106106
middlewares.FuncsMap = template.FuncMap{
107107
"t": fmt.Sprintf,
108108
"tHTML": fmt.Sprintf,
109+
"tArgs": func(key string, args interface{}) string { return fmt.Sprintf(key, args.([]interface{})...) },
109110
"split": strings.Split,
110111
"replace": strings.Replace,
111112
"hasSuffix": strings.HasSuffix,
@@ -132,6 +133,7 @@ func NewRenderer() (AssetRenderer, error) {
132133
middlewares.FuncsMap = template.FuncMap{
133134
"t": fmt.Sprintf,
134135
"tHTML": fmt.Sprintf,
136+
"tArgs": func(key string, args interface{}) string { return fmt.Sprintf(key, args.([]interface{})...) },
135137
"split": strings.Split,
136138
"replace": strings.Replace,
137139
"hasSuffix": strings.HasSuffix,
@@ -171,12 +173,18 @@ func (r *renderer) Render(w io.Writer, name string, data interface{}, c echo.Con
171173
funcMap = template.FuncMap{
172174
"t": i.Translate,
173175
"tHTML": i18n.TranslatorHTML(i.Locale, i.ContextName),
176+
"tArgs": func(key string, args []interface{}) string {
177+
return i18n.Translate(key, i.Locale, i.ContextName, args...)
178+
},
174179
}
175180
} else {
176181
lang := GetLanguageFromHeader(c.Request().Header)
177182
funcMap = template.FuncMap{
178183
"t": i18n.Translator(lang, ""),
179184
"tHTML": i18n.TranslatorHTML(lang, ""),
185+
"tArgs": func(key string, args []interface{}) string {
186+
return i18n.Translate(key, lang, "", args...)
187+
},
180188
}
181189
}
182190
var t *template.Template

0 commit comments

Comments
 (0)