-
Notifications
You must be signed in to change notification settings - Fork 673
SMQ-3093 - User email verification #3101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
bd2650c to
a7e5b8a
Compare
8792374 to
c47b711
Compare
c47b711 to
9160a8b
Compare
8565ae5 to
5c0cce2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI is failing.
1bdf4a4 to
fbcb871
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3101 +/- ##
==========================================
+ Coverage 35.15% 43.81% +8.65%
==========================================
Files 323 212 -111
Lines 47509 35520 -11989
==========================================
- Hits 16704 15562 -1142
+ Misses 29977 19206 -10771
+ Partials 828 752 -76 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
users/service.go
Outdated
|
|
||
| const ( | ||
| verificationTokenExpiryDuration = 24 * time.Hour | ||
| verificationTokenSeparator = "." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove if unused.
users/service.go
Outdated
| "github.com/absmach/supermq/pkg/policies" | ||
| ) | ||
|
|
||
| const ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once you remove unused const, make this inline and rename const verificationTokenDuration.
| if err != nil { | ||
| if err == repoerr.ErrNotFound { | ||
| return nil | ||
| } | ||
| return err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for nested if:
if err == repoerr.ErrNotFound {
return nil
}
if err != nil {
return err
}Why do we return nil in case of notFound?
users/service.go
Outdated
| } | ||
|
|
||
| token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: dbUser.ID, UserRole: uint32(dbUser.Role + 1), Type: uint32(smqauth.AccessKey)}) | ||
| token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: dbUser.ID, UserRole: uint32(dbUser.Role + 1), Type: uint32(smqauth.AccessKey), Verified: dbUser.Verified}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not related to this PR, but Role+1 is too cryptic.
users/service.go
Outdated
|
|
||
| type tokenPayload struct { | ||
| UserID string `json:"user_id"` | ||
| Email string `json:"email"` | ||
| Token string `json:"token"` | ||
| } | ||
|
|
||
| func generateVerificationToken(userID, email string) (string, error) { | ||
| randomBytes := make([]byte, 32) | ||
| if _, err := rand.Read(randomBytes); err != nil { | ||
| return "", errors.Wrap(errFailedToEncodeToken, err) | ||
| } | ||
|
|
||
| payload := tokenPayload{ | ||
| UserID: userID, | ||
| Email: email, | ||
| Token: base64.URLEncoding.EncodeToString(randomBytes), | ||
| } | ||
|
|
||
| jsonBytes, err := json.Marshal(payload) | ||
| if err != nil { | ||
| return "", errors.Wrap(errFailedToEncodeToken, err) | ||
| } | ||
|
|
||
| return base64.URLEncoding.EncodeToString(jsonBytes), nil | ||
| } | ||
|
|
||
| func getUserIDFromToken(token string) (tokenPayload, error) { | ||
| decodedPayload, err := base64.URLEncoding.DecodeString(token) | ||
| if err != nil { | ||
| return tokenPayload{}, errors.Wrap(errFailedToDecodeToken, err) | ||
| } | ||
|
|
||
| var payload tokenPayload | ||
| if err := json.Unmarshal(decodedPayload, &payload); err != nil { | ||
| return tokenPayload{}, errInvalidTokenFormat | ||
| } | ||
|
|
||
| return payload, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to a separate file.
users/users.go
Outdated
| if u.VerificationTokenExpiresAt.IsZero() { | ||
| return true | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check if verificationToken is empty first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will think about this, What we should return if token is empty ?
users/users.go
Outdated
| UpdateToUserVerificationDetails(ctx context.Context, user User) (User, error) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update instead of UpdateTo.
|
|
||
| UpdateEmail(ctx context.Context, user User) (User, error) | ||
|
|
||
| UpdateRole(ctx context.Context, user User) (User, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add godoc comments.
|
|
||
| SendVerification(ctx context.Context, session authn.Session) error | ||
|
|
||
| VerifyEmail(ctx context.Context, verificationToken string) (User, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, add godoc.
| policyCall := policies.On("AddPolicy", context.Background(), mock.Anything).Return(tc.addPolicyErr) | ||
| policyCall1 := policies.On("DeletePolicyFilter", context.Background(), mock.Anything).Return(tc.deletePolicyErr) | ||
| repoCall1 := cRepo.On("Update", context.Background(), mock.Anything, mock.Anything).Return(tc.updateRoleResponse, tc.updateRoleErr) | ||
| repoCall1 := cRepo.On("UpdateRole", context.Background(), mock.Anything).Return(tc.updateRoleResponse, tc.updateRoleErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are missing tests for the added methods. Either do it now or open a ticket for the follow-up PR, since this one is already large.
users/postgres/init.go
Outdated
| Id: "clients_07", | ||
| Up: []string{ | ||
| `ALTER TABLE users | ||
| ADD COLUMN verified BOOLEAN DEFAULT FALSE, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this redundancy? Why don't we just use verified_at as nullable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. We use a verified (boolean) field in JWTs and for other condition checks. Relying only on verified_at would make the logic less clear.
For example, if we only had verified_at, we’d need to write something like:
verified: !verified_at.IsZero()By keeping a verified field, the code is cleaner and easier to understand.
| r.Post("/tokens/refresh", otelhttp.NewHandler(kithttp.NewServer( | ||
| refreshTokenEndpoint(svc), | ||
| decodeRefreshToken, | ||
| api.EncodeResponse, | ||
| opts..., | ||
| ), "refresh_token").ServeHTTP) | ||
| r.Patch("/{id}/email", otelhttp.NewHandler(kithttp.NewServer( | ||
| updateEmailEndpoint(svc), | ||
| decodeUpdateUserEmail, | ||
| api.EncodeResponse, | ||
| opts..., | ||
| ), "update_user_email").ServeHTTP) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this allowed for unverified user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
Change Email Endpoint:
If a user enters the wrong email address during signup, they must have the ability to correct it. Allowing unverified users to change their email ensures they can complete the verification process successfully. -
Refresh Token Endpoint:
The refresh token enables users to request new access tokens regardless of their verification status. Both access and refresh tokens include a verified field. Before verification, this field is false. Once the user completes verification and refreshes their token, the newly issued tokens will correctly reflect verified = true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Change Email Endpoint:
If a user enters the wrong email address during signup, they must have the ability to correct it. Allowing unverified users to change their email ensures they can complete the verification process successfully.
No need -> they can restart the procedure process with the correct email.
- Refresh Token Endpoint:
The refresh token enables users to request new access tokens regardless of their verification status. Both access and refresh tokens include a verified field. Before verification, this field is false. Once the user completes verification and refreshes their token, the newly issued tokens will correctly reflect verified = true
Why? Isn't it simpler to disable issuing tokens to who don't have VerifiedAt field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need -> they can restart the procedure process with the correct email.
- A username cannot be reused until the associated account is deleted.
- This ensures usernames can be properly recycled.
- It helps prevent the buildup of unwanted or unverified user accounts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can provide some basic operation without verification of email
This provides flexibility.
During this basic operation time, If user want to reset password, then they need to verify the email.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
CI is failing. |
cfa419f to
e476d42
Compare
users/postgres/init.go
Outdated
| Down: []string{ | ||
| `ALTER TABLE users | ||
| DROP COLUMN verified_at, | ||
| DROP COLUMN verification_token, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the token needed at all? Can we put user ID in JWT we send for verification?
users/postgres/init.go
Outdated
| `ALTER TABLE users | ||
| DROP COLUMN verified_at, | ||
| DROP COLUMN verification_token, | ||
| DROP COLUMN verification_token_expires_at;`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed if we are going to use JWT that already contains the expiration?
23fff36 to
01a5310
Compare
77b1d8a to
af725c0
Compare
users/postgres/init.go
Outdated
| email VARCHAR(254) NOT NULL, | ||
| otp VARCHAR(255), | ||
| created_at TIMESTAMPTZ, | ||
| expiry_at TIMESTAMPTZ, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use expires_at since we used created_at.
users/service.go
Outdated
| return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err) | ||
| } | ||
|
|
||
| oguv, err := svc.users.RetrieveUserVerification(ctx, ruv.UserID, ruv.Email) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oguv?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oguv - Original (OG) User Verification
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both ruvs and oguv are not descriptive names. Please update. Rename ruvs to user; and user to updatedUser or just updated. Rename ruvs to token, and ruv := UserVerification to uv := userVerification. Especially since this does not have to be registration (r) related.
| @@ -0,0 +1,131 @@ | |||
| // Copyright (c) Abstract Machines | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename to verifications, no need for users prefix.
d10bd83 to
fdca173
Compare
60a2907 to
9083ba0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI is failing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@arvindh123
I get no token/invalid token when I call sdk verify email method with the token gotten from link query params.
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
9083ba0 to
acf452b
Compare
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
users/service.go
Outdated
| return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err) | ||
| } | ||
|
|
||
| oguv, err := svc.users.RetrieveUserVerification(ctx, ruv.UserID, ruv.Email) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both ruvs and oguv are not descriptive names. Please update. Rename ruvs to user; and user to updatedUser or just updated. Rename ruvs to token, and ruv := UserVerification to uv := userVerification. Especially since this does not have to be registration (r) related.
groups/api/http/transport.go
Outdated
|
|
||
| // MakeHandler returns a HTTP handler for Groups API endpoints. | ||
| func MakeHandler(svc groups.Service, authn authn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string, idp supermq.IDProvider) *chi.Mux { | ||
| func MakeHandler(svc groups.Service, authnMW authn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string, idp supermq.IDProvider) *chi.Mux { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just use authn for this variable.
Signed-off-by: Arvindh <arvindh91@gmail.com>
2de1dd1
Signed-off-by: Arvindh <arvindh91@gmail.com>
users/service.go
Outdated
| var ruv UserVerification | ||
| if err := ruv.Decode(ruvs); err != nil { | ||
| func (svc service) VerifyEmail(ctx context.Context, token string) (User, error) { | ||
| var uv UserVerification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use received and stored instead, so we have stored.Match(received).
Signed-off-by: Arvindh <arvindh91@gmail.com>


What type of PR is this?
What does this do?
Which issue(s) does this PR fix/relate to?
Have you included tests for your changes?
Did you document any new/modified feature?
Notes