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

rpc: reject RegisterSession when creating an account for existing user #76

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 19 additions & 0 deletions data/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ func (t *AccountTable) Get(ctx context.Context, projectID uint64, identity proto
return &acct, true, nil
}

func (t *AccountTable) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
input := &dynamodb.QueryInput{
TableName: &t.tableARN,
IndexName: &t.indices.ByUserID,
KeyConditionExpression: aws.String("UserID = :userID"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":userID": &types.AttributeValueMemberS{Value: userID},
},
Limit: aws.Int32(1),
}

out, err := t.db.Query(ctx, input)
if err != nil {
return false, fmt.Errorf("Query: %w", err)
}

return len(out.Items) > 0, nil
}

// ListByUserID returns all Accounts of a given user.
//
// TODO: implement pagination.
Expand Down
31 changes: 29 additions & 2 deletions rpc/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,27 @@ func (s *RPC) RegisterSession(
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("failed to retrieve account: %w", err)
}

// If there's no account for this identity then we know it's used for the first time. Prepare the account to be
// created at the end of the process.
if !accountFound {
// The user ID is deterministic and derived from the first session ever used by the user.
userID := fmt.Sprintf("%d|%s", tntData.ProjectID, sessionHash)

// If the user already has an account (of a different identity), we need to reject this intent. Otherwise, it
// would result in an accidental account federation as a new identity is connected to an existing user through
// unintended method.
userExists, err := s.Accounts.ExistsByUserID(ctx, userID)
if err != nil {
return nil, nil, proto.ErrWebrpcInternalError.WithCausef("failed to check if user exists: %w", err)
}
if userExists {
return nil, nil, proto.ErrWebrpcBadRequest.WithCausef("user already exists")
}

// Warn the user if another account already exists with the same email address. This allows them to go back and
// sign in using the other identity and then use account federation to add this one.
// Otherwise, this would result in a creation of a new user and thus a separate wallet and that's very unlikely
// to be the user's intent.
if !intentTyped.Data.ForceCreateAccount && ident.Email != "" {
accs, err := s.Accounts.ListByEmail(ctx, tntData.ProjectID, ident.Email)
if err != nil {
Expand All @@ -135,7 +155,7 @@ func (s *RPC) RegisterSession(

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

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

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

// Insert an account if it's new OR update it with a fresh email if it differs from what we have in the DB.
if !accountFound || (ident.Email != "" && account.Email != ident.Email) {
account.Email = ident.Email
if err := s.Accounts.Put(ctx, account); err != nil {
Expand Down Expand Up @@ -264,7 +291,7 @@ func (s *RPC) initiateAuth(

_, err = s.Wallets.InitiateAuth(waasapi.Context(ctx), waasapi.ConvertToAPIIntent(intent.ToIntent()), answer, challenge)
if err != nil {
return fmt.Errorf("initiating auth with WaaS API: %m", err)
return fmt.Errorf("initiating auth with WaaS API: %w", err)
}

encryptedKey, algorithm, ciphertext, err := crypto.EncryptData(ctx, att, tnt.KMSKeys[0], verifCtx)
Expand Down
Loading