diff --git a/lit.config.json b/lit.config.json index 73f3bb540..4a740de6d 100644 --- a/lit.config.json +++ b/lit.config.json @@ -22,5 +22,8 @@ "HEX_TEST_MEMO": "0x4a532d53444b2054657374", "PKP_PUBKEY_2": "04a9c9a5db5472e6fac05ec001b804d3daa340a9b791e75dd52180312c7f0d4e806150473da6c785fadd8050ced5aada250146a97d928d0deb8b584bc7f169f10a", - "PKP_ETH_ADDRESS_2": "0xf26Bdd71BACf9D99F5739B4b1a2733E209248170" + "PKP_ETH_ADDRESS_2": "0xf26Bdd71BACf9D99F5739B4b1a2733E209248170", + "STYTCH_APP_ID": "project-test-de4e2690-1506-4cf5-8bce-44571ddaebc9", + "STYTCH_USER_ID": "user-test-68103e01-7468-4abf-83c8-885db2ca1c6c", + "STYTCH_TEST_TOKEN": "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWZiMjhlYmY2LTQ3NTMtNDdkMS1iMGUzLTRhY2NkMWE1MTc1NyIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LWRlNGUyNjkwLTE1MDYtNGNmNS04YmNlLTQ0NTcxZGRhZWJjOSJdLCJleHAiOjE2ODg1Njc0MTQsImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTlkZDI3ZGE1LTVjNjQtNDE5NS04NjdlLWIxNGE3MWE5M2MxMSIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA3LTA1VDE0OjI1OjE0WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA3LTA1VDE0OjI1OjE0WiIsImV4cGlyZXNfYXQiOiIyMDIzLTA5LTEzVDAxOjA1OjE0WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6Im90cCIsImRlbGl2ZXJ5X21ldGhvZCI6ImVtYWlsIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wNy0wNVQxNDoyNToxNFoiLCJlbWFpbF9mYWN0b3IiOnsiZW1haWxfaWQiOiJlbWFpbC10ZXN0LTAwMzZmM2YzLTQ0MjQtNDg2My1iYWQ3LTFkNGU3NTM1ZDJiMCIsImVtYWlsX2FkZHJlc3MiOiJqb3NoQGxpdHByb3RvY29sLmNvbSJ9fV19LCJpYXQiOjE2ODg1NjcxMTQsImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LWRlNGUyNjkwLTE1MDYtNGNmNS04YmNlLTQ0NTcxZGRhZWJjOSIsIm5iZiI6MTY4ODU2NzExNCwic3ViIjoidXNlci10ZXN0LTY4MTAzZTAxLTc0NjgtNGFiZi04M2M4LTg4NWRiMmNhMWM2YyJ9.rZgaunT1UV2pmliZ0V7nYqYtyfdGas4eY6Q6RCzEEBc5y1K66lopUbvvkfNsLJUjSc3vw12NlIX3Q47zm0XEP8AahrJ0QWAC4v9gmZKVYbKiL2JppqnaxtNLZV9Zo1KAiqm9gdqRQSD29222RTC59PI52AOZd4iTv4lSBIPG2J9rUkUwaRI23bGLMQ8XVkTSS7wcd1Ls08Q-VDXuwl8vuoJhssBfNfxFigk7cKHwbbM-o1sh3upEzV-WFgvJrTstPUNbHOBvGnqKDZX6A_45M5zBnHrerifz4-ST771tajiuW2lQXWvocyYlRT8_a0XBsW77UhU-YBTvKVpj3jmH4A" } diff --git a/packages/constants/src/lib/enums.ts b/packages/constants/src/lib/enums.ts index d87838012..01871558c 100644 --- a/packages/constants/src/lib/enums.ts +++ b/packages/constants/src/lib/enums.ts @@ -30,6 +30,7 @@ export enum AuthMethodType { GoogleJwt, OTP, AppleJwt, + StytchOtp, } /** @@ -41,5 +42,6 @@ export enum ProviderType { EthWallet = 'ethwallet', WebAuthn = 'webauthn', Otp = 'otp', + StytchOtp = 'stytchOtp', Apple = 'apple', } diff --git a/packages/lit-auth-client/src/lib/lit-auth-client.spec.ts b/packages/lit-auth-client/src/lib/lit-auth-client.spec.ts index 232439197..ae7559e60 100644 --- a/packages/lit-auth-client/src/lib/lit-auth-client.spec.ts +++ b/packages/lit-auth-client/src/lib/lit-auth-client.spec.ts @@ -7,6 +7,8 @@ global.TextDecoder = TextDecoder; // @ts-ignore - set global variable for testing global.jestTesting = true; +import * as LITCONFIG from './../../../../lit.config.json'; + import { ProviderType } from '@lit-protocol/constants'; import { LitAuthClient } from './lit-auth-client'; import GoogleProvider from './providers/GoogleProvider'; @@ -15,6 +17,7 @@ import WebAuthnProvider from './providers/WebAuthnProvider'; import EthWalletProvider from './providers/EthWalletProvider'; import AppleProvider from './providers/AppleProvider'; import { OtpProvider } from './providers/OtpProvider'; +import { StytchOtpAuthenticateOptions } from '@lit-protocol/types'; const isClass = (v: unknown) => { return typeof v === 'function' && /^\s*class\s+/.test(v.toString()); @@ -125,6 +128,34 @@ describe('getProvider', () => { }); }); +describe('StytchOtpProvider', () => { + let client: LitAuthClient; + let provider: OtpProvider; + + beforeEach(() => { + client = new LitAuthClient({ + litRelayConfig: { relayApiKey: 'test-api-key' }, + }); + + provider = client.initProvider(ProviderType.StytchOtp, { + appId: LITCONFIG.STYTCH_APP_ID, + userId: LITCONFIG.STYTCH_USER_ID, + }); + }); + + it('should parse jwt and resolve session', async () => { + const token: string = LITCONFIG.STYTCH_TEST_TOKEN; + const userId: string = LITCONFIG.STYTCH_USER_ID; + const authMethod = await provider.authenticate({ + accessToken: token, + userId: userId, + }); + expect(authMethod).toBeDefined(); + + expect(authMethod.accessToken).toEqual(token); + }); +}); + // describe('GoogleProvider', () => { // let client: LitAuthClient; // let provider: GoogleProvider; diff --git a/packages/lit-auth-client/src/lib/lit-auth-client.ts b/packages/lit-auth-client/src/lib/lit-auth-client.ts index 0d3052b1f..7e9b41f0e 100644 --- a/packages/lit-auth-client/src/lib/lit-auth-client.ts +++ b/packages/lit-auth-client/src/lib/lit-auth-client.ts @@ -3,9 +3,10 @@ import { IRelay, LitAuthClientOptions, OAuthProviderOptions, - OtpProviderOptions, + StytchOtpProviderOptions, ProviderOptions, SignInWithOTPParams, + OtpProviderOptions } from '@lit-protocol/types'; import { ProviderType } from '@lit-protocol/constants'; import { LitNodeClient } from '@lit-protocol/lit-node-client'; @@ -15,8 +16,9 @@ import GoogleProvider from './providers/GoogleProvider'; import DiscordProvider from './providers/DiscordProvider'; import EthWalletProvider from './providers/EthWalletProvider'; import WebAuthnProvider from './providers/WebAuthnProvider'; -import { OtpProvider } from './providers/OtpProvider'; +import { StytchOtpProvider } from './providers/StytchOtpProvider'; import AppleProvider from './providers/AppleProvider'; +import { OtpProvider } from './providers/OtpProvider'; /** * Class that handles authentication through Lit login @@ -38,10 +40,9 @@ export class LitAuthClient { * Map of providers */ private providers: Map; - + private litOtpOptions: OtpProviderOptions | undefined; - /** * Create a LitAuthClient instance * @@ -66,7 +67,6 @@ export class LitAuthClient { 'An API key is required to use the default Lit Relay server. Please provide either an API key or a custom relay server.' ); } - if (options?.litOtpConfig) { this.litOtpOptions = options?.litOtpConfig; } @@ -139,15 +139,23 @@ export class LitAuthClient { ...baseParams, }) as unknown as T; break; - case `otp`: - provider = new OtpProvider( + case `stytchOtp`: + provider = new StytchOtpProvider( { ...baseParams, - ...(options as SignInWithOTPParams), }, - this.litOtpOptions + (options as StytchOtpProviderOptions) ) as unknown as T; break; + case `otp`: + provider = new OtpProvider( + { + ...baseParams, + ...(options as SignInWithOTPParams), + }, + this.litOtpOptions + ) as unknown as T; + break; default: throw new Error( "Invalid provider type provided. Only 'google', 'discord', 'ethereum', and 'webauthn' are supported at the moment." diff --git a/packages/lit-auth-client/src/lib/providers/OtpProvider.ts b/packages/lit-auth-client/src/lib/providers/OtpProvider.ts index fc191150e..95486fdf9 100644 --- a/packages/lit-auth-client/src/lib/providers/OtpProvider.ts +++ b/packages/lit-auth-client/src/lib/providers/OtpProvider.ts @@ -154,4 +154,4 @@ export class OtpProvider extends BaseProvider { return ''; } } -} +} \ No newline at end of file diff --git a/packages/lit-auth-client/src/lib/providers/StytchOtpProvider.ts b/packages/lit-auth-client/src/lib/providers/StytchOtpProvider.ts new file mode 100644 index 000000000..1e0d9340f --- /dev/null +++ b/packages/lit-auth-client/src/lib/providers/StytchOtpProvider.ts @@ -0,0 +1,104 @@ +import { AuthMethodType } from '@lit-protocol/constants'; +import { + AuthMethod, + BaseAuthenticateOptions, + BaseProviderOptions, + StytchOtpAuthenticateOptions, + StytchToken, +} from '@lit-protocol/types'; +import { BaseProvider } from './BaseProvider'; +import { StytchOtpProviderOptions } from '@lit-protocol/types'; + +export class StytchOtpProvider extends BaseProvider { + private _params: StytchOtpProviderOptions; + private _provider: string = 'https://stytch.com/session'; + + constructor(params: BaseProviderOptions, config: StytchOtpProviderOptions) { + super(params); + this._params = config; + } + + /** + * Validates claims within a stytch authenticated JSON Web Token + * @param options authentication option containing the authenticated token + * @returns {AuthMethod} Authentication Method for auth method type OTP + * */ + override authenticate( + options?: T | undefined + ): Promise { + return new Promise((resolve, reject) => { + if (!options) { + reject( + new Error( + 'No Authentication options provided, please supply an authenticated JWT' + ) + ); + } + + const userId: string | undefined = + this._params.userId ?? + (options as unknown as StytchOtpAuthenticateOptions).userId; + + if (!userId) { + reject(new Error('User id must be provided')); + } + const accessToken: string | undefined = ( + options as unknown as StytchOtpAuthenticateOptions + )?.accessToken; + if (!accessToken) { + reject( + new Error('No access token provided, please provide a stych auth jwt') + ); + } + + const parsedToken: StytchToken = this._parseJWT(accessToken); + console.log(`otpProvider: parsed token body`, parsedToken); + const audience = (parsedToken['aud'] as string[])[0]; + if (audience != this._params.appId) { + reject(new Error('Parsed application id does not match parameters')); + } + + if (!audience) { + reject( + new Error( + 'could not find project id in token body, is this a stych token?' + ) + ); + } + const session = parsedToken[this._provider]; + const authFactor = session['authentication_factors'][0]; + if (!authFactor) { + reject(new Error('Could not find authentication info in session')); + } + + if (userId != parsedToken['sub']) { + reject( + new Error( + 'AppId does not match token contents. is this the right token for your application?' + ) + ); + } + + resolve({ + authMethodType: AuthMethodType.StytchOtp, + accessToken: accessToken, + }); + }); + } + + /** + * + * @param jwt token to parse + * @returns {string}- userId contained within the token message + */ + private _parseJWT(jwt: string): StytchToken { + const parts = jwt.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid token length'); + } + const body = Buffer.from(parts[1], 'base64'); + const parsedBody: StytchToken = JSON.parse(body.toString('ascii')); + console.log('JWT body: ', parsedBody); + return parsedBody; + } +} diff --git a/packages/lit-auth-client/src/lib/relay.ts b/packages/lit-auth-client/src/lib/relay.ts index 063faf0ae..f26fa7f7e 100644 --- a/packages/lit-auth-client/src/lib/relay.ts +++ b/packages/lit-auth-client/src/lib/relay.ts @@ -207,6 +207,8 @@ export class LitRelay implements IRelay { return '/auth/google/userinfo'; case AuthMethodType.OTP: return `/auth/otp/userinfo`; + case AuthMethodType.StytchOtp: + return 'auth/stytch-otp/userinfo'; case AuthMethodType.WebAuthn: return '/auth/webauthn/userinfo'; default: @@ -233,6 +235,8 @@ export class LitRelay implements IRelay { return '/auth/google'; case AuthMethodType.OTP: return `/auth/otp`; + case AuthMethodType.StytchOtp: + return '/auth/stytch-otp'; case AuthMethodType.WebAuthn: return '/auth/webauthn/verify-registration'; default: diff --git a/packages/types/src/lib/interfaces.ts b/packages/types/src/lib/interfaces.ts index 6b363935a..936d90ca5 100644 --- a/packages/types/src/lib/interfaces.ts +++ b/packages/types/src/lib/interfaces.ts @@ -1202,6 +1202,17 @@ export interface SignInWithOTPParams { customName?: string; } +export interface EthWalletProviderOptions { + /** + * The domain from which the signing request is made + */ + domain?: string; + /** + * The origin from which the signing request is made + */ + origin?: string; +} + export interface OtpProviderOptions { baseUrl?: string; port?: string; @@ -1213,6 +1224,30 @@ export interface OtpEmailCustomizationOptions { from?: string; fromName: string; } + +export interface SignInWithStytchOTPParams { + // JWT from an authenticated session + // see stych docs for more info: https://stytch.com/docs/api/session-get + accessToken?: string; + // username or phone number where OTP was delivered + userId: string; +} + +export interface StytchOtpProviderOptions { + /* + Stytch application identifier + */ + appId: string; + /* + Stytch user identifier for a project + */ + userId?: string; +} + +export interface StytchToken { + [key: string]: any; +} + export interface BaseProviderSessionSigsParams { /** * Public key of PKP to auth with @@ -1257,10 +1292,6 @@ export interface LoginUrlParams { export interface BaseAuthenticateOptions {} -export interface OtpAuthenticateOptions { - code: string; -} - export interface EthWalletAuthenticateOptions extends BaseAuthenticateOptions { /** * Ethereum wallet address @@ -1290,3 +1321,15 @@ export interface OtpAuthenticateOptions extends BaseAuthenticateOptions { */ code: string; } + +export interface StytchOtpAuthenticateOptions extends BaseAuthenticateOptions { + /* + * JWT from an authenticated session + * see stych docs for more info: https://stytch.com/docs/api/session-get + */ + accessToken: string; + /* + Stytch user identifier for a project + */ + userId?: string; +}