diff --git a/.vscode/settings.json b/.vscode/settings.json index eb932e8f..0b83f500 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,9 @@ "cSpell.words": [ "Commitlint", "Monerium", + "PKCE", "sepolia", + "SIWE", "stylelint" ], "editor.codeActionsOnSave": { diff --git a/apps/customer/app/test/page.tsx b/apps/customer/app/test/page.tsx index 4dba8499..081c3629 100644 --- a/apps/customer/app/test/page.tsx +++ b/apps/customer/app/test/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { ChangeEvent, FormEvent, useContext, useEffect, useState } from 'react'; +import { ChangeEvent, FormEvent, useContext, useState } from 'react'; import Link from 'next/link'; import { useAccount, useChainId, useSignMessage } from 'wagmi'; import { ConnectButton } from '@rainbow-me/rainbowkit'; @@ -14,6 +14,7 @@ import { OrderState, PaymentStandard, placeOrderMessage, + rfc3339, } from '@monerium/sdk'; import { MoneriumContext, @@ -48,11 +49,16 @@ export default function Test() { * Monerium queries */ const context = useContext(MoneriumContext); - const { isAuthorized, authorize, revokeAccess, error: authError } = useAuth(); - const { data: profile } = useProfile(); + const { + isAuthorized, + authorize, + siwe, + revokeAccess, + error: authError, + } = useAuth(); - // const { authContext } = useAuthContext(); + const { data: profile } = useProfile(); const { data: orders } = useOrders(); @@ -634,7 +640,34 @@ export default function Test() { const autoLink = () => { signMessageAsync({ message: constants.LINK_MESSAGE }).then((signature) => { - authorize({ address, signature, chain: chainId }); + authorize({ address: `${address}`, signature, chain: chainId }); + }); + }; + const authorizeSiwe = () => { + const date = new Date(); + const issueDate = rfc3339(new Date(date.toISOString())); + + date.setMinutes(date.getMinutes() + 5); + const expiryDate = date.toISOString(); + + const siwe_message = `localhost:3000 wants you to sign in with your Ethereum account: +0xB64Fed2aFF534D5320BF401d0D5B93Ed7AbCf13E + +Allow SDK TEST APP to access my data on Monerium + +URI: http://localhost:3000/dashboard +Version: 1 +Chain ID: 100 +Nonce: ${Math.random().toString(36).substring(2, 16)} +Issued At: ${issueDate} +Expiration Time: ${expiryDate} +Resources: +- https://monerium.com/siwe +- https://example.com/privacy-policy +- https://example.com/terms-of-service`; + + signMessageAsync({ message: siwe_message }).then((signature) => { + siwe({ message: siwe_message, signature }); }); }; @@ -663,7 +696,7 @@ export default function Test() {

{!isAuthorized ? ( <> - + ) : ( + {isAuthorized &&

Authorized!

} {profile &&

{profile.name}

} {/* You can add more elements for other context values */} diff --git a/packages/sdk-react-provider/src/lib/provider.tsx b/packages/sdk-react-provider/src/lib/provider.tsx index 6edbc133..b4b22443 100644 --- a/packages/sdk-react-provider/src/lib/provider.tsx +++ b/packages/sdk-react-provider/src/lib/provider.tsx @@ -1,10 +1,13 @@ import { ReactNode, useCallback, useEffect, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { MoneriumClient } from '@monerium/sdk'; +import { + AuthFlowOptions, + AuthFlowSIWEOptions, + MoneriumClient, +} from '@monerium/sdk'; import { MoneriumContext } from './context'; -import { AuthorizeParams } from './types'; /** * Wrap your application with the Monerium provider. @@ -99,7 +102,7 @@ export const MoneriumProvider = ({ }, [refreshToken]); const authorize = useCallback( - async (params?: AuthorizeParams) => { + async (params?: AuthFlowOptions) => { try { if (sdk) { await sdk.authorize(params); @@ -112,6 +115,20 @@ export const MoneriumProvider = ({ [sdk] ); + const siwe = useCallback( + async (params: AuthFlowSIWEOptions) => { + try { + if (sdk) { + await sdk.siwe(params); + } + } catch (err) { + console.error('Error during sign in with ethereum:', err); + setError(err); + } + }, + [sdk] + ); + const revokeAccess = async () => { try { if (sdk) { @@ -131,6 +148,7 @@ export const MoneriumProvider = ({ value={{ sdk, authorize, + siwe, isAuthorized, isLoading: loadingAuth, error, diff --git a/packages/sdk-react-provider/src/lib/types.ts b/packages/sdk-react-provider/src/lib/types.ts index 70ffe67d..59aff8fc 100644 --- a/packages/sdk-react-provider/src/lib/types.ts +++ b/packages/sdk-react-provider/src/lib/types.ts @@ -6,23 +6,23 @@ import { } from '@tanstack/react-query'; import type MoneriumClient from '@monerium/sdk'; -import { ChainId } from '@monerium/sdk'; +import type { AuthFlowOptions, AuthFlowSIWEOptions } from '@monerium/sdk'; export type SdkInstance = { /** Monerium SDK instance. */ sdk?: MoneriumClient; }; -export type AuthorizeParams = - | { address: string; signature: string; chainId?: ChainId } - | { state?: string; scope?: string } - | {}; - export type UseAuthReturn = { /** * Constructs the url and redirects to the Monerium auth flow. */ - authorize: (params?: AuthorizeParams) => Promise; + authorize: (params?: AuthFlowOptions) => Promise; + /** + * Sign in with Ethereum. + * https://monerium.com/siwe + */ + siwe: (params: AuthFlowSIWEOptions) => Promise; /** * Indicates whether the SDK is authorized. */ diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 91ad332a..7d7cc5d4 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -2,10 +2,10 @@ import { MONERIUM_CONFIG } from './config'; import constants from './constants'; import { cleanQueryString, - getAuthFlowUrlAndStoreCodeVerifier, isAuthCode, isClientCredentials, isRefreshToken, + preparePKCEChallenge, queryParams, rest, } from './helpers'; @@ -18,6 +18,7 @@ import type { AuthArgs, AuthCodePayload, AuthFlowOptions, + AuthFlowSIWEOptions, AuthorizationCodeCredentials, Balances, BearerProfile, @@ -41,7 +42,8 @@ import type { OrderFilter, OrderNotificationQueryParams, OrdersResponse, - PKCERequestArgs, + PKCERequest, + PKCESIWERequest, Profile, ProfilesQueryParams, ProfilesResponse, @@ -152,41 +154,86 @@ export class MoneriumClient { } /** - * Construct the url to the authorization code flow and redirects, + * Constructs the url to the authorization code flow and redirects, * Code Verifier needed for the code challenge is stored in local storage * For automatic wallet link, add the following properties: `address`, `signature` & `chain` * + * This authorization code is then used to request an access token via the token endpoint. (https://monerium.dev/api-docs#operation/auth-token) + * * @group Authentication * @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation} * @param {AuthFlowOptions} [params] - the auth flow params - * @returns string + * @returns void * */ async authorize(params?: AuthFlowOptions) { - const clientId = - params?.clientId || - (this.#client as AuthorizationCodeCredentials)?.clientId; - const redirectUri = - params?.redirectUri || - (this.#client as AuthorizationCodeCredentials)?.redirectUri; - - if (!clientId) { - throw new Error('Missing ClientId'); - } - - const authFlowUrl = getAuthFlowUrlAndStoreCodeVerifier(this.#env, { - client_id: clientId, - redirect_uri: redirectUri, - address: params?.address, - signature: params?.signature, - chain: params?.chain, + const codeChallenge = preparePKCEChallenge(); + + const autoLink = params?.address + ? { + address: params?.address, + signature: params?.signature, + chain: params?.chain + ? parseChainBackwardsCompatible(this.#env.name, params?.chain) + : undefined, + } + : {}; + + const queryParams = urlEncoded({ + client_id: (this.#client as AuthorizationCodeCredentials)?.clientId, + redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri, + code_challenge: codeChallenge, + code_challenge_method: 'S256' as PKCERequest['code_challenge_method'], + response_type: 'code' as PKCERequest['response_type'], state: params?.state, - email: params?.email, skip_create_account: params?.skipCreateAccount, skip_kyc: params?.skipKyc, + email: params?.email, + ...autoLink, }); - this.#debug(`Authorization URL: ${authFlowUrl}`); + const authFlowUrl = `${this.#env.api}/auth?${queryParams}`; + + this.#debug(`Auth flow URL: ${authFlowUrl}`); + // Redirect to the authFlow + window.location.assign(authFlowUrl); + } + /** + * Constructs the url to the authorization code flow and redirects, + * Code Verifier needed for the code challenge is stored in local storage + * + * "Sign in with Ethereum" (SIWE) flow can be used for existing Monerium customers. + * In this case the payload must include a valid EIP-4361 (https://eips.ethereum.org/EIPS/eip-4361) message and signature. + * On successful authorization the authorization code is returned at once. + * + * This authorization code is then used to request an access token via the token endpoint. + * + * https://monerium.com/siwe + * + * @group Authentication + * @see {@link https://monerium.dev/api-docs-v2#tag/auth/operation/auth | API Documentation} + * @param {AuthFlowSIWEOptions} [params] - the auth flow SIWE params + * @returns void + * + */ + async siwe(params: AuthFlowSIWEOptions) { + const codeChallenge = preparePKCEChallenge(); + + const queryParams = urlEncoded({ + client_id: (this.#client as AuthorizationCodeCredentials)?.clientId, + redirect_uri: (this.#client as AuthorizationCodeCredentials)?.redirectUri, + message: params.message, + signature: params.signature, + code_challenge: codeChallenge, + code_challenge_method: 'S256' as PKCESIWERequest['code_challenge_method'], + authentication_method: 'siwe' as PKCESIWERequest['authentication_method'], + state: params?.state, + }); + + const authFlowUrl = `${this.#env.api}/auth?${queryParams}`; + + this.#debug(`Auth flow SIWE URL: ${authFlowUrl}`); + // Redirect to the authFlow window.location.assign(authFlowUrl); } @@ -791,15 +838,4 @@ export class MoneriumClient { * @hidden */ getEnvironment = (): Environment => this.#env; - /** - * - * @hidden - */ - getAuthFlowURI = (args: PKCERequestArgs): string => { - const url = getAuthFlowUrlAndStoreCodeVerifier( - this.#env, - mapChainIdToChain(this.#env.name, args) - ); - return url; - }; } diff --git a/packages/sdk/src/helpers/auth.helpers.ts b/packages/sdk/src/helpers/auth.helpers.ts index 274888b5..bcb2ede0 100644 --- a/packages/sdk/src/helpers/auth.helpers.ts +++ b/packages/sdk/src/helpers/auth.helpers.ts @@ -6,56 +6,8 @@ import { AuthArgs, AuthCodePayload, ClientCredentialsPayload, - Environment, - PKCERequest, - PKCERequestArgs, RefreshTokenPayload, } from '../types'; -import { mapChainIdToChain, urlEncoded } from '../utils'; - -/** Structure the Auth Flow params */ -export const getAuthFlowParams = ( - args: PKCERequestArgs, - codeChallenge: string -) => { - const { - client_id, - redirect_uri, - scope, - state, - address, - signature, - chain, - email, - skip_create_account, - skip_kyc, - } = args; - - const autoLink = address - ? { - address: address, - ...(signature !== undefined ? { signature: signature } : {}), - ...(chain !== undefined ? { chain: chain } : {}), - } - : {}; - - return urlEncoded({ - client_id, - redirect_uri, - ...(scope !== undefined ? { scope: scope } : {}), - ...(email !== undefined ? { email: email } : {}), - ...(state !== undefined ? { state: state } : {}), - ...(skip_create_account !== undefined - ? { skip_create_account: skip_create_account } - : {}), - ...(skip_kyc !== undefined ? { skip_kyc: skip_kyc } : {}), - code_challenge: codeChallenge, - code_challenge_method: 'S256' as PKCERequest['code_challenge_method'], - response_type: 'code' as PKCERequest['response_type'], - - ...autoLink, - }); -}; /** * Find a more secure way to generate a random string @@ -81,19 +33,13 @@ export const generateCodeChallenge = (codeVerifier: string) => { return encodeBase64Url.stringify(SHA256(codeVerifier as string)); }; -/** - * Constructs the Auth Flow URL and stores the code verifier in the local storage - */ -export const getAuthFlowUrlAndStoreCodeVerifier = ( - environment: Environment, - args: PKCERequestArgs -): string => { +export const preparePKCEChallenge = (): string => { const codeVerifier = generateRandomString(); const codeChallenge = generateCodeChallenge(codeVerifier); localStorage.setItem(constants.STORAGE_CODE_VERIFIER, codeVerifier || ''); - return `${environment.api}/auth?${getAuthFlowParams(mapChainIdToChain(environment.name, args), codeChallenge)}`; + return codeChallenge; }; /** diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 5ffffe10..d39e8dd1 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -61,12 +61,6 @@ export type AuthArgs = | Omit | Omit; -export type OpenArgs = - | Omit - | Omit - | Omit - | PKCERequestArgs; - /** One of the options for the {@link AuthArgs}. * * [Auth endpoint in API documentation:](https://monerium.dev/api-docs#operation/auth). @@ -110,6 +104,18 @@ export interface BearerProfile { // -- pkceRequest +export interface PKCERequestShared { + /** the authentication flow client id of the application */ + client_id: string; + /** the redirect uri of the application */ + redirect_uri?: string; + /** the code challenge automatically generated by the SDK */ + code_challenge: string; + /** the code challenge method for the authentication flow , handled by the SDK */ + code_challenge_method: 'S256'; + /** the state of the application */ + state?: string; +} /** * @returns A {@link PKCERequest} object with properties omitted that are automatically computed in by the SDK. */ @@ -118,22 +124,12 @@ export type PKCERequestArgs = Omit< 'code_challenge' | 'code_challenge_method' | 'response_type' >; -export type PKCERequest = { - /** the authentication flow client id of the application */ - client_id: string; - /** the code challenge automatically generated by the SDK */ - code_challenge: string; - /** the code challenge method for the authentication flow , handled by the SDK */ - code_challenge_method: 'S256'; +export interface PKCERequest extends PKCERequestShared { /** the response type of the authentication flow, handled by the SDK */ response_type: 'code'; - /** the state of the application */ - state?: string; - /** the redirect uri of the application */ - redirect_uri: string; /** the email of the user to prefill the login form */ email?: string; - /** the scope of the application */ + /** @deprecated: will be removed, the scope of the application */ scope?: string; /** the address of the wallet to automatically link */ address?: string; @@ -145,7 +141,29 @@ export type PKCERequest = { skip_create_account?: boolean; /** You can skip the KYC onboarding steps in the Authorization Flow and use the the details, additional data, and verifications API endpoints after you have gotten the authorization. */ skip_kyc?: boolean; -}; +} + +/** + * @returns A {@link PKCESIWERequest} object with properties omitted that are automatically computed in by the SDK. + */ +export type PKCERSIWERequestArgs = Omit< + PKCESIWERequest, + 'code_challenge' | 'code_challenge_method' | 'authentication_method' +>; + +export interface PKCESIWERequest extends PKCERequestShared { + /** + * Authentication method used. The default is to redirect the user to Monerium login screen, where the user can either sign in, or go through the register flow. + * `siwe` is only applicable for existing Monerium customers who have already linked at least one of their addresses with Monerium. + **/ + authentication_method: 'siwe'; + /** An EIP-4361 compatible message. https://eips.ethereum.org/EIPS/eip-4361 + * https://monerium.com/siwe + * */ + message: string; + /** Signature for the SIWE message. Must include the 0x prefix. */ + signature: string; +} // -- authContext @@ -562,25 +580,34 @@ export type ClassOptions = { debug?: boolean; } & BearerTokenCredentials; -export interface AuthFlowOptions { - /** the auth flow client ID for your application */ - clientId?: string; - /** the redirect URI defined by your application */ - redirectUri?: string; +export interface AuthFlowOptionsShared { + /** the state oauth parameter */ + state?: string; +} +export interface AuthFlowOptions extends AuthFlowOptionsShared { /** the email of the user to prefill the login form */ email?: string; + /** skip account creation in auth flow */ + skipCreateAccount?: boolean; + /** skip KYC in auth flow */ + skipKyc?: boolean; /** the address your customer should link in auth flow */ address?: string; /** the signature of the address */ signature?: string; /** the chain of the address */ chain?: Chain | ChainId; - /** the state oauth parameter */ - state?: string; - /** skip account creation in auth flow */ - skipCreateAccount?: boolean; - /** skip KYC in auth flow */ - skipKyc?: boolean; +} + +export interface AuthFlowSIWEOptions extends AuthFlowOptionsShared { + /** Signature for the SIWE message. Must include the 0x prefix. */ + signature: string; + /** + * An EIP-4361 compatible message. https://eips.ethereum.org/EIPS/eip-4361 + * + * https://monerium.com/siwe + * */ + message: string; } export interface ClientCredentials { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index d4f11905..0fa04b71 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -144,14 +144,14 @@ export const placeOrderMessage = ( * @returns 'application/x-www-form-urlencoded' compatible string */ export const urlEncoded = ( - body: Record + body: Record ): string | undefined => { return body && Object.entries(body)?.length > 0 ? Object.entries(body) .filter(([_, value]) => value !== undefined) // Filter out undefined values .map( ([key, value]) => - `${encodeURIComponent(key)}=${encodeURIComponent(value)}` + `${encodeURIComponent(key)}=${encodeURIComponent(value as string | boolean | number)}` ) .join('&') : ''; diff --git a/packages/sdk/test/client-apimock.test.ts b/packages/sdk/test/client-apimock.test.ts index bcdb2be4..02f8e2dd 100644 --- a/packages/sdk/test/client-apimock.test.ts +++ b/packages/sdk/test/client-apimock.test.ts @@ -5,8 +5,11 @@ import 'jest-localstorage-mock'; import fetchMock from 'jest-fetch-mock'; -import { constants, MoneriumClient } from '../src/index'; +import constants from '../src/constants'; +import { generateCodeChallenge } from '../src/helpers'; +import { MoneriumClient } from '../src/index'; import { + AuthFlowSIWEOptions, Chain, Currency, NewOrder, @@ -17,14 +20,33 @@ import { } from '../src/types'; import { OWNER_SIGNATURE, PUBLIC_KEY } from './constants'; -const message = constants.LINK_MESSAGE; +const { STORAGE_CODE_VERIFIER } = constants; + +const assignMock = jest.fn(); + +const clientId = 'testClientId'; +const redirectUri = 'http://example.com'; let client: MoneriumClient; describe('MoneriumClient', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + assign: assignMock, + }, + writable: true, + }); + }); beforeEach(async () => { - client = new MoneriumClient(); + client = new MoneriumClient({ clientId, redirectUri }); fetchMock.resetMocks(); }); + afterEach(() => { + window.localStorage.clear(); + jest.restoreAllMocks(); + assignMock.mockRestore(); + }); + test('get tokens', async () => { await client.getTokens().catch(() => ({})); @@ -39,6 +61,120 @@ describe('MoneriumClient', () => { }) ); }); + describe('Authorize', () => { + const setupAuthorizeTest = async ( + params?: Record, + isSiwe: boolean = false + ) => { + if (isSiwe) { + await client.siwe(params as AuthFlowSIWEOptions).catch(() => ({})); + } else { + await client.authorize(params).catch(() => ({})); + } + + const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); + const challenge = generateCodeChallenge(codeVerifier as string); + + const assignedURL = assignMock.mock.calls[0][0]; // Retrieve the called URL + + expect(assignMock).toHaveBeenCalled(); + expect(assignedURL).toMatch(/^https:\/\/api\.monerium\.dev\/auth\?/); + expect(assignedURL).toContain(`code_challenge=${challenge}`); + expect(assignedURL).toContain(`code_challenge_method=S256`); + + if (isSiwe) { + expect(assignedURL).toContain(`authentication_method=siwe`); + } else { + expect(assignedURL).toContain(`response_type=code`); + } + + return assignedURL; + }; + test('authorize', async () => { + const assignedURL = await setupAuthorizeTest(); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).toContain( + `redirect_uri=${encodeURIComponent(redirectUri)}` + ); + }); + test('authorize - should skip incomplete autoLink params', async () => { + const args = { + signature: '0xShouldBeSkipped', + chain: 'ethereum', + }; + const assignedURL = await setupAuthorizeTest(args); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).not.toContain(`signature=`); + expect(assignedURL).not.toContain(`chain=`); + }); + test('authorize - should include autoLink params', async () => { + const args = { + address: '0xAddress', + signature: '0xSignature', + chain: 'ethereum', + }; + const assignedURL = await setupAuthorizeTest(args); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).toContain(`address=${args.address}`); + expect(assignedURL).toContain(`signature=${args.signature}`); + expect(assignedURL).toContain(`chain=sepolia`); + }); + test('authorize - should include various params', async () => { + const args = { + skipKyc: true, + skipCreateAccount: true, + signature: undefined, + email: 'test@email.is', + }; + const assignedURL = await setupAuthorizeTest(args); + + expect(assignedURL).toContain(`email=${encodeURIComponent(args.email)}`); + expect(assignedURL).toContain(`skip_kyc=${args.skipKyc}`); + expect(assignedURL).toContain( + `skip_create_account=${args.skipCreateAccount}` + ); + expect(assignedURL).not.toContain('signature='); + }); + test('siwe', async () => { + const args = { + message: '0xEIP-4361Message', + signature: 'signature', + }; + const assignedURL = await setupAuthorizeTest(args, true); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).toContain(`message=${args.message}`); + expect(assignedURL).toContain(`signature=${args.signature}`); + expect(assignedURL).not.toContain(`state=`); + }); + test('siwe - with state', async () => { + const args = { + message: '0xEIP-4361Message', + signature: 'signature', + state: 'fooBar', + }; + const assignedURL = await setupAuthorizeTest(args, true); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).toContain(`message=${args.message}`); + expect(assignedURL).toContain(`signature=${args.signature}`); + expect(assignedURL).toContain(`state=${args.state}`); + }); + test('siwe', async () => { + const args = { + message: '0xEIP-4361Message', + signature: 'signature', + }; + const assignedURL = await setupAuthorizeTest(args, true); + + expect(assignedURL).toContain(`client_id=${clientId}`); + expect(assignedURL).toContain(`message=${args.message}`); + expect(assignedURL).toContain(`signature=${args.signature}`); + }); + }); describe('Addresses', () => { test('link address using chainId', async () => { const body = { diff --git a/packages/sdk/test/client.test.ts b/packages/sdk/test/client.test.ts index d653ee97..4cccb894 100644 --- a/packages/sdk/test/client.test.ts +++ b/packages/sdk/test/client.test.ts @@ -10,7 +10,6 @@ import 'jest-localstorage-mock'; import constants from '../src/constants'; -import { generateCodeChallenge } from '../src/helpers'; import { MoneriumClient } from '../src/index'; import { getChain } from '../src/utils'; import { APP_ONE_AUTH_FLOW_CLIENT_ID, APP_ONE_REDIRECT_URL } from './constants'; @@ -19,19 +18,9 @@ const { LINK_MESSAGE, STORAGE_CODE_VERIFIER, STORAGE_ACCESS_TOKEN } = constants; const message = LINK_MESSAGE; -const assignMock = jest.fn(); - // Can't run in CI because of Cloudflare process.env.CI !== 'true' && describe('MoneriumClient', () => { - beforeAll(() => { - Object.defineProperty(window, 'location', { - value: { - assign: assignMock, - }, - writable: true, - }); - }); afterEach(() => { window.localStorage.clear(); jest.restoreAllMocks(); @@ -72,113 +61,6 @@ process.env.CI !== 'true' && expect(url).toContain('https://api.monerium.app'); }); - test('authorization code flow with chainId', async () => { - const client = new MoneriumClient(); - - const authFlowUrl = client.getAuthFlowURI({ - redirect_uri: 'http://example.com', - client_id: 'testClientId', - address: '0x', - chain: 11155111, - }); - const codeVerifier = window.localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(authFlowUrl).toBe( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code&address=0x&chain=sepolia` - ); - }); - - test('authorization code flow with chain and network', async () => { - const client = new MoneriumClient(); - - const authFlowUrl = client.getAuthFlowURI({ - redirect_uri: 'http://example.com', - client_id: 'testClientId', - address: '0x', - chain: 'ethereum', - }); - - const codeVerifier = window.localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(authFlowUrl).toBe( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code&address=0x&chain=sepolia` - ); - }); - - test('authorization code flow without chain info', async () => { - const client = new MoneriumClient(); - - const test = client.getAuthFlowURI({ - redirect_uri: 'http://example.com', - client_id: 'testClientId', - }); - - const codeVerifier = window.localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(test).toBe( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code` - ); - }); - - test('connect wallet', async () => { - const client = new MoneriumClient(); - - await client.authorize({ - redirectUri: 'http://example.com', - clientId: 'testClientId', - address: '0x1234', - signature: '0x5678', - chain: 'gnosis', - }); - - const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(assignMock).toHaveBeenCalledWith( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code&address=0x1234&signature=0x5678&chain=chiado` - ); - assignMock.mockRestore(); - }); - test('redirect', async () => { - const client = new MoneriumClient(); - - await client.authorize({ - redirectUri: 'http://example.com', - clientId: 'testClientId', - }); - - const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(assignMock).toHaveBeenCalledWith( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code` - ); - assignMock.mockRestore(); - }); - test('redirect w auto-link', async () => { - const client = new MoneriumClient({ - clientId: 'testClientId', - redirectUri: 'http://example.com', - }); - - await client.authorize({ - address: '0x1234', - signature: '0x5678', - chain: 137, - }); - - const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); - const challenge = generateCodeChallenge(codeVerifier as string); - - expect(assignMock).toHaveBeenCalledWith( - `https://api.monerium.dev/auth?client_id=testClientId&redirect_uri=http%3A%2F%2Fexample.com&code_challenge=${challenge}&code_challenge_method=S256&response_type=code&address=0x1234&signature=0x5678&chain=polygon` - ); - assignMock.mockRestore(); - }); - test('authorize with refresh token attempt', async () => { const client = new MoneriumClient({ clientId: APP_ONE_AUTH_FLOW_CLIENT_ID, diff --git a/packages/sdk/test/helpers.test.ts b/packages/sdk/test/helpers.test.ts index 9fde8867..11f14440 100644 --- a/packages/sdk/test/helpers.test.ts +++ b/packages/sdk/test/helpers.test.ts @@ -7,65 +7,25 @@ import constants from '../src/constants'; import { queryParams } from '../src/helpers'; import { generateCodeChallenge, - getAuthFlowUrlAndStoreCodeVerifier, + preparePKCEChallenge, } from '../src/helpers/auth.helpers'; const { STORAGE_CODE_VERIFIER } = constants; -describe('getAuthFlowUrlAndStoreCodeVerifier', () => { +describe('preparePKCEChallenge', () => { afterEach(() => { localStorage.clear(); }); - test('should generate auth flow URL and store code verifier', () => { - const baseUrl = 'https://api.test.com'; - const args = { - client_id: 'testClientId', - redirect_uri: 'http://example.com', - }; - - const url = getAuthFlowUrlAndStoreCodeVerifier( - { name: 'sandbox', api: baseUrl, web: '', wss: '' }, - args - ); + test('should generate code challenge and store code verifier', () => { + const codeChallenge = preparePKCEChallenge(); const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); - const codeChallenge = generateCodeChallenge(codeVerifier as string); - - expect(url).toContain(baseUrl); - expect(url).toContain(`client_id=${args.client_id}`); - expect(url).toContain( - `redirect_uri=${encodeURIComponent(args.redirect_uri)}` - ); - expect(url).toContain(`code_challenge=${codeChallenge}`); - expect(codeVerifier).toBeTruthy(); - }); - test('should generate auth flow URL and store code verifier - auto link', () => { - const baseUrl = 'https://api.test.com'; - const args = { - client_id: 'testClientId', - redirect_uri: 'http://example.com/test', - address: '0x1234', - chain: 'ethereum', - }; - - const url = getAuthFlowUrlAndStoreCodeVerifier( - { name: 'sandbox', api: baseUrl, web: '', wss: '' }, - args + const codeChallengeFromVerifier = generateCodeChallenge( + codeVerifier as string ); - const codeVerifier = localStorage.getItem(STORAGE_CODE_VERIFIER); - const codeChallenge = generateCodeChallenge(codeVerifier as string); - - expect(url).toContain(baseUrl); - expect(url).toContain(`client_id=${args.client_id}`); - expect(url).toContain( - `redirect_uri=${encodeURIComponent(args.redirect_uri)}` - ); - expect(url).toContain(`address=0x1234`); - expect(url).toContain(`chain=sepolia`); - expect(url).toContain(`code_challenge=${codeChallenge}`); - expect(codeVerifier).toBeTruthy(); + expect(codeChallenge).toEqual(codeChallengeFromVerifier); }); });