Skip to content

Commit

Permalink
feat: sign in with ethereum support
Browse files Browse the repository at this point in the history
  • Loading branch information
einaralex committed Jan 10, 2025
1 parent 5862bfc commit 69847f2
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 307 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"cSpell.words": [
"Commitlint",
"Monerium",
"PKCE",
"sepolia",
"SIWE",
"stylelint"
],
"editor.codeActionsOnSave": {
Expand Down
48 changes: 42 additions & 6 deletions apps/customer/app/test/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,6 +14,7 @@ import {
OrderState,
PaymentStandard,
placeOrderMessage,
rfc3339,
} from '@monerium/sdk';
import {
MoneriumContext,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 });
});
};

Expand Down Expand Up @@ -663,7 +696,7 @@ export default function Test() {
<p>
{!isAuthorized ? (
<>
<button type="submit" onClick={authorize}>
<button type="submit" onClick={() => authorize()}>
Authorize
</button>
<button
Expand All @@ -681,6 +714,9 @@ export default function Test() {
<button type="submit" onClick={autoLink}>
Authorize with auto linking.
</button>
<button type="submit" onClick={authorizeSiwe}>
Siwe.
</button>
</>
) : (
<button
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk-react-provider/src/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function useSdk(): MoneriumClient | undefined {
*
* @returns {UseAuthReturn}
* - `authorize` - Redirects to the Monerium auth flow.
* - `siwe` - Sign in with Ethereum. https://monerium.com/siwe
* - `isAuthorized` - Whether the user is authorized.
* - `isLoading` - Whether the auth flow is loading.
* - `error` - Error message if the auth flow fails.
Expand All @@ -123,9 +124,11 @@ export function useAuth(): UseAuthReturn {
if (context === null) {
throw new Error('useAuth must be used within a MoneriumProvider');
}

return {
isAuthorized: context.isAuthorized,
authorize: context.authorize,
siwe: context.siwe,
isLoading: context.isLoading,
error: context.error,
disconnect: context.disconnect,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-react-provider/src/lib/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const TestConsumerComponent = () => {

return (
<div>
<button onClick={authorize}>Authorize</button>
<button onClick={() => authorize()}>Authorize</button>
{isAuthorized && <p>Authorized!</p>}
{profile && <p data-testid="profile">{profile.name}</p>}
{/* You can add more elements for other context values */}
Expand Down
24 changes: 21 additions & 3 deletions packages/sdk-react-provider/src/lib/provider.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -99,7 +102,7 @@ export const MoneriumProvider = ({
}, [refreshToken]);

const authorize = useCallback(
async (params?: AuthorizeParams) => {
async (params?: AuthFlowOptions) => {
try {
if (sdk) {
await sdk.authorize(params);
Expand All @@ -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) {
Expand All @@ -131,6 +148,7 @@ export const MoneriumProvider = ({
value={{
sdk,
authorize,
siwe,
isAuthorized,
isLoading: loadingAuth,
error,
Expand Down
14 changes: 7 additions & 7 deletions packages/sdk-react-provider/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
authorize: (params?: AuthFlowOptions) => Promise<void>;
/**
* Sign in with Ethereum.
* https://monerium.com/siwe
*/
siwe: (params: AuthFlowSIWEOptions) => Promise<void>;
/**
* Indicates whether the SDK is authorized.
*/
Expand Down
104 changes: 70 additions & 34 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { MONERIUM_CONFIG } from './config';
import constants from './constants';
import {
cleanQueryString,
getAuthFlowUrlAndStoreCodeVerifier,
isAuthCode,
isClientCredentials,
isRefreshToken,
preparePKCEChallenge,
queryParams,
rest,
} from './helpers';
Expand All @@ -18,6 +18,7 @@ import type {
AuthArgs,
AuthCodePayload,
AuthFlowOptions,
AuthFlowSIWEOptions,
AuthorizationCodeCredentials,
Balances,
BearerProfile,
Expand All @@ -41,7 +42,8 @@ import type {
OrderFilter,
OrderNotificationQueryParams,
OrdersResponse,
PKCERequestArgs,
PKCERequest,
PKCESIWERequest,
Profile,
ProfilesQueryParams,
ProfilesResponse,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
};
}
Loading

0 comments on commit 69847f2

Please sign in to comment.