@@ -118,7 +118,27 @@ func (s *RPC) RegisterSession(
118
118
return nil , nil , proto .ErrWebrpcInternalError .WithCausef ("failed to retrieve account: %w" , err )
119
119
}
120
120
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.
121
123
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.
122
142
if ! intentTyped .Data .ForceCreateAccount && ident .Email != "" {
123
143
accs , err := s .Accounts .ListByEmail (ctx , tntData .ProjectID , ident .Email )
124
144
if err != nil {
@@ -135,7 +155,7 @@ func (s *RPC) RegisterSession(
135
155
136
156
accData := & proto.AccountData {
137
157
ProjectID : tntData .ProjectID ,
138
- UserID : fmt . Sprintf ( "%d|%s" , tntData . ProjectID , sessionHash ) ,
158
+ UserID : userID ,
139
159
Identity : ident .String (),
140
160
CreatedAt : time .Now (),
141
161
}
@@ -144,6 +164,7 @@ func (s *RPC) RegisterSession(
144
164
return nil , nil , proto .ErrWebrpcInternalError .WithCausef ("encrypting account data: %w" , err )
145
165
}
146
166
167
+ // This account is inserted to the DB later once the WaaS API returns successfully.
147
168
account = & data.Account {
148
169
ProjectID : tntData .ProjectID ,
149
170
Identity : data .Identity (ident ),
@@ -157,11 +178,17 @@ func (s *RPC) RegisterSession(
157
178
}
158
179
}
159
180
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.
160
186
res , err := s .Wallets .RegisterSession (waasapi .Context (ctx ), account .UserID , waasapi .ConvertToAPIIntent (intent ))
161
187
if err != nil {
162
188
return nil , nil , proto .ErrWebrpcInternalError .WithCausef ("registering session with WaaS API: %w" , err )
163
189
}
164
190
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.
165
192
if ! accountFound || (ident .Email != "" && account .Email != ident .Email ) {
166
193
account .Email = ident .Email
167
194
if err := s .Accounts .Put (ctx , account ); err != nil {
0 commit comments