2022-01-14 23:03:31 +08:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-28 02:20:29 +08:00
// SPDX-License-Identifier: MIT
2022-01-14 23:03:31 +08:00
package auth
import (
2024-06-30 06:50:03 +08:00
"encoding/binary"
2022-01-14 23:03:31 +08:00
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
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/setting"
2024-02-27 15:12:22 +08:00
"code.gitea.io/gitea/services/context"
2022-01-14 23:03:31 +08:00
"code.gitea.io/gitea/services/externalaccount"
2023-01-12 10:51:00 +08:00
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
2022-01-14 23:03:31 +08:00
)
var tplWebAuthn base . TplName = "user/auth/webauthn"
// WebAuthn shows the WebAuthn login page
func WebAuthn ( ctx * context . Context ) {
ctx . Data [ "Title" ] = ctx . Tr ( "twofa" )
2023-10-14 08:56:41 +08:00
if CheckAutoLogin ( ctx ) {
2022-01-14 23:03:31 +08:00
return
}
2022-01-21 01:46:10 +08:00
// Ensure user is in a 2FA session.
2022-01-14 23:03:31 +08:00
if ctx . Session . Get ( "twofaUid" ) == nil {
ctx . ServerError ( "UserSignIn" , errors . New ( "not in WebAuthn session" ) )
return
}
2023-10-12 04:12:54 +08:00
hasTwoFactor , err := auth . HasTwoFactorByUID ( ctx , ctx . Session . Get ( "twofaUid" ) . ( int64 ) )
if err != nil {
ctx . ServerError ( "HasTwoFactorByUID" , err )
return
}
ctx . Data [ "HasTwoFactor" ] = hasTwoFactor
2022-03-23 12:54:07 +08:00
ctx . HTML ( http . StatusOK , tplWebAuthn )
2022-01-14 23:03:31 +08:00
}
2024-06-30 06:50:03 +08:00
// 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 ( "webauthnPasskeyAssertion" , sessionData ) ; err != nil {
ctx . ServerError ( "Session.Set" , err )
return
}
ctx . JSON ( http . StatusOK , assertion )
}
// WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
func WebAuthnPasskeyLogin ( ctx * context . Context ) {
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 ( "webauthnPasskeyAssertion" )
} ( )
// Validate the parsed response.
2024-11-27 00:04:17 +08:00
// ParseCredentialRequestResponse+ValidateDiscoverableLogin equals to FinishDiscoverableLogin, but we need to ParseCredentialRequestResponse first to get flags
2024-06-30 06:50:03 +08:00
var user * user_model . User
2024-11-27 00:04:17 +08:00
parsedResponse , err := protocol . ParseCredentialRequestResponse ( ctx . Req )
if err != nil {
// Failed authentication attempt.
log . Info ( "Failed authentication attempt for %s from %s: %v" , user . Name , ctx . RemoteAddr ( ) , err )
ctx . Status ( http . StatusForbidden )
return
}
cred , err := wa . WebAuthn . ValidateDiscoverableLogin ( func ( rawID , userHandle [ ] byte ) ( webauthn . User , error ) {
2024-06-30 06:50:03 +08:00
userID , n := binary . Varint ( userHandle )
if n <= 0 {
return nil , errors . New ( "invalid rawID" )
}
var err error
user , err = user_model . GetUserByID ( ctx , userID )
if err != nil {
return nil , err
}
2024-11-27 00:04:17 +08:00
return wa . NewWebAuthnUser ( ctx , user , parsedResponse . Response . AuthenticatorData . Flags ) , nil
} , * sessionData , parsedResponse )
2024-06-30 06:50:03 +08:00
if err != nil {
// Failed authentication attempt.
log . Info ( "Failed authentication attempt for passkey from %s: %v" , ctx . RemoteAddr ( ) , err )
ctx . Status ( http . StatusForbidden )
return
}
if ! cred . Flags . UserPresent {
ctx . Status ( http . StatusBadRequest )
return
}
if user == nil {
ctx . Status ( http . StatusBadRequest )
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 := false // TODO: implement remember me
redirect := handleSignInFull ( ctx , user , remember , false )
if redirect == "" {
redirect = setting . AppSubURL + "/"
}
ctx . JSONRedirect ( redirect )
}
2022-01-14 23:03:31 +08:00
// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
func WebAuthnLoginAssertion ( ctx * context . Context ) {
// Ensure user is in a WebAuthn session.
idSess , ok := ctx . Session . Get ( "twofaUid" ) . ( int64 )
if ! ok || idSess == 0 {
ctx . ServerError ( "UserSignIn" , errors . New ( "not in WebAuthn session" ) )
return
}
2022-12-03 10:48:26 +08:00
user , err := user_model . GetUserByID ( ctx , idSess )
2022-01-14 23:03:31 +08:00
if err != nil {
ctx . ServerError ( "UserSignIn" , err )
return
}
2023-09-16 22:39:12 +08:00
exists , err := auth . ExistsWebAuthnCredentialsForUID ( ctx , user . ID )
2022-01-14 23:03:31 +08:00
if err != nil {
ctx . ServerError ( "UserSignIn" , err )
return
}
if ! exists {
ctx . ServerError ( "UserSignIn" , errors . New ( "no device registered" ) )
return
}
2024-11-27 00:04:17 +08:00
webAuthnUser := wa . NewWebAuthnUser ( ctx , user )
assertion , sessionData , err := wa . WebAuthn . BeginLogin ( webAuthnUser )
2022-01-14 23:03:31 +08:00
if err != nil {
ctx . ServerError ( "webauthn.BeginLogin" , err )
return
}
if err := ctx . Session . Set ( "webauthnAssertion" , sessionData ) ; err != nil {
ctx . ServerError ( "Session.Set" , err )
return
}
ctx . JSON ( http . StatusOK , assertion )
}
// WebAuthnLoginAssertionPost validates the signature and logs the user in
func WebAuthnLoginAssertionPost ( ctx * context . Context ) {
idSess , ok := ctx . Session . Get ( "twofaUid" ) . ( int64 )
sessionData , okData := ctx . Session . Get ( "webauthnAssertion" ) . ( * webauthn . SessionData )
if ! ok || ! okData || sessionData == nil || idSess == 0 {
ctx . ServerError ( "UserSignIn" , errors . New ( "not in WebAuthn session" ) )
return
}
defer func ( ) {
_ = ctx . Session . Delete ( "webauthnAssertion" )
} ( )
// Load the user from the db
2022-12-03 10:48:26 +08:00
user , err := user_model . GetUserByID ( ctx , idSess )
2022-01-14 23:03:31 +08:00
if err != nil {
ctx . ServerError ( "UserSignIn" , err )
return
}
log . Trace ( "Finishing webauthn authentication with user: %s" , user . Name )
// Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
// (from webauthnAssertion) and verify the provided request.0
parsedResponse , err := protocol . ParseCredentialRequestResponse ( ctx . Req )
if err != nil {
// Failed authentication attempt.
log . Info ( "Failed authentication attempt for %s from %s: %v" , user . Name , ctx . RemoteAddr ( ) , err )
ctx . Status ( http . StatusForbidden )
return
}
// Validate the parsed response.
2024-11-27 00:04:17 +08:00
webAuthnUser := wa . NewWebAuthnUser ( ctx , user , parsedResponse . Response . AuthenticatorData . Flags )
cred , err := wa . WebAuthn . ValidateLogin ( webAuthnUser , * sessionData , parsedResponse )
2022-01-14 23:03:31 +08:00
if err != nil {
// Failed authentication attempt.
log . Info ( "Failed authentication attempt for %s from %s: %v" , user . Name , ctx . RemoteAddr ( ) , err )
ctx . Status ( http . StatusForbidden )
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.
2023-09-16 22:39:12 +08:00
dbCred , err := auth . GetWebAuthnCredentialByCredID ( ctx , user . ID , cred . ID )
2022-01-14 23:03:31 +08:00
if err != nil {
ctx . ServerError ( "GetWebAuthnCredentialByCredID" , err )
return
}
dbCred . SignCount = cred . Authenticator . SignCount
2023-09-16 22:39:12 +08:00
if err := dbCred . UpdateSignCount ( ctx ) ; err != nil {
2022-01-14 23:03:31 +08:00
ctx . ServerError ( "UpdateSignCount" , err )
return
}
// Now handle account linking if that's requested
if ctx . Session . Get ( "linkAccount" ) != nil {
2023-09-25 21:17:37 +08:00
if err := externalaccount . LinkAccountFromStore ( ctx , ctx . Session , user ) ; err != nil {
2022-01-14 23:03:31 +08:00
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" )
2023-07-26 14:04:01 +08:00
ctx . JSONRedirect ( redirect )
2022-01-14 23:03:31 +08:00
}