From b196ec4d8d556462bffafa7d2e32b0ffdca40900 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:18:12 +0200 Subject: [PATCH 01/21] Add passkeys support --- routers/web/auth/webauthn.go | 108 ++++++++++++++++++++++ routers/web/web.go | 2 + web_src/js/features/user-auth-webauthn.js | 2 +- 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 1079f44a085b3..64676254c5e10 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -12,7 +12,9 @@ import ( wa "code.gitea.io/gitea/modules/auth/webauthn" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/externalaccount" @@ -47,6 +49,112 @@ func WebAuthn(ctx *context.Context) { ctx.HTML(http.StatusOK, tplWebAuthn) } +// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser +func WebAuthnLoginAssertion1(ctx *context.Context) { + assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin() + if err != nil { + ctx.ServerError("webauthn.BeginDiscoverableLogin", err) + return + } + + if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil { + ctx.ServerError("Session.Set", err) + return + } + + ctx.JSON(http.StatusOK, assertion) +} + +// SignInPost response for sign in request +func WebAuthnLogin(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("sign_in") + + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) + if err != nil { + ctx.ServerError("UserSignIn", err) + return + } + ctx.Data["OAuth2Providers"] = oauth2Providers + ctx.Data["Title"] = ctx.Tr("sign_in") + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" + ctx.Data["PageIsSignIn"] = true + ctx.Data["PageIsLogin"] = true + ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplSignIn) + return + } + + sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData) + if !okData || sessionData == nil { + ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) + return + } + defer func() { + _ = ctx.Session.Delete("webauthnAssertion") + }() + + // Validate the parsed response. + // func(rawID, userHandle []byte) (user User, err error) + cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) { + user := &wa.User{} + // TODO: get actual user using rawID and userHandle from database + return user, nil + }, *sessionData, ctx.Req) + if err != nil { + // Failed authentication attempt. + log.Info("Failed authentication attempt for passkey from %s: %v", ctx.RemoteAddr(), err) + ctx.Status(http.StatusForbidden) + return + } + + userID := int64(1) + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + ctx.ServerError("UserSignIn", err) + return + } + + // Ensure that the credential wasn't cloned by checking if CloneWarning is set. + // (This is set if the sign counter is less than the one we have stored.) + if cred.Authenticator.CloneWarning { + log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr()) + ctx.Status(http.StatusForbidden) + return + } + + // Success! Get the credential and update the sign count with the new value we received. + dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID) + if err != nil { + ctx.ServerError("GetWebAuthnCredentialByCredID", err) + return + } + + dbCred.SignCount = cred.Authenticator.SignCount + if err := dbCred.UpdateSignCount(ctx); err != nil { + ctx.ServerError("UpdateSignCount", err) + return + } + + // Now handle account linking if that's requested + if ctx.Session.Get("linkAccount") != nil { + if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil { + ctx.ServerError("LinkAccountFromStore", err) + return + } + } + + remember := ctx.Session.Get("twofaRemember").(bool) + redirect := handleSignInFull(ctx, user, remember, false) + if redirect == "" { + redirect = setting.AppSubURL + "/" + } + _ = ctx.Session.Delete("twofaUid") + + ctx.JSONRedirect(redirect) +} + // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser func WebAuthnLoginAssertion(ctx *context.Context) { // Ensure user is in a WebAuthn session. diff --git a/routers/web/web.go b/routers/web/web.go index 9f9a1bb0988e6..db4fee89cb559 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -535,6 +535,8 @@ func registerRoutes(m *web.Router) { }) m.Group("/webauthn", func() { m.Get("", auth.WebAuthn) + m.Get("/passkey/assertion", auth.WebAuthnLoginAssertion1) + m.Get("/passkey/login", auth.WebAuthnLogin) m.Get("/assertion", auth.WebAuthnLoginAssertion) m.Post("/assertion", auth.WebAuthnLoginAssertionPost) }) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index ea26614ba7696..b1ec297f41c46 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -14,7 +14,7 @@ export async function initUserAuthWebAuthn() { return; } - const res = await GET(`${appSubUrl}/user/webauthn/assertion`); + const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`); if (res.status !== 200) { webAuthnError('unknown'); return; From 03f1e5065b26d5774ad04101fd561ca577aa6e37 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:50:28 +0200 Subject: [PATCH 02/21] add login btn --- options/locale/locale_en-US.ini | 1 + templates/user/auth/signin_inner.tmpl | 6 ++++ web_src/js/features/user-auth-webauthn.js | 37 +++++++++++++++-------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 815cba6eecaf1..d10f61f2ffc9e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -458,6 +458,7 @@ sspi_auth_failed = SSPI authentication failed password_pwned = The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too. password_pwned_err = Could not complete request to HaveIBeenPwned last_admin = You cannot remove the last admin. There must be at least one admin. +signin_passkey = Sign in with a passkey [mail] view_it_on = View it on %s diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 9872096fbc6af..67528ebce329f 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -9,6 +9,8 @@ {{end}}
+ {{template "user/auth/webauthn_error" .}} +
{{.CsrfTokenHtml}}
@@ -49,6 +51,10 @@
{{end}} + + {{if .OAuth2Providers}}
{{ctx.Locale.Tr "sign_in_or"}} diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index b1ec297f41c46..bfac70f4a16fd 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -4,26 +4,21 @@ import {GET, POST} from '../modules/fetch.js'; const {appSubUrl} = window.config; -export async function initUserAuthWebAuthn() { - const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); - if (!elPrompt) { - return; - } - - if (!detectWebAuthnSupport()) { - return; - } - - const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`); +async function doWebAuthn(assertionUrl) { + const res = await GET(assertionUrl); if (res.status !== 200) { webAuthnError('unknown'); return; } + const options = await res.json(); options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge); - for (const cred of options.publicKey.allowCredentials) { + for (const cred of options.publicKey.allowCredentials ?? []) { cred.id = decodeURLEncodedBase64(cred.id); } + + console.log('leggo3', options); + try { const credential = await navigator.credentials.get({ publicKey: options.publicKey, @@ -46,6 +41,24 @@ export async function initUserAuthWebAuthn() { } } +export async function initUserAuthWebAuthn() { + console.log('moin initUserAuthWebAuthn'); + + if (!detectWebAuthnSupport()) { + return; + } + + const elSignInPasskeyBtn = document.querySelector('.signin-passkey'); + if (elSignInPasskeyBtn) { + elSignInPasskeyBtn.onclick = () => doWebAuthn(`${appSubUrl}/user/webauthn/passkey/assertion`); + } + + const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); + if (elPrompt) { + doWebAuthn(`${appSubUrl}/user/webauthn/assertion`); + } +} + async function verifyAssertion(assertedCredential) { // Move data into Arrays in case it is super long const authData = new Uint8Array(assertedCredential.response.authenticatorData); From 701e51b550623deb23f80eb4b70a0f6998be9482 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:25:39 +0200 Subject: [PATCH 03/21] finish --- modules/auth/webauthn/webauthn.go | 6 +- routers/web/auth/webauthn.go | 66 +++++++------ routers/web/user/setting/security/webauthn.go | 5 +- routers/web/web.go | 4 +- web_src/js/features/user-auth-webauthn.js | 92 ++++++++++++++----- 5 files changed, 112 insertions(+), 61 deletions(-) diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index 189d197333e00..db4b88892d187 100644 --- a/modules/auth/webauthn/webauthn.go +++ b/modules/auth/webauthn/webauthn.go @@ -31,9 +31,9 @@ func Init() { RPID: setting.Domain, RPOrigins: []string{appURL}, AuthenticatorSelection: protocol.AuthenticatorSelection{ - UserVerification: "discouraged", + UserVerification: protocol.VerificationDiscouraged, }, - AttestationPreference: protocol.PreferDirectAttestation, + AttestationPreference: protocol.PreferNoAttestation, }, } } @@ -66,7 +66,7 @@ func (u *User) WebAuthnIcon() string { return (*user_model.User)(u).AvatarLink(db.DefaultContext) } -// WebAuthnCredentials implementns the webauthn.User interface +// WebAuthnCredentials implements the webauthn.User interface func (u *User) WebAuthnCredentials() []webauthn.Credential { dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID) if err != nil { diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 64676254c5e10..284b378cca4f7 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -4,6 +4,7 @@ package auth import ( + "encoding/binary" "errors" "net/http" @@ -12,9 +13,7 @@ import ( wa "code.gitea.io/gitea/modules/auth/webauthn" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/externalaccount" @@ -49,15 +48,15 @@ func WebAuthn(ctx *context.Context) { ctx.HTML(http.StatusOK, tplWebAuthn) } -// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser -func WebAuthnLoginAssertion1(ctx *context.Context) { +// WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser +func WebAuthnPasskeyAssertion(ctx *context.Context) { assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin() if err != nil { ctx.ServerError("webauthn.BeginDiscoverableLogin", err) return } - if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil { + if err := ctx.Session.Set("webauthnAssertion-passkey-todo", sessionData); err != nil { ctx.ServerError("Session.Set", err) return } @@ -65,30 +64,11 @@ func WebAuthnLoginAssertion1(ctx *context.Context) { ctx.JSON(http.StatusOK, assertion) } -// SignInPost response for sign in request -func WebAuthnLogin(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("sign_in") - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignIn", err) - return - } - ctx.Data["OAuth2Providers"] = oauth2Providers - ctx.Data["Title"] = ctx.Tr("sign_in") - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" - ctx.Data["PageIsSignIn"] = true - ctx.Data["PageIsLogin"] = true - ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplSignIn) - return - } - - sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData) +// WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey +func WebAuthnPasskeyLogin(ctx *context.Context) { + sessionData, okData := ctx.Session.Get("webauthnAssertion-passkey-todo").(*webauthn.SessionData) if !okData || sessionData == nil { - ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) + ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session")) return } defer func() { @@ -96,11 +76,20 @@ func WebAuthnLogin(ctx *context.Context) { }() // Validate the parsed response. - // func(rawID, userHandle []byte) (user User, err error) + userID := int64(-1) cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) { - user := &wa.User{} - // TODO: get actual user using rawID and userHandle from database - return user, nil + var n int + userID, n = binary.Varint(userHandle) + if n <= 0 { + return nil, errors.New("invalid rawID") + } + + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + return nil, err + } + + return (*wa.User)(user), nil }, *sessionData, ctx.Req) if err != nil { // Failed authentication attempt. @@ -109,7 +98,16 @@ func WebAuthnLogin(ctx *context.Context) { return } - userID := int64(1) + if !cred.Flags.UserPresent { + ctx.Status(http.StatusBadRequest) + return + } + + if userID == -1 { + ctx.Status(http.StatusBadRequest) + return + } + user, err := user_model.GetUserByID(ctx, userID) if err != nil { ctx.ServerError("UserSignIn", err) @@ -145,7 +143,7 @@ func WebAuthnLogin(ctx *context.Context) { } } - remember := ctx.Session.Get("twofaRemember").(bool) + remember := false // TODO: implement remember me redirect := handleSignInFull(ctx, user, remember, false) if redirect == "" { redirect = setting.AppSubURL + "/" diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go index e382c8b9af413..2ae189f2ca526 100644 --- a/routers/web/user/setting/security/webauthn.go +++ b/routers/web/user/setting/security/webauthn.go @@ -45,7 +45,10 @@ func WebAuthnRegister(ctx *context.Context) { return } - credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer)) + credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer), webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ + UserVerification: protocol.VerificationRequired, + ResidentKey: protocol.ResidentKeyRequirementRequired, + })) if err != nil { ctx.ServerError("Unable to BeginRegistration", err) return diff --git a/routers/web/web.go b/routers/web/web.go index db4fee89cb559..d08e8da772857 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -535,8 +535,8 @@ func registerRoutes(m *web.Router) { }) m.Group("/webauthn", func() { m.Get("", auth.WebAuthn) - m.Get("/passkey/assertion", auth.WebAuthnLoginAssertion1) - m.Get("/passkey/login", auth.WebAuthnLogin) + m.Get("/passkey/assertion", auth.WebAuthnPasskeyAssertion) + m.Post("/passkey/login", auth.WebAuthnPasskeyLogin) m.Get("/assertion", auth.WebAuthnLoginAssertion) m.Post("/assertion", auth.WebAuthnLoginAssertionPost) }) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index bfac70f4a16fd..4f51adc53de3d 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -4,8 +4,24 @@ import {GET, POST} from '../modules/fetch.js'; const {appSubUrl} = window.config; -async function doWebAuthn(assertionUrl) { - const res = await GET(assertionUrl); +export async function initUserAuthWebAuthn() { + if (!detectWebAuthnSupport()) { + return; + } + + const elSignInPasskeyBtn = document.querySelector('.signin-passkey'); + if (elSignInPasskeyBtn) { + elSignInPasskeyBtn.addEventListener('click', loginPasskey); + } + + const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); + if (elPrompt) { + login2FA(); + } +} + +async function loginPasskey() { + const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`); if (res.status !== 200) { webAuthnError('unknown'); return; @@ -17,7 +33,59 @@ async function doWebAuthn(assertionUrl) { cred.id = decodeURLEncodedBase64(cred.id); } - console.log('leggo3', options); + try { + const credential = await navigator.credentials.get({ + publicKey: options.publicKey, + }); + + // Move data into Arrays in case it is super long + const authData = new Uint8Array(credential.response.authenticatorData); + const clientDataJSON = new Uint8Array(credential.response.clientDataJSON); + const rawId = new Uint8Array(credential.rawId); + const sig = new Uint8Array(credential.response.signature); + const userHandle = new Uint8Array(credential.response.userHandle); + + const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, { + data: { + id: credential.id, + rawId: encodeURLEncodedBase64(rawId), + type: credential.type, + clientExtensionResults: credential.getClientExtensionResults(), + response: { + authenticatorData: encodeURLEncodedBase64(authData), + clientDataJSON: encodeURLEncodedBase64(clientDataJSON), + signature: encodeURLEncodedBase64(sig), + userHandle: encodeURLEncodedBase64(userHandle), + }, + }, + }); + if (res.status === 500) { + webAuthnError('unknown'); + return; + } else if (res.status !== 200) { + webAuthnError('unable-to-process'); + return; + } + const reply = await res.json(); + + window.location.href = reply?.redirect ?? `${appSubUrl}/`; + } catch (err) { + webAuthnError('general', err.message); + } +} + +async function login2FA() { + const res = await GET(`${appSubUrl}/user/webauthn/assertion`); + if (res.status !== 200) { + webAuthnError('unknown'); + return; + } + + const options = await res.json(); + options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge); + for (const cred of options.publicKey.allowCredentials ?? []) { + cred.id = decodeURLEncodedBase64(cred.id); + } try { const credential = await navigator.credentials.get({ @@ -41,24 +109,6 @@ async function doWebAuthn(assertionUrl) { } } -export async function initUserAuthWebAuthn() { - console.log('moin initUserAuthWebAuthn'); - - if (!detectWebAuthnSupport()) { - return; - } - - const elSignInPasskeyBtn = document.querySelector('.signin-passkey'); - if (elSignInPasskeyBtn) { - elSignInPasskeyBtn.onclick = () => doWebAuthn(`${appSubUrl}/user/webauthn/passkey/assertion`); - } - - const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); - if (elPrompt) { - doWebAuthn(`${appSubUrl}/user/webauthn/assertion`); - } -} - async function verifyAssertion(assertedCredential) { // Move data into Arrays in case it is super long const authData = new Uint8Array(assertedCredential.response.authenticatorData); From b4faa4ae151b8b89b5c303df1dd696ae1387f180 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:41:17 +0200 Subject: [PATCH 04/21] simplify changes --- modules/auth/webauthn/webauthn.go | 2 +- routers/web/user/setting/security/webauthn.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index db4b88892d187..790006ee567cf 100644 --- a/modules/auth/webauthn/webauthn.go +++ b/modules/auth/webauthn/webauthn.go @@ -33,7 +33,7 @@ func Init() { AuthenticatorSelection: protocol.AuthenticatorSelection{ UserVerification: protocol.VerificationDiscouraged, }, - AttestationPreference: protocol.PreferNoAttestation, + AttestationPreference: protocol.PreferDirectAttestation, }, } } diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go index 2ae189f2ca526..1b8d0171f5633 100644 --- a/routers/web/user/setting/security/webauthn.go +++ b/routers/web/user/setting/security/webauthn.go @@ -46,8 +46,7 @@ func WebAuthnRegister(ctx *context.Context) { } credentialOptions, sessionData, err := wa.WebAuthn.BeginRegistration((*wa.User)(ctx.Doer), webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ - UserVerification: protocol.VerificationRequired, - ResidentKey: protocol.ResidentKeyRequirementRequired, + ResidentKey: protocol.ResidentKeyRequirementRequired, })) if err != nil { ctx.ServerError("Unable to BeginRegistration", err) From 9b9a140599729a9cf92a669ae58a92615e5a2dc0 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:50:07 +0200 Subject: [PATCH 05/21] fix typo --- models/auth/webauthn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go index a65d2e1e343db..553130ee2e9ee 100644 --- a/models/auth/webauthn.go +++ b/models/auth/webauthn.go @@ -181,7 +181,7 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) { return had > 0, err } -// WebAuthnCredentials implementns the webauthn.User interface +// WebAuthnCredentials implements the webauthn.User interface func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) { dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID) if err != nil { From eda2056798ac79e8efb01969a5f23070121f7162 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:50:15 +0200 Subject: [PATCH 06/21] use res.ok --- web_src/js/features/user-auth-webauthn.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index 4f51adc53de3d..ade452c68d6b4 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -22,7 +22,7 @@ export async function initUserAuthWebAuthn() { async function loginPasskey() { const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`); - if (res.status !== 200) { + if (!res.ok) { webAuthnError('unknown'); return; } @@ -62,7 +62,7 @@ async function loginPasskey() { if (res.status === 500) { webAuthnError('unknown'); return; - } else if (res.status !== 200) { + } else if (!res.ok) { webAuthnError('unable-to-process'); return; } @@ -134,7 +134,7 @@ async function verifyAssertion(assertedCredential) { if (res.status === 500) { webAuthnError('unknown'); return; - } else if (res.status !== 200) { + } else if (!res.ok) { webAuthnError('unable-to-process'); return; } @@ -230,7 +230,7 @@ async function webAuthnRegisterRequest() { if (res.status === 409) { webAuthnError('duplicated'); return; - } else if (res.status !== 200) { + } else if (!res.ok) { webAuthnError('unknown'); return; } From ddc8ef1b5bc2bfe356bfd9825b889eecd5ddb197 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:51:17 +0200 Subject: [PATCH 07/21] rm href --- templates/user/auth/signin_inner.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 67528ebce329f..51e0e3b982563 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -52,7 +52,7 @@ {{end}} {{if .OAuth2Providers}} From a050b806b0f838263b8c3254f19ac97400c022d7 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:57:17 +0200 Subject: [PATCH 08/21] cleanup --- routers/web/auth/webauthn.go | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 284b378cca4f7..3160c5e23f03c 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -56,7 +56,7 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) { return } - if err := ctx.Session.Set("webauthnAssertion-passkey-todo", sessionData); err != nil { + if err := ctx.Session.Set("webauthnPasskeyAssertion", sessionData); err != nil { ctx.ServerError("Session.Set", err) return } @@ -66,25 +66,25 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) { // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey func WebAuthnPasskeyLogin(ctx *context.Context) { - sessionData, okData := ctx.Session.Get("webauthnAssertion-passkey-todo").(*webauthn.SessionData) + sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData) if !okData || sessionData == nil { ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session")) return } defer func() { - _ = ctx.Session.Delete("webauthnAssertion") + _ = ctx.Session.Delete("webauthnPasskeyAssertion") }() // Validate the parsed response. - userID := int64(-1) + var user *user_model.User cred, err := wa.WebAuthn.FinishDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) { - var n int - userID, n = binary.Varint(userHandle) + userID, n := binary.Varint(userHandle) if n <= 0 { return nil, errors.New("invalid rawID") } - user, err := user_model.GetUserByID(ctx, userID) + var err error + user, err = user_model.GetUserByID(ctx, userID) if err != nil { return nil, err } @@ -103,17 +103,11 @@ func WebAuthnPasskeyLogin(ctx *context.Context) { return } - if userID == -1 { + if user == nil { ctx.Status(http.StatusBadRequest) return } - user, err := user_model.GetUserByID(ctx, userID) - if err != nil { - ctx.ServerError("UserSignIn", err) - return - } - // Ensure that the credential wasn't cloned by checking if CloneWarning is set. // (This is set if the sign counter is less than the one we have stored.) if cred.Authenticator.CloneWarning { @@ -148,7 +142,6 @@ func WebAuthnPasskeyLogin(ctx *context.Context) { if redirect == "" { redirect = setting.AppSubURL + "/" } - _ = ctx.Session.Delete("twofaUid") ctx.JSONRedirect(redirect) } From 1dfccd6582e1db36b60f611fc3af94fa79cc0539 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:44:13 +0200 Subject: [PATCH 09/21] Update web_src/js/features/user-auth-webauthn.js Co-authored-by: silverwind --- web_src/js/features/user-auth-webauthn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index ade452c68d6b4..a317fee7e2763 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -76,7 +76,7 @@ async function loginPasskey() { async function login2FA() { const res = await GET(`${appSubUrl}/user/webauthn/assertion`); - if (res.status !== 200) { + if (!res.ok) { webAuthnError('unknown'); return; } From ac3b9daea98589e7bef302b7f2a2f716ce4e447f Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 30 Jun 2024 08:45:25 +0200 Subject: [PATCH 10/21] Refactor login page --- templates/user/auth/signin.tmpl | 2 +- templates/user/auth/signin_inner.tmpl | 134 ++++++++++++++------------ 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index b0e9ce8c74fa9..7d598a8d919b3 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -2,7 +2,7 @@
{{template "user/auth/signin_navbar" .}}
-
+
{{template "user/auth/signin_inner" .}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 51e0e3b982563..641cf323c865d 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -1,76 +1,82 @@ -{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} -{{template "base/alert" .}} -{{end}} -

- {{if .LinkAccountMode}} - {{ctx.Locale.Tr "auth.oauth_signin_title"}} - {{else}} - {{ctx.Locale.Tr "auth.login_userpass"}} +
+ {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} + {{template "base/alert" .}} {{end}} -

-
- {{template "user/auth/webauthn_error" .}} +

+ {{if .LinkAccountMode}} + {{ctx.Locale.Tr "auth.oauth_signin_title"}} + {{else}} + {{ctx.Locale.Tr "auth.login_userpass"}} + {{end}} +

+
+ {{template "user/auth/webauthn_error" .}} - - {{.CsrfTokenHtml}} -
- - -
- {{if or (not .DisablePassword) .LinkAccountMode}} -
- - -
- {{end}} - {{if not .LinkAccountMode}} -
-
- - -
-
- {{end}} + + {{.CsrfTokenHtml}} +
+ + +
+ {{if or (not .DisablePassword) .LinkAccountMode}} +
+
+ + {{ctx.Locale.Tr "auth.forgot_password"}} +
+ +
+ {{end}} + {{if not .LinkAccountMode}} +
+
+ + +
+
+ {{end}} - {{template "user/auth/captcha" .}} + {{template "user/auth/captcha" .}} -
- - {{ctx.Locale.Tr "auth.forgot_password"}} -
+
+ +
+ - {{if .ShowRegistrationButton}} -
- {{ctx.Locale.Tr "auth.sign_up_now"}} + {{if .OAuth2Providers}} +
+ {{ctx.Locale.Tr "sign_in_or"}}
- {{end}} + + {{end}} +
+
-
+
+ - {{if .OAuth2Providers}} -
- {{ctx.Locale.Tr "sign_in_or"}} -
-
-
-
- {{range $provider := .OAuth2Providers}} - - {{end}} + {{if .ShowRegistrationButton}} + -
+ {{end}}
- {{end}} -
From ab764a45499b8e32f223768f92ed3d042e90d362 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 30 Jun 2024 09:34:38 +0200 Subject: [PATCH 11/21] adjust auth --- options/locale/locale_en-US.ini | 4 +++- templates/user/auth/signin.tmpl | 3 ++- templates/user/auth/signin_inner.tmpl | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d10f61f2ffc9e..03cc7baffcf69 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -396,7 +396,8 @@ remember_me = Remember This Device remember_me.compromised = The login token is not valid anymore which may indicate a compromised account. Please check your account for unusual activities. forgot_password_title= Forgot Password forgot_password = Forgot password? -sign_up_now = Need an account? Register now. +need_account = Need an account? +sign_up_now = Register now. sign_up_successful = Account was successfully created. Welcome! confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to %s. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it. must_change_password = Update your password @@ -459,6 +460,7 @@ password_pwned = The password you chose is on a - {{template "user/auth/signin_navbar" .}}
+ + {{template "user/auth/signin_inner" .}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 641cf323c865d..5e9ac0ab89307 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -70,11 +70,12 @@
-
+
{{if .ShowRegistrationButton}}
+ {{ctx.Locale.Tr "auth.need_account"}} {{ctx.Locale.Tr "auth.sign_up_now"}}
{{end}} From 5db71fd0322510ee014c9a72439491402dc14abc Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Sun, 30 Jun 2024 09:36:21 +0200 Subject: [PATCH 12/21] adjust ui --- templates/user/auth/oauth_container.tmpl | 23 ++++++++ templates/user/auth/signin.tmpl | 4 +- templates/user/auth/signin_inner.tmpl | 22 ++------ templates/user/auth/signin_openid.tmpl | 69 ++++++++++++++---------- templates/user/auth/signup.tmpl | 5 +- templates/user/auth/signup_inner.tmpl | 40 ++++++-------- 6 files changed, 89 insertions(+), 74 deletions(-) create mode 100644 templates/user/auth/oauth_container.tmpl diff --git a/templates/user/auth/oauth_container.tmpl b/templates/user/auth/oauth_container.tmpl new file mode 100644 index 0000000000000..ff4fe015dd017 --- /dev/null +++ b/templates/user/auth/oauth_container.tmpl @@ -0,0 +1,23 @@ +{{if or .OAuth2Providers .EnableOpenIDSignIn}} +
+ {{ctx.Locale.Tr "sign_in_or"}} +
+ +{{end}} \ No newline at end of file diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index 2d709f89dcaed..f0a462c6a5cdd 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -2,7 +2,9 @@
- + + + {{template "user/auth/signin_inner" .}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 5e9ac0ab89307..663ab74f794ec 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -10,8 +10,6 @@ {{end}}
- {{template "user/auth/webauthn_error" .}} -
{{.CsrfTokenHtml}}
@@ -49,27 +47,13 @@
- {{if .OAuth2Providers}} -
- {{ctx.Locale.Tr "sign_in_or"}} -
- - {{end}} + {{template "user/auth/oauth_container" .}}
+ {{template "user/auth/webauthn_error" .}} +
diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl index c1f392dc134df..ed38c11b97e80 100644 --- a/templates/user/auth/signin_openid.tmpl +++ b/templates/user/auth/signin_openid.tmpl @@ -1,35 +1,50 @@ {{template "base/head" .}}