Skip to content

Commit 14f7daf

Browse files
authored
rpc: reject RegisterSession when creating an account for existing user (#76)
* rpc: reject RegisterSession when creating an account for existing user * add more explanation to the RegisterSession flow * small comment grammar fixes
1 parent 323bd5d commit 14f7daf

File tree

2 files changed

+47
-1
lines changed

2 files changed

+47
-1
lines changed

data/account.go

+19
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,25 @@ func (t *AccountTable) Get(ctx context.Context, projectID uint64, identity proto
128128
return &acct, true, nil
129129
}
130130

131+
func (t *AccountTable) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
132+
input := &dynamodb.QueryInput{
133+
TableName: &t.tableARN,
134+
IndexName: &t.indices.ByUserID,
135+
KeyConditionExpression: aws.String("UserID = :userID"),
136+
ExpressionAttributeValues: map[string]types.AttributeValue{
137+
":userID": &types.AttributeValueMemberS{Value: userID},
138+
},
139+
Limit: aws.Int32(1),
140+
}
141+
142+
out, err := t.db.Query(ctx, input)
143+
if err != nil {
144+
return false, fmt.Errorf("Query: %w", err)
145+
}
146+
147+
return len(out.Items) > 0, nil
148+
}
149+
131150
// ListByUserID returns all Accounts of a given user.
132151
//
133152
// TODO: implement pagination.

rpc/sessions.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,27 @@ func (s *RPC) RegisterSession(
118118
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("failed to retrieve account: %w", err)
119119
}
120120

121+
// If there's no account for this identity then we know it's used for the first time. Prepare the account to be
122+
// created at the end of the process.
121123
if !accountFound {
124+
// The user ID is deterministic and derived from the first session ever used by the user.
125+
userID := fmt.Sprintf("%d|%s", tntData.ProjectID, sessionHash)
126+
127+
// If the user already has an account (of a different identity), we need to reject this intent. Otherwise, it
128+
// would result in an accidental account federation as a new identity is connected to an existing user through
129+
// unintended method.
130+
userExists, err := s.Accounts.ExistsByUserID(ctx, userID)
131+
if err != nil {
132+
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("failed to check if user exists: %w", err)
133+
}
134+
if userExists {
135+
return nil, nil, proto.ErrWebrpcBadRequest.WithCausef("user already exists")
136+
}
137+
138+
// Warn the user if another account already exists with the same email address. This allows them to go back and
139+
// sign in using the other identity and then use account federation to add this one.
140+
// Otherwise, this would result in a creation of a new user and thus a separate wallet and that's very unlikely
141+
// to be the user's intent.
122142
if !intentTyped.Data.ForceCreateAccount && ident.Email != "" {
123143
accs, err := s.Accounts.ListByEmail(ctx, tntData.ProjectID, ident.Email)
124144
if err != nil {
@@ -135,7 +155,7 @@ func (s *RPC) RegisterSession(
135155

136156
accData := &proto.AccountData{
137157
ProjectID: tntData.ProjectID,
138-
UserID: fmt.Sprintf("%d|%s", tntData.ProjectID, sessionHash),
158+
UserID: userID,
139159
Identity: ident.String(),
140160
CreatedAt: time.Now(),
141161
}
@@ -144,6 +164,7 @@ func (s *RPC) RegisterSession(
144164
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("encrypting account data: %w", err)
145165
}
146166

167+
// This account is inserted to the DB later once the WaaS API returns successfully.
147168
account = &data.Account{
148169
ProjectID: tntData.ProjectID,
149170
Identity: data.Identity(ident),
@@ -157,11 +178,17 @@ func (s *RPC) RegisterSession(
157178
}
158179
}
159180

181+
// This calls the Sequence WaaS API. No changes to the DB were done yet up to this point, and we can only execute
182+
// them if the call is successful.
183+
// Note that if we return an error *after* this and *before* the DB is updated, we risk having data desync between
184+
// the enclave and the guard. This is a dangerous state to be in, so this call is expected -- and assumed -- to be
185+
// idempotent. Retrying it with the same input is safe.
160186
res, err := s.Wallets.RegisterSession(waasapi.Context(ctx), account.UserID, waasapi.ConvertToAPIIntent(intent))
161187
if err != nil {
162188
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("registering session with WaaS API: %w", err)
163189
}
164190

191+
// Insert an account if it's new OR update it with a fresh email if it differs from what we have in the DB.
165192
if !accountFound || (ident.Email != "" && account.Email != ident.Email) {
166193
account.Email = ident.Email
167194
if err := s.Accounts.Put(ctx, account); err != nil {

0 commit comments

Comments
 (0)