code.gitea.io/gitea@v1.21.7/routers/web/auth/webauthn.go (about) 1 // Copyright 2018 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package auth 5 6 import ( 7 "errors" 8 "net/http" 9 10 "code.gitea.io/gitea/models/auth" 11 user_model "code.gitea.io/gitea/models/user" 12 wa "code.gitea.io/gitea/modules/auth/webauthn" 13 "code.gitea.io/gitea/modules/base" 14 "code.gitea.io/gitea/modules/context" 15 "code.gitea.io/gitea/modules/log" 16 "code.gitea.io/gitea/modules/setting" 17 "code.gitea.io/gitea/services/externalaccount" 18 19 "github.com/go-webauthn/webauthn/protocol" 20 "github.com/go-webauthn/webauthn/webauthn" 21 ) 22 23 var tplWebAuthn base.TplName = "user/auth/webauthn" 24 25 // WebAuthn shows the WebAuthn login page 26 func WebAuthn(ctx *context.Context) { 27 ctx.Data["Title"] = ctx.Tr("twofa") 28 29 // Check auto-login. 30 if checkAutoLogin(ctx) { 31 return 32 } 33 34 // Ensure user is in a 2FA session. 35 if ctx.Session.Get("twofaUid") == nil { 36 ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) 37 return 38 } 39 40 hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Session.Get("twofaUid").(int64)) 41 if err != nil { 42 ctx.ServerError("HasTwoFactorByUID", err) 43 return 44 } 45 46 ctx.Data["HasTwoFactor"] = hasTwoFactor 47 48 ctx.HTML(http.StatusOK, tplWebAuthn) 49 } 50 51 // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser 52 func WebAuthnLoginAssertion(ctx *context.Context) { 53 // Ensure user is in a WebAuthn session. 54 idSess, ok := ctx.Session.Get("twofaUid").(int64) 55 if !ok || idSess == 0 { 56 ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) 57 return 58 } 59 60 user, err := user_model.GetUserByID(ctx, idSess) 61 if err != nil { 62 ctx.ServerError("UserSignIn", err) 63 return 64 } 65 66 exists, err := auth.ExistsWebAuthnCredentialsForUID(ctx, user.ID) 67 if err != nil { 68 ctx.ServerError("UserSignIn", err) 69 return 70 } 71 if !exists { 72 ctx.ServerError("UserSignIn", errors.New("no device registered")) 73 return 74 } 75 76 assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user)) 77 if err != nil { 78 ctx.ServerError("webauthn.BeginLogin", err) 79 return 80 } 81 82 if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil { 83 ctx.ServerError("Session.Set", err) 84 return 85 } 86 ctx.JSON(http.StatusOK, assertion) 87 } 88 89 // WebAuthnLoginAssertionPost validates the signature and logs the user in 90 func WebAuthnLoginAssertionPost(ctx *context.Context) { 91 idSess, ok := ctx.Session.Get("twofaUid").(int64) 92 sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData) 93 if !ok || !okData || sessionData == nil || idSess == 0 { 94 ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session")) 95 return 96 } 97 defer func() { 98 _ = ctx.Session.Delete("webauthnAssertion") 99 }() 100 101 // Load the user from the db 102 user, err := user_model.GetUserByID(ctx, idSess) 103 if err != nil { 104 ctx.ServerError("UserSignIn", err) 105 return 106 } 107 108 log.Trace("Finishing webauthn authentication with user: %s", user.Name) 109 110 // Now we do the equivalent of webauthn.FinishLogin using a combination of our session data 111 // (from webauthnAssertion) and verify the provided request.0 112 parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req) 113 if err != nil { 114 // Failed authentication attempt. 115 log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err) 116 ctx.Status(http.StatusForbidden) 117 return 118 } 119 120 // Validate the parsed response. 121 cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse) 122 if err != nil { 123 // Failed authentication attempt. 124 log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err) 125 ctx.Status(http.StatusForbidden) 126 return 127 } 128 129 // Ensure that the credential wasn't cloned by checking if CloneWarning is set. 130 // (This is set if the sign counter is less than the one we have stored.) 131 if cred.Authenticator.CloneWarning { 132 log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr()) 133 ctx.Status(http.StatusForbidden) 134 return 135 } 136 137 // Success! Get the credential and update the sign count with the new value we received. 138 dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID) 139 if err != nil { 140 ctx.ServerError("GetWebAuthnCredentialByCredID", err) 141 return 142 } 143 144 dbCred.SignCount = cred.Authenticator.SignCount 145 if err := dbCred.UpdateSignCount(ctx); err != nil { 146 ctx.ServerError("UpdateSignCount", err) 147 return 148 } 149 150 // Now handle account linking if that's requested 151 if ctx.Session.Get("linkAccount") != nil { 152 if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil { 153 ctx.ServerError("LinkAccountFromStore", err) 154 return 155 } 156 } 157 158 remember := ctx.Session.Get("twofaRemember").(bool) 159 redirect := handleSignInFull(ctx, user, remember, false) 160 if redirect == "" { 161 redirect = setting.AppSubURL + "/" 162 } 163 _ = ctx.Session.Delete("twofaUid") 164 165 ctx.JSONRedirect(redirect) 166 }