diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go index 0e24095..076990b 100644 --- a/server/db/providers/arangodb/otp.go +++ b/server/db/providers/arangodb/otp.go @@ -45,7 +45,7 @@ func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP) bindVars := map[string]interface{}{ "email": emailAddress, @@ -64,13 +64,13 @@ func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*mod } break } - _, err := cursor.ReadDocument(ctx, otp) + _, err := cursor.ReadDocument(ctx, &otp) if err != nil { return nil, err } } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go index c3f637e..bbf0426 100644 --- a/server/db/providers/mongodb/otp.go +++ b/server/db/providers/mongodb/otp.go @@ -33,15 +33,15 @@ func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) - err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(otp) + err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(&otp) if err != nil { return nil, err } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/db/providers/sql/otp.go b/server/db/providers/sql/otp.go index 8fd7780..9aabcab 100644 --- a/server/db/providers/sql/otp.go +++ b/server/db/providers/sql/otp.go @@ -32,13 +32,13 @@ func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP - result := p.db.Where("email = ?", emailAddress).First(otp) + result := p.db.Where("email = ?", emailAddress).First(&otp) if result.Error != nil { return nil, result.Error } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 213e028..506f61f 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -2,11 +2,111 @@ package resolvers import ( "context" + "fmt" + "strings" + "time" + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" + log "github.com/sirupsen/logrus" ) // VerifyOtpResolver resolver for verify otp mutation func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) { - return nil, nil + var res *model.AuthResponse + gc, err := utils.GinContextFromContext(ctx) + + if err != nil { + log.Debug("Failed to get GinContext: ", err) + return res, err + } + + otp, err := db.Provider.GetOTPByEmail(ctx, params.Email) + + if err != nil { + log.Debug("Failed to get otp request by email: ", err) + return res, fmt.Errorf(`invalid email: %s`, err.Error()) + } + + expiresIn := otp.ExpiresAt - time.Now().Unix() + + if params.Otp != otp.Otp || expiresIn < 0 { + log.Debug("Failed to verify otp request: ", err) + return res, fmt.Errorf(`invalid otp: %s`, err.Error()) + } + + user, err := db.Provider.GetUserByEmail(ctx, params.Email) + + if err != nil { + log.Debug("Failed to get user by email: ", err) + return res, err + } + + isSignUp := user.EmailVerifiedAt == nil + + // TODO - Add Login method in DB + + loginMethod := constants.AuthRecipeMethodBasicAuth + + roles := strings.Split(user.Roles, ",") + scope := []string{"openid", "email", "profile"} + authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) + + if err != nil { + log.Debug("Failed to create auth token: ", err) + return res, err + } + + go func() { + err = db.Provider.DeleteOTP(gc, otp) + + if err != nil { + log.Debug("Failed to delete otp: ", err) + } + if isSignUp { + utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) + } else { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user) + } + + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() + + authTokenExpiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if authTokenExpiresIn <= 0 { + authTokenExpiresIn = 1 + } + + if authTokenExpiresIn <= 0 { + authTokenExpiresIn = 1 + } + + res = &model.AuthResponse{ + Message: `OTP verified successfully.`, + AccessToken: &authToken.AccessToken.Token, + IDToken: &authToken.IDToken.Token, + ExpiresIn: &authTokenExpiresIn, + User: user.AsAPIUser(), + } + + sessionKey := loginMethod + ":" + user.ID + cookie.SetSession(gc, authToken.FingerPrintHash) + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash) + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) + + if authToken.RefreshToken != nil { + res.RefreshToken = &authToken.RefreshToken.Token + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) + } + return res, nil } diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index 1a49632..a76522e 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -114,7 +114,7 @@ func TestResolvers(t *testing.T) { metaTests(t, s) inviteUserTest(t, s) validateJwtTokenTest(t, s) - + verifyOTPTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done deleteWebhookTest(t, s) // delete webhooks (admin resolver) }) diff --git a/server/test/verify_otp_test.go b/server/test/verify_otp_test.go new file mode 100644 index 0000000..d236644 --- /dev/null +++ b/server/test/verify_otp_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "testing" + "time" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func verifyOTPTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should verify otp`, func(t *testing.T) { + _, 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) + otp, err := db.Provider.UpsertOTP(ctx, &models.OTP{ + Otp: "123456", + Email: email, + ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + }) + assert.Equal(t, email, otp.Email) + assert.Nil(t, res.AccessToken, "access token should be nil") + + verifyRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + Otp: "123456", + Email: email, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") + cleanData(email) + }) +}