Skip to content

Commit 357d38a

Browse files
committed
feat: add new method to ingest the OAuth token from social login
and try to use the token for automatic pairing after SRP login
1 parent 31b7973 commit 357d38a

File tree

4 files changed

+137
-5
lines changed

4 files changed

+137
-5
lines changed

packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import type {
1313
import type { HandleSnapRequest } from '@metamask/snaps-controllers';
1414

1515
import {
16-
createSnapPublicKeyRequest,
1716
createSnapAllPublicKeysRequest,
17+
createSnapPublicKeyRequest,
1818
createSnapSignMessageRequest,
1919
} from './auth-snap-requests';
2020
import type { LoginResponse, SRPInterface, UserProfile } from '../../sdk';
@@ -32,6 +32,8 @@ const controllerName = 'AuthenticationController';
3232
export type AuthenticationControllerState = {
3333
isSignedIn: boolean;
3434
srpSessionData?: Record<string, LoginResponse>;
35+
socialPairingToken?: string;
36+
socialPairingDone?: boolean;
3537
};
3638
export const defaultState: AuthenticationControllerState = {
3739
isSignedIn: false,
@@ -45,6 +47,14 @@ const metadata: StateMetadata<AuthenticationControllerState> = {
4547
persist: true,
4648
anonymous: false,
4749
},
50+
socialPairingToken: {
51+
persist: true,
52+
anonymous: true,
53+
},
54+
socialPairingDone: {
55+
persist: true,
56+
anonymous: true,
57+
},
4858
};
4959

5060
// Messenger Actions
@@ -60,6 +70,7 @@ type ActionsObj = CreateActionsObj<
6070
| 'getBearerToken'
6171
| 'getSessionProfile'
6272
| 'isSignedIn'
73+
| 'ingestSocialLoginToken'
6374
>;
6475
export type Actions =
6576
| ActionsObj[keyof ActionsObj]
@@ -270,20 +281,27 @@ export default class AuthenticationController extends BaseController<
270281
const allPublicKeys = await this.#snapGetAllPublicKeys();
271282
const accessTokens = [];
272283

273-
// We iterate sequentially in order to be sure that the first entry
284+
// We iterate sequentially to be sure that the first entry
274285
// is the primary SRP LoginResponse.
275286
for (const [entropySourceId] of allPublicKeys) {
276287
const accessToken = await this.#auth.getAccessToken(entropySourceId);
277288
accessTokens.push(accessToken);
278289
}
279290

291+
// don't await for the pairing to finish
292+
this.#tryPairingWithSocialToken().catch((_) => {
293+
// don't care
294+
});
295+
280296
return accessTokens;
281297
}
282298

283299
public performSignOut(): void {
284300
this.update((state) => {
285301
state.isSignedIn = false;
286302
state.srpSessionData = undefined;
303+
state.socialPairingToken = undefined;
304+
state.socialPairingDone = false;
287305
});
288306
}
289307

@@ -318,6 +336,47 @@ export default class AuthenticationController extends BaseController<
318336
return this.state.isSignedIn;
319337
}
320338

339+
/**
340+
* Stores a social login JWT token in controller state temporarily
341+
* until it can be used for pairing.
342+
* This token will automatically be removed from state after
343+
* successful pairing or during a sign-out request.
344+
*
345+
* @param token - The JWT token from seedless onboarding OAuth flow
346+
*/
347+
public ingestSocialLoginToken(token: string) {
348+
this.update((state) => {
349+
state.socialPairingToken = token;
350+
state.socialPairingDone = false;
351+
});
352+
}
353+
354+
async #tryPairingWithSocialToken(): Promise<void> {
355+
console.log(`GIGEL: trying to pair with seedless token`);
356+
const { socialPairingToken, socialPairingDone } = this.state;
357+
if (socialPairingDone || !socialPairingToken) {
358+
console.log(`GIGEL: pairing conditions not met`);
359+
return;
360+
}
361+
362+
try {
363+
console.log(`GIGEL: pairing with seedless token ${socialPairingToken}`);
364+
if (await this.#auth.pairSocialIdentifier(socialPairingToken)) {
365+
console.log(`GIGEL: successfully paired with seedless onboarding token`);
366+
this.update((state) => {
367+
state.socialPairingDone = true;
368+
state.socialPairingToken = undefined;
369+
});
370+
} else {
371+
console.log(`GIGEL: pairing with seedless token failed`);
372+
// ignore the error
373+
}
374+
} catch (error) {
375+
console.error('GIGEL: Failed to pair identifiers:', error);
376+
// ignore the error
377+
}
378+
}
379+
321380
/**
322381
* Returns the auth snap public key.
323382
*

packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import type { Eip1193Provider } from 'ethers';
22

3-
import { authenticate, authorizeOIDC, getNonce } from './services';
3+
import {
4+
authenticate,
5+
authorizeOIDC,
6+
getNonce,
7+
PAIR_SOCIAL_IDENTIFIER,
8+
} from './services';
49
import type {
510
AuthConfig,
611
AuthSigningOptions,
712
AuthStorageOptions,
8-
AuthType,
13+
ErrorMessage,
914
IBaseAuth,
1015
LoginResponse,
1116
UserProfile,
1217
} from './types';
18+
import { AuthType } from './types';
1319
import type { MetaMetricsAuth } from '../../shared/types/services';
14-
import { ValidationError } from '../errors';
20+
import { PairError, ValidationError } from '../errors';
1521
import { getMetaMaskProviderEIP6963 } from '../utils/eip-6963-metamask-provider';
1622
import {
1723
MESSAGE_SIGNING_SNAP,
@@ -201,4 +207,63 @@ export class SRPJwtBearerAuth implements IBaseAuth {
201207
): `metamask:${string}:${string}` {
202208
return `metamask:${nonce}:${publicKey}` as const;
203209
}
210+
211+
async pairSocialIdentifier(jwt: string): Promise<boolean> {
212+
console.log(
213+
`GIGEL: pairing primary SRP with social token ${jwt}`,
214+
);
215+
216+
const { env, platform } = this.#config;
217+
218+
// Exchange the social token with an access token
219+
console.log(`GIGEL: exchanging social token for access token`);
220+
const tokenResponse = await authorizeOIDC(jwt, env, platform);
221+
console.log(`GIGEL: obtained access token ${tokenResponse.accessToken}`);
222+
223+
// Prepare the SRP signature
224+
const identifier = await this.getIdentifier();
225+
const profile = await this.getUserProfile();
226+
const n = await getNonce(profile.profileId, env);
227+
console.log(
228+
`GIGEL: pairing social token with profile ${profile.profileId} with nonce ${n.nonce}`,
229+
);
230+
const raw = `metamask:${n.nonce}:${identifier}`;
231+
const sig = await this.signMessage(raw);
232+
const primaryIdentifierSignature = {
233+
signature: sig,
234+
raw_message: raw,
235+
identifier_type: AuthType.SRP,
236+
encrypted_storage_key: '', // Not yet part of this flow, so we leave it empty
237+
};
238+
239+
const pairUrl = new URL(PAIR_SOCIAL_IDENTIFIER(env));
240+
241+
try {
242+
const response = await fetch(pairUrl, {
243+
method: 'POST',
244+
headers: {
245+
'Content-Type': 'application/json',
246+
Authorization: `Bearer ${tokenResponse.accessToken}`,
247+
},
248+
body: JSON.stringify({
249+
nonce: n.nonce,
250+
login: primaryIdentifierSignature,
251+
}),
252+
});
253+
254+
if (!response.ok) {
255+
const responseBody = (await response.json()) as ErrorMessage;
256+
throw new Error(
257+
`HTTP error message: ${responseBody.message}, error: ${responseBody.error}`,
258+
);
259+
}
260+
} catch (e) {
261+
/* istanbul ignore next */
262+
const errorMessage =
263+
e instanceof Error ? e.message : JSON.stringify(e ?? '');
264+
throw new PairError(`unable to pair identifiers: ${errorMessage}`);
265+
}
266+
267+
return false;
268+
}
204269
}

packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export const NONCE_URL = (env: Env) =>
1616
export const PAIR_IDENTIFIERS = (env: Env) =>
1717
`${getEnvUrls(env).authApiUrl}/api/v2/identifiers/pair`;
1818

19+
export const PAIR_SOCIAL_IDENTIFIER = (env: Env) =>
20+
`${getEnvUrls(env).authApiUrl}/api/v2/identifiers/pair/social`;
21+
1922
export const OIDC_TOKEN_URL = (env: Env) =>
2023
`${getEnvUrls(env).oidcApiUrl}/oauth2/token`;
2124

packages/profile-sync-controller/src/sdk/authentication.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ export class JwtBearerAuth implements SIWEInterface, SRPInterface {
109109
await pairIdentifiers(n.nonce, logins, accessToken, this.#env);
110110
}
111111

112+
async pairSocialIdentifier(jwt: string): Promise<boolean> {
113+
this.#assertSRP(this.#type, this.#sdk);
114+
return await this.#sdk.pairSocialIdentifier(jwt);
115+
}
116+
112117
prepare(signer: {
113118
address: string;
114119
chainId: number;

0 commit comments

Comments
 (0)