diff --git a/server/crypto/rsa.go b/server/crypto/rsa.go index 45be9ad..6eba852 100644 --- a/server/crypto/rsa.go +++ b/server/crypto/rsa.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/pem" "errors" - "fmt" ) // NewRSAKey to generate new RSA Key if env is not set @@ -138,28 +137,5 @@ func DecryptRSA(cipherText string, privateKey rsa.PrivateKey) (string, error) { if err != nil { return "", err } - fmt.Println("Plaintext:", string(plaintext)) return string(plaintext), nil } - -func ParseRSAPublicKey(key string) (*rsa.PublicKey, error) { - // Decode the PEM-encoded public key data. - block, _ := pem.Decode([]byte(key)) - if block == nil { - return nil, fmt.Errorf("failed to parse PEM block containing public key") - } - - // Parse the DER-encoded public key data. - pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err - } - - // Type-assert the parsed public key to an rsa.PublicKey. - rsaPublicKey, ok := pubKey.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("parsed public key is not an RSA public key") - } - - return rsaPublicKey, nil -} diff --git a/server/db/models/user.go b/server/db/models/user.go index c077356..f0b1e4f 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -35,6 +35,7 @@ type User struct { CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` AppData *string `json:"app_data" bson:"app_data" cql:"app_data" dynamo:"app_data"` TotpSecret *string `json:"totp_secret" bson:"totp_secret" cql:"totp_secret" dynamo:"totp_secret"` + TotpVerified bool `json:"totp_verified" bson:"totp_verified" cql:"totp_verified" dynamo:"totp_verified"` } func (user *User) AsAPIUser() *model.User { diff --git a/server/db/providers/arangodb/totp.go b/server/db/providers/arangodb/totp.go index d64d821..35dd7a3 100644 --- a/server/db/providers/arangodb/totp.go +++ b/server/db/providers/arangodb/totp.go @@ -3,13 +3,8 @@ package arangodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/cassandradb/totp.go b/server/db/providers/cassandradb/totp.go index b34a6c3..38965d7 100644 --- a/server/db/providers/cassandradb/totp.go +++ b/server/db/providers/cassandradb/totp.go @@ -3,13 +3,8 @@ package cassandradb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/couchbase/totp.go b/server/db/providers/couchbase/totp.go index 4a3b44f..cd8d56b 100644 --- a/server/db/providers/couchbase/totp.go +++ b/server/db/providers/couchbase/totp.go @@ -3,13 +3,8 @@ package couchbase import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/dynamodb/totp.go b/server/db/providers/dynamodb/totp.go index 8a8a705..844f57c 100644 --- a/server/db/providers/dynamodb/totp.go +++ b/server/db/providers/dynamodb/totp.go @@ -3,13 +3,8 @@ package dynamodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/mongodb/totp.go b/server/db/providers/mongodb/totp.go index 220c1c8..8070850 100644 --- a/server/db/providers/mongodb/totp.go +++ b/server/db/providers/mongodb/totp.go @@ -3,13 +3,8 @@ package mongodb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/provider_template/totp.go b/server/db/providers/provider_template/totp.go index 9da1d0d..4e349e5 100644 --- a/server/db/providers/provider_template/totp.go +++ b/server/db/providers/provider_template/totp.go @@ -3,13 +3,8 @@ package provider_template import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "image/png" - "os" "time" "github.com/pquerna/otp/totp" @@ -62,44 +57,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - for { - status := totp.Validate(passcode, *user.TotpSecret) + status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) return status, nil } + return status, nil } -} - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - key := os.Getenv("TOTP_PRIVATE_KEY") - var privateKey *rsa.PrivateKey - if key == "" { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - - privateKeyPEM := encodePrivateKeyToPEM(privateKey) - os.Setenv("TOTP_PRIVATE_KEY", string(privateKeyPEM)) - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} - -func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { - // Marshal the private key to DER format. - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - - // Create a PEM block for the private key. - privateKeyPEMBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - // Encode the PEM block to PEM format. - privateKeyPEM := pem.EncodeToMemory(privateKeyPEMBlock) - - return privateKeyPEM + return status, nil } diff --git a/server/db/providers/sql/totp.go b/server/db/providers/sql/totp.go index 2e8ad11..66a46aa 100644 --- a/server/db/providers/sql/totp.go +++ b/server/db/providers/sql/totp.go @@ -3,8 +3,6 @@ package sql import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" "fmt" log "github.com/sirupsen/logrus" "image/png" @@ -60,18 +58,14 @@ func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id str if err != nil { return false, fmt.Errorf("error while getting user details") } - - // validate passcode inputted by user - status := totp.Validate(passcode, *user.TotpSecret) + if !user.TotpVerified { + if status { + user.TotpVerified = true + p.UpdateUser(ctx, user) + return status, nil + } + return status, nil + } return status, nil } - -func (p *provider) GenerateKeysTOTP() (*rsa.PublicKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - publicKey := privateKey.PublicKey - return &publicKey, nil -} diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 91040be..002a00b 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "github.com/authorizerdev/authorizer/server/crypto" "strings" "time" @@ -161,7 +162,21 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isTOTPLoginDisabled { - if user.TotpSecret == nil { + pubKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey) + if err != nil { + log.Debug("error while getting public key") + } + + publicKey, err := crypto.ParseRsaPublicKeyFromPemStr(pubKey) + if err != nil { + log.Debug("error while parsing public key") + } + + encryptedUserId, err := crypto.EncryptRSA(user.ID, *publicKey) + if err != nil { + log.Debug("error while encrypting user id") + } + if !user.TotpVerified { base64URL, err := db.Provider.GenerateTotp(ctx, user.ID) if err != nil { log.Debug("error while generating base64 url: ", err) @@ -170,14 +185,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes res = &model.AuthResponse{ Message: `Proceed to totp screen`, TotpBase64url: base64URL, - TokenTotp: &user.ID, + TokenTotp: &encryptedUserId, } return res, nil } else { - //res.TokenTotp = &user.ID res = &model.AuthResponse{ Message: `Proceed to totp screen`, - TokenTotp: &user.ID, + TokenTotp: &encryptedUserId, } return res, nil } diff --git a/server/resolvers/verify_totp.go b/server/resolvers/verify_totp.go index c335aa0..97aa592 100644 --- a/server/resolvers/verify_totp.go +++ b/server/resolvers/verify_totp.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "github.com/authorizerdev/authorizer/server/crypto" "strings" "time" @@ -24,7 +25,22 @@ import ( func VerifyTotpResolver(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) { var res *model.AuthResponse - userID := params.Token + encryptedkey := params.Token + + pvtKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPrivateKey) + if err != nil { + log.Debug("error while getting private key") + } + + privateKey, err := crypto.ParseRsaPrivateKeyFromPemStr(pvtKey) + if err != nil { + log.Debug("error while parsing private key") + } + + userID, err := crypto.DecryptRSA(encryptedkey, *privateKey) + if err != nil { + log.Debug("error while decrypting userId") + } gc, err := utils.GinContextFromContext(ctx) if err != nil { diff --git a/server/test/integration_test.go b/server/test/integration_test.go index 3d4bb3d..415cb0a 100644 --- a/server/test/integration_test.go +++ b/server/test/integration_test.go @@ -141,6 +141,7 @@ func TestResolvers(t *testing.T) { inviteUserTest(t, s) validateJwtTokenTest(t, s) verifyOTPTest(t, s) + verifyTOTPTest(t, s) resendOTPTest(t, s) validateSessionTests(t, s) diff --git a/server/test/verify_totp_test.go b/server/test/verify_totp_test.go new file mode 100644 index 0000000..741f347 --- /dev/null +++ b/server/test/verify_totp_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "testing" +) + +func verifyTOTPTest(t *testing.T, s TestSetup) { + //t.Helper() + //t.Run(`should verify totp`, func(t *testing.T) { + // req, ctx := createContext(s) + // email := "verify_otp." + s.TestInfo.Email + // res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ + // Email: email, + // Password: s.TestInfo.Password, + // ConfirmPassword: s.TestInfo.Password, + // }) + // assert.NoError(t, err) + // assert.NotNil(t, res) + // + // // Login should fail as email is not verified + // loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{ + // Email: email, + // Password: s.TestInfo.Password, + // }) + // assert.Error(t, err) + // assert.Nil(t, loginRes) + // verificationRequest, err := db.Provider.GenerateTotp(ctx, loginRes.User.ID) + // assert.Nil(t, err) + // assert.Equal(t, email, verificationRequest.Email) + // verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ + // Token: verificationRequest.Token, + // }) + // assert.Nil(t, err) + // assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") + // + // // Using access token update profile + // s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken)) + // ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) + // updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + // IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + // }) + // assert.NoError(t, err) + // assert.NotEmpty(t, updateProfileRes.Message) + // + // // Login should not return error but access token should be empty as otp should have been sent + // loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ + // Email: email, + // Password: s.TestInfo.Password, + // }) + // assert.NoError(t, err) + // assert.NotNil(t, loginRes) + // assert.Nil(t, loginRes.AccessToken) + // + // // Get otp from db + // otp, err := db.Provider.GetOTPByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotEmpty(t, otp.Otp) + // // Get user by email + // user, err := db.Provider.GetUserByEmail(ctx, email) + // assert.NoError(t, err) + // assert.NotNil(t, user) + // // Set mfa cookie session + // mfaSession := uuid.NewString() + // memorystore.Provider.SetMfaSession(user.ID, mfaSession, time.Now().Add(1*time.Minute).Unix()) + // cookie := fmt.Sprintf("%s=%s;", constants.MfaCookieName+"_session", mfaSession) + // cookie = strings.TrimSuffix(cookie, ";") + // req.Header.Set("Cookie", cookie) + // verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + // Email: &email, + // Otp: otp.Otp, + // }) + // assert.Nil(t, err) + // assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") + // cleanData(email) + //}) +}