2022-01-14 18:03:31 +03:00
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
2022-01-15 19:52:56 +03:00
"encoding/base32"
2022-01-14 18:03:31 +03: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/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/externalaccount"
"github.com/duo-labs/webauthn/protocol"
"github.com/duo-labs/webauthn/webauthn"
)
var tplWebAuthn base . TplName = "user/auth/webauthn"
// WebAuthn shows the WebAuthn login page
func WebAuthn ( ctx * context . Context ) {
ctx . Data [ "Title" ] = ctx . Tr ( "twofa" )
// Check auto-login.
if checkAutoLogin ( ctx ) {
return
}
2022-01-20 20:46:10 +03:00
// Ensure user is in a 2FA session.
2022-01-14 18:03:31 +03:00
if ctx . Session . Get ( "twofaUid" ) == nil {
ctx . ServerError ( "UserSignIn" , errors . New ( "not in WebAuthn session" ) )
return
}
2022-03-23 07:54:07 +03:00
ctx . HTML ( http . StatusOK , tplWebAuthn )
2022-01-14 18:03:31 +03: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
}
user , err := user_model . GetUserByID ( idSess )
if err != nil {
ctx . ServerError ( "UserSignIn" , err )
return
}
exists , err := auth . ExistsWebAuthnCredentialsForUID ( user . ID )
if err != nil {
ctx . ServerError ( "UserSignIn" , err )
return
}
if ! exists {
ctx . ServerError ( "UserSignIn" , errors . New ( "no device registered" ) )
return
}
2022-01-20 20:00:38 +03:00
// FIXME: DEPRECATED appid is deprecated and is planned to be removed in v1.18.0
2022-01-14 18:03:31 +03:00
assertion , sessionData , err := wa . WebAuthn . BeginLogin ( ( * wa . User ) ( user ) , webauthn . WithAssertionExtensions ( protocol . AuthenticationExtensions {
"appid" : setting . U2F . AppID ,
} ) )
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
user , err := user_model . GetUserByID ( idSess )
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.
cred , err := wa . WebAuthn . ValidateLogin ( ( * wa . User ) ( user ) , * sessionData , parsedResponse )
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.
2022-01-15 19:52:56 +03:00
dbCred , err := auth . GetWebAuthnCredentialByCredID ( user . ID , base32 . HexEncoding . EncodeToString ( cred . ID ) )
2022-01-14 18:03:31 +03:00
if err != nil {
ctx . ServerError ( "GetWebAuthnCredentialByCredID" , err )
return
}
dbCred . SignCount = cred . Authenticator . SignCount
if err := dbCred . UpdateSignCount ( ) ; 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 . 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" )
// Finally check if the appid extension was used:
if value , ok := parsedResponse . ClientExtensionResults [ "appid" ] ; ok {
if appid , ok := value . ( bool ) ; ok && appid {
ctx . Flash . Error ( ctx . Tr ( "webauthn_u2f_deprecated" , dbCred . Name ) )
}
}
2022-03-23 07:54:07 +03:00
ctx . JSON ( http . StatusOK , map [ string ] string { "redirect" : redirect } )
2022-01-14 18:03:31 +03:00
}