Skip to content
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

Fix EmailAlreadyInUse issues #56

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions proto/authenticator.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions proto/authenticator.ridl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ error 1000 Unauthorized "Unauthorized access" HTTP 401
error 1001 TenantNotFound "Tenant not found" HTTP 404

error 2000 EmailAlreadyInUse "Could not create account as the email is already in use" HTTP 409
error 2001 AccountAlreadyLinked "Could not link account as it is linked to another wallet" HTTP 409
error 2002 ProofVerificationFailed "The authentication proof could not be verified" HTTP 400
error 2003 AnswerIncorrect "The provided answer is incorrect" HTTP 400
error 2004 ChallengeExpired "The challenge has expired" HTTP 400


##
Expand Down
14 changes: 9 additions & 5 deletions proto/clients/authenticator.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 62 additions & 2 deletions proto/clients/authenticator.gen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// sequence-waas-authenticator v0.1.0 0162a6fb35dd49d6f4d924ebce4bceb847479f3a
// sequence-waas-authenticator v0.1.0 12981425046f42285c75880877768ce81be8b3f9
// --
// Code generated by [email protected] with typescript generator. DO NOT EDIT.
//
Expand All @@ -12,7 +12,7 @@ export const WebRPCVersion = "v1"
export const WebRPCSchemaVersion = "v0.1.0"

// Schema hash generated from your RIDL schema
export const WebRPCSchemaHash = "0162a6fb35dd49d6f4d924ebce4bceb847479f3a"
export const WebRPCSchemaHash = "12981425046f42285c75880877768ce81be8b3f9"

//
// Types
Expand Down Expand Up @@ -709,6 +709,58 @@ export class EmailAlreadyInUseError extends WebrpcError {
}
}

export class AccountAlreadyLinkedError extends WebrpcError {
constructor(
name: string = 'AccountAlreadyLinked',
code: number = 2001,
message: string = 'Could not link account as it is linked to another wallet',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, AccountAlreadyLinkedError.prototype)
}
}

export class ProofVerificationFailedError extends WebrpcError {
constructor(
name: string = 'ProofVerificationFailed',
code: number = 2002,
message: string = 'The authentication proof could not be verified',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, ProofVerificationFailedError.prototype)
}
}

export class AnswerIncorrectError extends WebrpcError {
constructor(
name: string = 'AnswerIncorrect',
code: number = 2003,
message: string = 'The provided answer is incorrect',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, AnswerIncorrectError.prototype)
}
}

export class ChallengeExpiredError extends WebrpcError {
constructor(
name: string = 'ChallengeExpired',
code: number = 2004,
message: string = 'The challenge has expired',
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, ChallengeExpiredError.prototype)
}
}


export enum errors {
WebrpcEndpoint = 'WebrpcEndpoint',
Expand All @@ -725,6 +777,10 @@ export enum errors {
Unauthorized = 'Unauthorized',
TenantNotFound = 'TenantNotFound',
EmailAlreadyInUse = 'EmailAlreadyInUse',
AccountAlreadyLinked = 'AccountAlreadyLinked',
ProofVerificationFailed = 'ProofVerificationFailed',
AnswerIncorrect = 'AnswerIncorrect',
ChallengeExpired = 'ChallengeExpired',
}

const webrpcErrorByCode: { [code: number]: any } = {
Expand All @@ -742,6 +798,10 @@ const webrpcErrorByCode: { [code: number]: any } = {
[1000]: UnauthorizedError,
[1001]: TenantNotFoundError,
[2000]: EmailAlreadyInUseError,
[2001]: AccountAlreadyLinkedError,
patrislav marked this conversation as resolved.
Show resolved Hide resolved
[2002]: ProofVerificationFailedError,
[2003]: AnswerIncorrectError,
[2004]: ChallengeExpiredError,
}

export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise<Response>
Expand Down
45 changes: 32 additions & 13 deletions rpc/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rpc

import (
"context"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -55,16 +56,16 @@ func (s *RPC) federateAccount(
tntData := tenant.FromContext(ctx)

if intent.Data.SessionID != sess.ID {
return nil, fmt.Errorf("sessionId mismatch")
return nil, proto.ErrWebrpcBadRequest.WithCausef("sessionId mismatch")
}

authProvider, err := s.getAuthProvider(intent.Data.IdentityType)
if err != nil {
return nil, fmt.Errorf("get auth provider: %w", err)
return nil, proto.ErrWebrpcBadRequest.WithCausef("get auth provider: %w", err)
}

if intent.Data.IdentityType == intents.IdentityType_Guest {
return nil, fmt.Errorf("cannot federate a guest account")
return nil, proto.ErrWebrpcBadRequest.WithCausef("cannot federate a guest account")
}

var verifCtx *proto.VerificationContext
Expand All @@ -75,34 +76,52 @@ func (s *RPC) federateAccount(
}
dbVerifCtx, found, err := s.VerificationContexts.Get(ctx, authID)
if err != nil {
return nil, fmt.Errorf("getting verification context: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("getting verification context: %w", err)
}
if found && dbVerifCtx != nil {
verifCtx, _, err = crypto.DecryptData[*proto.VerificationContext](ctx, dbVerifCtx.EncryptedKey, dbVerifCtx.Ciphertext, tntData.KMSKeys)
if err != nil {
return nil, fmt.Errorf("decrypting verification context data: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("decrypting verification context data: %w", err)
}

if time.Now().After(verifCtx.ExpiresAt) {
return nil, fmt.Errorf("auth session expired")
return nil, proto.ErrChallengeExpired
}

if !dbVerifCtx.CorrespondsTo(verifCtx) {
return nil, fmt.Errorf("malformed verification context data")
return nil, proto.ErrWebrpcInternalError.WithCausef("malformed verification context data")
}
}

ident, err := authProvider.Verify(ctx, verifCtx, sess.ID, intent.Data.Answer)
if err != nil {
return nil, fmt.Errorf("verifying identity: %w", err)
if verifCtx != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo... verifCtx ... I think call it .. verifyCtx ..?

also, what is the code doing, and why..? it looks like its making multiple attempts to encrypt and update something..? is the underlining service prone to failing / timing out..?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually short for "verification context" 😄

The whole block comes directly from https://github.com/0xsequence/waas-authenticator/blob/master/rpc/sessions.go#L86.

For context, this is executed whenever the client/user responds with an answer to a challenge (e.g. after inputting code from email OR in commit/reveal flow, when the client provides the revealed auth proof). If we failed answer verification (e.g. incorrect code input by the user) AND if there is an existing verification context (always true for email flow), then we increment the attempts count in the verification context's encrypted data, re-encrypt that data, and store it back in the database.

This lets us keep track of failed attempts and prevent the user from trying every single code combination there is 😄

The one here is the same but for account federation/linking instead of authentication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood :) thanks for explaining, sounds good

now := time.Now()
verifCtx.Attempts += 1
verifCtx.LastAttemptAt = &now

encryptedKey, algorithm, ciphertext, err := crypto.EncryptData(ctx, att, tntData.KMSKeys[0], verifCtx)
if err != nil {
return nil, proto.ErrWebrpcInternalError.WithCausef("encrypt data: %w", err)
}
if err := s.VerificationContexts.UpdateData(ctx, dbVerifCtx, encryptedKey, algorithm, ciphertext); err != nil {
return nil, proto.ErrWebrpcInternalError.WithCausef("update verification context: %w", err)
}
}

var wErr proto.WebRPCError
if errors.As(err, &wErr) {
return nil, wErr
}
return nil, proto.ErrAnswerIncorrect.WithCausef("verifying answer: %w", err)
}

_, found, err = s.Accounts.Get(ctx, tntData.ProjectID, ident)
if err != nil {
return nil, fmt.Errorf("retrieving account: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("retrieving account: %w", err)
}
if found {
return nil, fmt.Errorf("account already exists")
return nil, proto.ErrAccountAlreadyLinked
}

accData := &proto.AccountData{
Expand All @@ -114,7 +133,7 @@ func (s *RPC) federateAccount(

encryptedKey, algorithm, ciphertext, err := crypto.EncryptData(ctx, att, tntData.KMSKeys[0], accData)
if err != nil {
return nil, fmt.Errorf("encrypting account data: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("encrypting account data: %w", err)
}

account := &data.Account{
Expand All @@ -130,11 +149,11 @@ func (s *RPC) federateAccount(
}

if _, err := s.Wallets.FederateAccount(waasapi.Context(ctx), account.UserID, waasapi.ConvertToAPIIntent(intent.ToIntent())); err != nil {
return nil, fmt.Errorf("creating account with WaaS API: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("creating account with WaaS API: %w", err)
}

if err := s.Accounts.Put(ctx, account); err != nil {
return nil, fmt.Errorf("save account: %w", err)
return nil, proto.ErrWebrpcInternalError.WithCausef("save account: %w", err)
}

outAcc := &intents.Account{
Expand Down
4 changes: 2 additions & 2 deletions rpc/auth/email/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ func extractVerifier(verifier string) (emailAddress string, sessionID string, er
if !found {
return "", "", fmt.Errorf("invalid verifier")
}
return normalizeEmail(emailAddress), sessionID, nil
return Normalize(emailAddress), sessionID, nil
}

func normalizeEmail(email string) string {
func Normalize(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}

Expand Down
54 changes: 28 additions & 26 deletions rpc/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,35 +185,37 @@ func TestGuestAuth(t *testing.T) {
ctx, err := proto.WithHTTPRequestHeaders(context.Background(), header)
require.NoError(t, err)

sessWallet, err := ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
signingSession := intents.NewSessionP256K1(sessWallet)
for i := 0; i < 10; i++ {
sessWallet, err := ethwallet.NewWalletFromRandomEntropy()
require.NoError(t, err)
signingSession := intents.NewSessionP256K1(sessWallet)

initiateAuth := generateSignedIntent(t, intents.IntentName_initiateAuth, intents.IntentDataInitiateAuth{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
}, signingSession)
initRes, err := c.SendIntent(ctx, initiateAuth)
require.NoError(t, err)
assert.Equal(t, proto.IntentResponseCode_authInitiated, initRes.Code)
initiateAuth := generateSignedIntent(t, intents.IntentName_initiateAuth, intents.IntentDataInitiateAuth{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
}, signingSession)
initRes, err := c.SendIntent(ctx, initiateAuth)
require.NoError(t, err)
assert.Equal(t, proto.IntentResponseCode_authInitiated, initRes.Code)

b, err := json.Marshal(initRes.Data)
require.NoError(t, err)
var initResData intents.IntentResponseAuthInitiated
require.NoError(t, json.Unmarshal(b, &initResData))
b, err := json.Marshal(initRes.Data)
require.NoError(t, err)
var initResData intents.IntentResponseAuthInitiated
require.NoError(t, json.Unmarshal(b, &initResData))

answer := crypto.Keccak256([]byte(*initResData.Challenge + signingSession.SessionID()))
registerSession := generateSignedIntent(t, intents.IntentName_openSession, intents.IntentDataOpenSession{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
Answer: hexutil.Encode(answer),
}, signingSession)
sess, registerRes, err := c.RegisterSession(ctx, registerSession, "Friendly name")
require.NoError(t, err)
assert.Equal(t, "Guest:"+signingSession.SessionID(), sess.Identity.String())
assert.Equal(t, proto.IntentResponseCode_sessionOpened, registerRes.Code)
answer := crypto.Keccak256([]byte(*initResData.Challenge + signingSession.SessionID()))
registerSession := generateSignedIntent(t, intents.IntentName_openSession, intents.IntentDataOpenSession{
SessionID: signingSession.SessionID(),
IdentityType: intents.IdentityType_Guest,
Verifier: signingSession.SessionID(),
Answer: hexutil.Encode(answer),
}, signingSession)
sess, registerRes, err := c.RegisterSession(ctx, registerSession, "Friendly name")
require.NoError(t, err)
assert.Equal(t, "Guest:"+signingSession.SessionID(), sess.Identity.String())
assert.Equal(t, proto.IntentResponseCode_sessionOpened, registerRes.Code)
}
})
}

Expand Down
Loading
Loading