Skip to content

Commit

Permalink
feat(login): added SignIn With OTP
Browse files Browse the repository at this point in the history
  • Loading branch information
cnlangzi committed Apr 9, 2024
1 parent 4b925ad commit 7d264d0
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 15 deletions.
5 changes: 5 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ var (
defaultDHTMobile = "auth:mobile"
)

var (
noSession Session
noProfileData ProfileData
)

type Auth struct {
db *sqle.DB
prefix string
Expand Down
53 changes: 52 additions & 1 deletion auth_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func (a *Auth) createUserProfile(ctx context.Context, tx *sqle.Tx, userID shardi
buf, _ := json.Marshal(ProfileData{
Email: email,
Mobile: mobile,
TKey: key.String(),
TKey: key.Secret(),
})

if a.aesKey == nil {
Expand Down Expand Up @@ -602,3 +602,54 @@ func (a *Auth) createUserMobile(ctx context.Context, tx *sqle.Tx, userID shardid

return nil
}

func (a *Auth) getUserProfileData(ctx context.Context, userID shardid.ID) (ProfileData, error) {
var data string
err := a.db.On(userID).
QueryRowBuilder(ctx, a.createBuilder().
Select("<prefix>user_profile", "data").
Where("user_id = {user_id}").
Param("user_id", userID)).
Scan(&data)

if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return noProfileData, ErrProfileNotFound
}
a.logger.Error("auth: getUserProfileData",
slog.String("tag", "db"),
slog.Int64("user_id", userID.Int64),
slog.Any("err", err))
return noProfileData, ErrBadDatabase
}

if data == "" {
return noProfileData, ErrProfileNotFound
}

var pd ProfileData

if a.aesKey != nil {
data, err = decryptText(data, a.aesKey)
if err != nil {
a.logger.Error("auth: getUserProfileData",
slog.String("step", "decryptText"),
slog.String("tag", "crypto"),
slog.String("text", data),
slog.Any("err", err))

return noProfileData, ErrUnknown
}
}

err = json.Unmarshal([]byte(data), &pd)
if err != nil {
a.logger.Error("auth: getUserProfileData",
slog.String("step", "json"),
slog.Int64("user_id", userID.Int64),
slog.Any("err", err))
return noProfileData, ErrUnknown
}

return pd, nil
}
56 changes: 46 additions & 10 deletions auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"context"
"errors"

"github.com/pquerna/otp/totp"
"github.com/yaitoo/sqle/shardid"
)

// SignIn sign in with email and password.
func (a *Auth) SignIn(ctx context.Context, email, passwd string, option LoginOption) (Session, error) {
var (
s Session
u User
err error
)
Expand All @@ -22,25 +22,42 @@ func (a *Auth) SignIn(ctx context.Context, email, passwd string, option LoginOpt
return a.createSession(ctx, u.ID)
}

return s, ErrPasswdNotMatched
return noSession, ErrPasswdNotMatched
}

if option.CreateIfNotExists && errors.Is(err, ErrEmailNotFound) {
u, err = a.createLoginWithEmail(ctx, email, passwd, option.FirstName, option.LastName)
if err != nil {
return s, err
return noSession, err
}

return a.createSession(ctx, u.ID)
}

return s, err
return noSession, err

}

// SignInWithOTP sign in with email and otp.
func (a *Auth) SignInWithOTP(ctx context.Context, email, otp string, option LoginOption) (*Session, error) {
return nil, nil
func (a *Auth) SignInWithOTP(ctx context.Context, email, otp string) (Session, error) {

u, err := a.getUserByEmail(ctx, email)

if err != nil {
return noSession, ErrEmailNotFound
}

pd, err := a.getUserProfileData(ctx, u.ID)
if err != nil {
return noSession, err
}

if !totp.Validate(otp, pd.TKey) {
return noSession, ErrOTPNotMatched
}

return a.createSession(ctx, u.ID)

}

// SignInWithCode sign in with email and code.
Expand All @@ -51,7 +68,6 @@ func (a *Auth) SignInWithCode(ctx context.Context, email, code string, option Lo
// SignInMobile sign in with mobile and password.
func (a *Auth) SignInMobile(ctx context.Context, mobile, passwd string, option LoginOption) (Session, error) {
var (
s Session
u User
err error
)
Expand All @@ -63,19 +79,39 @@ func (a *Auth) SignInMobile(ctx context.Context, mobile, passwd string, option L
return a.createSession(ctx, u.ID)
}

return s, ErrPasswdNotMatched
return noSession, ErrPasswdNotMatched
}

if option.CreateIfNotExists && errors.Is(err, ErrMobileNotFound) {
u, err = a.createLoginWithMobile(ctx, mobile, passwd, option.FirstName, option.LastName)
if err != nil {
return s, err
return noSession, err
}

return a.createSession(ctx, u.ID)
}

return s, err
return noSession, err
}

// SignInMobileWithOTP sign in with mobile and otp.
func (a *Auth) SignInMobileWithOTP(ctx context.Context, mobile, otp string) (Session, error) {
u, err := a.getUserByMobile(ctx, mobile)

if err != nil {
return noSession, ErrMobileNotFound
}

pd, err := a.getUserProfileData(ctx, u.ID)
if err != nil {
return noSession, err
}

if !totp.Validate(otp, pd.TKey) {
return noSession, ErrOTPNotMatched
}

return a.createSession(ctx, u.ID)
}

// SignInMobileWithCode sign in with mobile and code.
Expand Down
File renamed without changes.
148 changes: 148 additions & 0 deletions auth_sign_in_with_otp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package auth

import (
"context"
"testing"
"time"

"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/require"
)

func TestSignInWithOTP(t *testing.T) {

authTest := createAuthTest("./tests_sign_in_with_otp.db")

tests := []struct {
name string
setup func(r *require.Assertions) string
email string
wantedErr error
assert func(r *require.Assertions, s Session)
}{
{
name: "email_not_found_should_not_work",
email: "not_found@sign_in_with_otp.com",
setup: func(r *require.Assertions) string {
return ""
},
wantedErr: ErrEmailNotFound,
},
{
name: "otp_not_matched_should_not_work",
email: "otp_not_matched@sign_in_with_otp.com",
wantedErr: ErrOTPNotMatched,
setup: func(r *require.Assertions) string {
_, err := authTest.createLoginWithEmail(context.Background(), "otp_not_matched@sign_in_with_otp.com", "abc123", "", "")
r.NoError(err)

return ""
},
},
{
name: "otp_should_work",
email: "otp@sign_in_with_otp.com",
setup: func(r *require.Assertions) string {
u, err := authTest.createLoginWithEmail(context.Background(), "otp@sign_in_with_otp.com", "abc123", "", "")
r.NoError(err)

pd, err := authTest.getUserProfileData(context.Background(), u.ID)
r.NoError(err)

code, err := totp.GenerateCode(pd.TKey, time.Now())
r.NoError(err)
return code

},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := require.New(t)

code := test.setup(r)

s, err := authTest.SignInWithOTP(context.TODO(), test.email, code)
if test.wantedErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, test.wantedErr)
}

if test.assert != nil {
test.assert(r, s)
}

})
}
}

func TestSignInMobileWithOTP(t *testing.T) {

authTest := createAuthTest("./tests_sign_in_mobile_with_otp.db")

tests := []struct {
name string
setup func(r *require.Assertions) string
mobile string
wantedErr error
assert func(r *require.Assertions, s Session)
}{
{
name: "mobile_not_found_should_not_work",
mobile: "1+111222333",
setup: func(r *require.Assertions) string {
return ""
},
wantedErr: ErrMobileNotFound,
},
{
name: "otp_not_matched_should_not_work",
mobile: "1+222333444",
wantedErr: ErrOTPNotMatched,
setup: func(r *require.Assertions) string {
_, err := authTest.createLoginWithMobile(context.Background(), "1+222333444", "abc123", "", "")
r.NoError(err)

return ""
},
},
{
name: "otp_should_work",
mobile: "1+333444555",
setup: func(r *require.Assertions) string {
u, err := authTest.createLoginWithMobile(context.Background(), "1+333444555", "abc123", "", "")
r.NoError(err)

pd, err := authTest.getUserProfileData(context.Background(), u.ID)
r.NoError(err)

code, err := totp.GenerateCode(pd.TKey, time.Now())
r.NoError(err)
return code

},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := require.New(t)

code := test.setup(r)

s, err := authTest.SignInMobileWithOTP(context.TODO(), test.mobile, code)
if test.wantedErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, test.wantedErr)
}

if test.assert != nil {
test.assert(r, s)
}

})
}
}
2 changes: 1 addition & 1 deletion crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func encryptText(plainText []byte, key []byte) (string, error) {
//Create a nonce. Nonce should be from GCM
nonce := make([]byte, aesGCM.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
return "", err
}

//Encrypt the data using aesGCM.Seal
Expand Down
10 changes: 7 additions & 3 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ var (
)

var (
ErrEmailNotFound = errors.New("auth: email_not_found")
ErrMobileNotFound = errors.New("auth: mobile_not_found")
ErrUserNotFound = errors.New("auth: user_not_found")
ErrEmailNotFound = errors.New("auth: email_not_found")
ErrMobileNotFound = errors.New("auth: mobile_not_found")
ErrUserNotFound = errors.New("auth: user_not_found")
ErrProfileNotFound = errors.New("auth: profile_not_found")

ErrPasswdNotMatched = errors.New("auth: passwd_not_matched")

ErrOTPNotMatched = errors.New("auth: otp_not_matched")
)

0 comments on commit 7d264d0

Please sign in to comment.