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