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