diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c88a062 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index bf15d45..c2ea984 100644 --- a/README.md +++ b/README.md @@ -40,81 +40,50 @@ yarn add @openfort/openfort-js ## Usage -The package needs to be configured with your account's public key, which is -available in the [Openfort Dashboard][api-keys]. Require it with the key's -value: - -```js -import Openfort from '@openfort/openfort-js'; -const openfort = new Openfort('pk_test_...'); -``` -In order to sign messages, you have 4 options to choose from: -* Let Openfort handle the signing process, dont need to pass any signer to the Openfort instance. -* Sign yourself and pass the signature to Openfort, dont need to pass any signer to the Openfort instance. -* Use a Session Key to sign messages, you need to pass a SessionSigner to the Openfort instance. -* Use Embedded Signer to sign messages, you need to pass an Embedded Signer to the Openfort instance. - -#### Session Signer -```ts -const sessionSigner = new SessionSigner() -const openfort = new Openfort('pk_test_...', sessionSigner); -``` - -#### Embedded Signer -For the embedded signer, if your player has an account you can pass it to the embedded signer to use it. If the account is not provided, the embedded signer will check if the localstorage has a device which is already registered, if not, it will create a new device and store it in the localstorage. -For the recovery process, you can ask the user for a password to encrypt the recovery share. - -```ts -const embeddedSigner = new EmbeddedSigner('pk_test_...', 'acc_...', '********'); -const openfort = new Openfort('pk_test_...', embeddedSigner); -``` - - -### Create and store a new player session key - -1. Create a session key pair for the player: - +With the Openfort Unity SDK, you can sign transaction intents using one of four methods or signers: ```typescript -openfort.createSessionKey(); +const sdk = new Openfort("pk_test_XXXXXXX"); ``` -2. Save the generated session key pair on device: +### 1. Session Signer +The Session Signer allows you to use external signing keys, without needing to provide it every time. Here's how to use it: +- **Configure the Session Key**: Call `configureSessionKey()`. This method returns an Ethereum address and a boolean indicating whether you need to register the key from the backend. ```typescript -openfort.saveSessionKey(); +const sessionKey = sdk.configureSessionKey(); ``` +- **Register Key and Send Signature Session Request**: If `sessionKey.isRegistered` boolean is false, register the key from the backend. Refer to the documentation for [session management](https://www.openfort.xyz/docs/guides/accounts/sessions). +- **Send Signature Transaction Intent Request**: When calling sendSignatureTransactionIntentRequest, pass the transaction intent ID and the user operation hash. The session signer will handle the signing. + +### 2. External Sign -3. Authorize player with the game backend service and passing the address of the session key pair: +This method allows you to externally sign transaction intents without logging in or additional configurations: +- **Call SendSignatureTransactionIntentRequest**: Simply pass the transaction intent ID and the signature. ```typescript -const address = openfort.sessionKey.address -// API call to the game backend with the address to register it +const response = await sdk.sendSignatureTransactionIntentRequest("transactionIntentId", null, "signature"); ``` -#### Register the session key using a non-custodial signer - -If the Openfort account is owned by an external signer, the owner must use it to sign and approve the registration of the session key. The hash containing the message to be signed appears in [next_actions][next-action] from the create session request. - +### 3. Embedded Signer +The Embedded Signer uses SSS to manage the private key on the client side. To learn more, visit our [security documentation](https://www.openfort.xyz/docs/security). +- **Login and Configure the Embedded Signer**: First, ensure the user is logged in, using `LoginWithEmailPassword`, `LoginWithOAuth` or if not registred `SignUpWithEmailPassword`. Then call `ConfigureEmbeddedSigner`. If a `MissingRecoveryMethod` exception is thrown, it indicates there's no share on the device and you have to call `ConfigureEmbeddedRecovery` to provide a recovery method. ```typescript -// Sign the message with the signer -await openfort.sendSignatureSessionRequest( - session_id, - signed_message -); +try { + await sdk.loginWithEmailPassword("email", "password"); + sdk.configureEmbeddedSigner(chainId); +} catch (e) { + if (e instanceof MissingRecoveryMethod) { + await sdk.configureEmbeddedSignerRecovery(new PasswordRecovery("password")); + } +} ``` - -### Use the session key to sign a message - -The hash containing the message to be signed appears in [next_actions][next-action] from the create transactionIntent request. - +For now the only recovery method available is the `PasswordRecovery` method. +- **Send Signature Transaction Intent Request**: Similar to the session signer, pass the transaction intent ID and the user operation hash. The embedded signer reconstructs the key and signs the transaction. ```typescript -await openfort.signMessage(message); -await openfort.sendSignatureTransactionIntentRequest( - transactionIntent_id, - signed_message -); +const response = await sdk.sendSignatureTransactionIntentRequest("transactionIntentId", "userOp"); ``` + ## Usage examples - [Next.js application with non-custodial signer](https://github.com/openfort-xyz/samples/tree/main/rainbow-ssv-nextjs) - [Next.js application with custodial signer and social login](https://github.com/openfort-xyz/samples/tree/main/ssv-social-nextjs) diff --git a/package.json b/package.json index e2359f4..404bec1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfort/openfort-js", - "version": "0.4.6", + "version": "0.5.0", "description": "", "author": "Openfort", "repository": { diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..534a05e Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/utils/iframe-client.ts b/src/clients/iframe-client.ts similarity index 76% rename from src/utils/iframe-client.ts rename to src/clients/iframe-client.ts index ec1c0e1..c4e2af8 100644 --- a/src/utils/iframe-client.ts +++ b/src/clients/iframe-client.ts @@ -6,24 +6,30 @@ export class IframeClient { if (!document) { throw new Error("must be run in a browser"); } + + const actualIframeURL = document.getElementById("openfort-iframe"); + if (actualIframeURL) { + this._iframe = actualIframeURL as HTMLIFrameElement; + return; + } + this._chainId = chainId; this._iframe = document.createElement("iframe"); const baseURL = iframeURL || "https://iframe.openfort.xyz"; this._iframe.src = baseURL + "/iframe?accessToken=" + accessToken + "&publishableKey=" + publishableKey; this._iframe.style.display = "none"; + this._iframe.id = "openfort-iframe"; document.body.appendChild(this._iframe); } - private waitForIframeLoad(): Promise { - if (!this._iframe.contentWindow) { - return new Promise((resolve) => { - this._iframe.onload = () => { - resolve(); - }; - }); - } + public isLoaded(): boolean { + return this._iframe.contentWindow !== null; + } - return Promise.resolve(); + private async waitForIframeLoad(): Promise { + while (!this.isLoaded()) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } } async createAccount(password?: string): Promise { @@ -158,7 +164,37 @@ export class IframeClient { }); } - dispose() { - document.body.removeChild(this._iframe); + async dispose(): Promise { + await this.waitForIframeLoad(); + + return new Promise((resolve, reject) => { + const handleMessage = (event: MessageEvent) => { + if (event.data.action === "loggedOut") { + if (event.data.success) { + document.body.removeChild(this._iframe); + resolve(); + } else { + reject(new Error(event.data.error || "Dispose failed")); + } + + window.removeEventListener("message", handleMessage); + } + }; + + window.addEventListener("message", handleMessage); + + setTimeout(() => { + if (this._iframe.contentWindow) { + this._iframe.contentWindow.postMessage( + { + action: "logout", + }, + "*", + ); + } else { + console.error("No iframe content window"); + } + }, 1000); + }); } } diff --git a/src/key-pair.ts b/src/crypto/key-pair.ts similarity index 63% rename from src/key-pair.ts rename to src/crypto/key-pair.ts index 8683e80..df23a8a 100644 --- a/src/key-pair.ts +++ b/src/crypto/key-pair.ts @@ -1,14 +1,10 @@ import {secp256k1} from "@noble/curves/secp256k1"; -import {LocalStorage} from "./storage/local-storage"; -import {StorageKeys} from "./storage/storage-keys"; import {SigningKey} from "@ethersproject/signing-key"; import {arrayify, Bytes, BytesLike, joinSignature} from "@ethersproject/bytes"; import {computeAddress} from "@ethersproject/transactions"; import {hashMessage} from "@ethersproject/hash"; export class KeyPair extends SigningKey { - private static readonly storage = new LocalStorage(); - /** * Initialize keypair based on the private key, if it is provided or generate a brand new keypair. * @param privateKey Optional parameter to initialize private key from @@ -25,32 +21,21 @@ export class KeyPair extends SigningKey { return joinSignature(this.signDigest(hashMessage(arrayify(message)))); } - /** - * Save to the storage initialized as a static property of the KeyPair class - */ - public save(): void { - KeyPair.storage.save(StorageKeys.SESSION_KEY, this.privateKey); - } - - /** - * Remove the keypair from the storage - */ - public remove(): void { - KeyPair.storage.remove(StorageKeys.SESSION_KEY); - } - /** * Load private key from the storage and generate keypair based on it. */ - public static load(): KeyPair | null { - const privateKey = KeyPair.storage.get(StorageKeys.SESSION_KEY); + public static load(privateKey: string): KeyPair | null { return privateKey ? new KeyPair(arrayify(privateKey)) : null; } /** * Return the address for the keypair */ - public get address(): string { + public getPublicKey(): string { return computeAddress(this.privateKey); } + + public getPrivateKey(): string { + return this.privateKey; + } } diff --git a/src/generated/api.ts b/src/generated/api.ts index 5c5f5d4..449a73f 100644 --- a/src/generated/api.ts +++ b/src/generated/api.ts @@ -2353,6 +2353,25 @@ export interface DeployRequest { */ 'policy': string; } +/** + * + * @export + * @interface DeprecatedAuthenticatedPlayerResponse + */ +export interface DeprecatedAuthenticatedPlayerResponse { + /** + * Player\'s identifier. + * @type {string} + * @memberof DeprecatedAuthenticatedPlayerResponse + */ + 'playerId': string; + /** + * JWT access token. + * @type {string} + * @memberof DeprecatedAuthenticatedPlayerResponse + */ + 'token': string; +} /** * * @export @@ -9674,6 +9693,132 @@ export const AdminAuthenticationApiAxiosParamCreator = function (configuration?: let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Verifies the token generated by Openfort Auth. + * @summary Verify auth token. + * @param {string} token Specifies the auth token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyAuthToken: async (token: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'token' is not null or undefined + assertParamExists('verifyAuthToken', 'token', token) + const localVarPath = `/iam/v1/verify`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication sk required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (token !== undefined) { + localVarQueryParameter['token'] = token; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + verifyOAuth: async (provider: OAuthProvider, oAuthRequest: OAuthRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'provider' is not null or undefined + assertParamExists('verifyOAuth', 'provider', provider) + // verify required parameter 'oAuthRequest' is not null or undefined + assertParamExists('verifyOAuth', 'oAuthRequest', oAuthRequest) + const localVarPath = `/iam/v1/oauth/{provider}/verify` + .replace(`{${"provider"}}`, encodeURIComponent(String(provider))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication sk required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(oAuthRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by oauth token. + * @param {AuthenticateOAuthRequest} authenticateOAuthRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyOAuthToken: async (authenticateOAuthRequest: AuthenticateOAuthRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'authenticateOAuthRequest' is not null or undefined + assertParamExists('verifyOAuthToken', 'authenticateOAuthRequest', authenticateOAuthRequest) + const localVarPath = `/iam/v1/oauth/verify`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication sk required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(authenticateOAuthRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -9707,7 +9852,7 @@ export const AdminAuthenticationApiFp = function(configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async deleteAuthPlayer(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async deleteAuthPlayer(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAuthPlayer(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9757,6 +9902,41 @@ export const AdminAuthenticationApiFp = function(configuration?: Configuration) const localVarAxiosArgs = await localVarAxiosParamCreator.listOAuthConfig(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Verifies the token generated by Openfort Auth. + * @summary Verify auth token. + * @param {string} token Specifies the auth token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async verifyAuthToken(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.verifyAuthToken(token, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + async verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuth(provider, oAuthRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by oauth token. + * @param {AuthenticateOAuthRequest} authenticateOAuthRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuthToken(authenticateOAuthRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -9784,7 +9964,7 @@ export const AdminAuthenticationApiFactory = function (configuration?: Configura * @param {*} [options] Override http request option. * @throws {RequiredError} */ - deleteAuthPlayer(id: string, options?: any): AxiosPromise { + deleteAuthPlayer(id: string, options?: any): AxiosPromise { return localVarFp.deleteAuthPlayer(id, options).then((request) => request(axios, basePath)); }, /** @@ -9829,6 +10009,38 @@ export const AdminAuthenticationApiFactory = function (configuration?: Configura listOAuthConfig(options?: any): AxiosPromise { return localVarFp.listOAuthConfig(options).then((request) => request(axios, basePath)); }, + /** + * Verifies the token generated by Openfort Auth. + * @summary Verify auth token. + * @param {string} token Specifies the auth token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyAuthToken(token: string, options?: any): AxiosPromise { + return localVarFp.verifyAuthToken(token, options).then((request) => request(axios, basePath)); + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + */ + verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: any): AxiosPromise { + return localVarFp.verifyOAuth(provider, oAuthRequest, options).then((request) => request(axios, basePath)); + }, + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by oauth token. + * @param {AuthenticateOAuthRequest} authenticateOAuthRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: any): AxiosPromise { + return localVarFp.verifyOAuthToken(authenticateOAuthRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -9912,6 +10124,44 @@ export class AdminAuthenticationApi extends BaseAPI { public listOAuthConfig(options?: AxiosRequestConfig) { return AdminAuthenticationApiFp(this.configuration).listOAuthConfig(options).then((request) => request(this.axios, this.basePath)); } + + /** + * Verifies the token generated by Openfort Auth. + * @summary Verify auth token. + * @param {string} token Specifies the auth token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AdminAuthenticationApi + */ + public verifyAuthToken(token: string, options?: AxiosRequestConfig) { + return AdminAuthenticationApiFp(this.configuration).verifyAuthToken(token, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest + * @param {*} [options] Override http request option. + * @deprecated + * @throws {RequiredError} + * @memberof AdminAuthenticationApi + */ + public verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig) { + return AdminAuthenticationApiFp(this.configuration).verifyOAuth(provider, oAuthRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by oauth token. + * @param {AuthenticateOAuthRequest} authenticateOAuthRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AdminAuthenticationApi + */ + public verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig) { + return AdminAuthenticationApiFp(this.configuration).verifyOAuthToken(authenticateOAuthRequest, options).then((request) => request(this.axios, this.basePath)); + } } @@ -10425,16 +10675,21 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf }; }, /** - * Verifies the token generated by Openfort Auth. - * @summary Verify auth token. - * @param {string} token Specifies the auth token. + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - verifyAuthToken: async (token: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'token' is not null or undefined - assertParamExists('verifyAuthToken', 'token', token) - const localVarPath = `/iam/v1/verify`; + verifyOAuth: async (provider: OAuthProvider, oAuthRequest: OAuthRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'provider' is not null or undefined + assertParamExists('verifyOAuth', 'provider', provider) + // verify required parameter 'oAuthRequest' is not null or undefined + assertParamExists('verifyOAuth', 'oAuthRequest', oAuthRequest) + const localVarPath = `/iam/v1/oauth/{provider}/verify` + .replace(`{${"provider"}}`, encodeURIComponent(String(provider))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -10442,7 +10697,7 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -10450,15 +10705,14 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (token !== undefined) { - localVarQueryParameter['token'] = token; - } - + localVarHeaderParameter['Content-Type'] = 'application/json'; + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(oAuthRequest, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -10472,9 +10726,9 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf * @param {*} [options] Override http request option. * @throws {RequiredError} */ - verifyOAuth: async (authenticateOAuthRequest: AuthenticateOAuthRequest, options: AxiosRequestConfig = {}): Promise => { + verifyOAuthToken: async (authenticateOAuthRequest: AuthenticateOAuthRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'authenticateOAuthRequest' is not null or undefined - assertParamExists('verifyOAuth', 'authenticateOAuthRequest', authenticateOAuthRequest) + assertParamExists('verifyOAuthToken', 'authenticateOAuthRequest', authenticateOAuthRequest) const localVarPath = `/iam/v1/oauth/verify`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -10491,10 +10745,6 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - // authentication pk required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - localVarHeaderParameter['Content-Type'] = 'application/json'; @@ -10504,55 +10754,6 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(authenticateOAuthRequest, localVarRequestOptions, configuration) - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. - * @summary Retrieve player by token. - * @param {OAuthProvider} provider OAuth provider - * @param {OAuthRequest} oAuthRequest - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - verifyOAuthToken: async (provider: OAuthProvider, oAuthRequest: OAuthRequest, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'provider' is not null or undefined - assertParamExists('verifyOAuthToken', 'provider', provider) - // verify required parameter 'oAuthRequest' is not null or undefined - assertParamExists('verifyOAuthToken', 'oAuthRequest', oAuthRequest) - const localVarPath = `/iam/v1/oauth/{provider}/verify` - .replace(`{${"provider"}}`, encodeURIComponent(String(provider))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication sk required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - // authentication pk required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(oAuthRequest, localVarRequestOptions, configuration) - return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -10599,7 +10800,7 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { * @deprecated * @throws {RequiredError} */ - async authorizeWithOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async authorizeWithOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeWithOAuthToken(provider, oAuthRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -10714,14 +10915,16 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * Verifies the token generated by Openfort Auth. - * @summary Verify auth token. - * @param {string} token Specifies the auth token. + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async verifyAuthToken(token: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.verifyAuthToken(token, options); + async verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuth(provider, oAuthRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -10731,21 +10934,8 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async verifyOAuth(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuth(authenticateOAuthRequest, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, - /** - * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. - * @summary Retrieve player by token. - * @param {OAuthProvider} provider OAuth provider - * @param {OAuthRequest} oAuthRequest - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - async verifyOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuthToken(provider, oAuthRequest, options); + async verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.verifyOAuthToken(authenticateOAuthRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -10787,7 +10977,7 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, * @deprecated * @throws {RequiredError} */ - authorizeWithOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: any): AxiosPromise { + authorizeWithOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: any): AxiosPromise { return localVarFp.authorizeWithOAuthToken(provider, oAuthRequest, options).then((request) => request(axios, basePath)); }, /** @@ -10891,14 +11081,16 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, return localVarFp.unlinkSIWE(sIWERequest, options).then((request) => request(axios, basePath)); }, /** - * Verifies the token generated by Openfort Auth. - * @summary Verify auth token. - * @param {string} token Specifies the auth token. + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - verifyAuthToken(token: string, options?: any): AxiosPromise { - return localVarFp.verifyAuthToken(token, options).then((request) => request(axios, basePath)); + verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: any): AxiosPromise { + return localVarFp.verifyOAuth(provider, oAuthRequest, options).then((request) => request(axios, basePath)); }, /** * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. @@ -10907,20 +11099,8 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - verifyOAuth(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: any): AxiosPromise { - return localVarFp.verifyOAuth(authenticateOAuthRequest, options).then((request) => request(axios, basePath)); - }, - /** - * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. - * @summary Retrieve player by token. - * @param {OAuthProvider} provider OAuth provider - * @param {OAuthRequest} oAuthRequest - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - */ - verifyOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: any): AxiosPromise { - return localVarFp.verifyOAuthToken(provider, oAuthRequest, options).then((request) => request(axios, basePath)); + verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: any): AxiosPromise { + return localVarFp.verifyOAuthToken(authenticateOAuthRequest, options).then((request) => request(axios, basePath)); }, }; }; @@ -11091,15 +11271,17 @@ export class AuthenticationApi extends BaseAPI { } /** - * Verifies the token generated by Openfort Auth. - * @summary Verify auth token. - * @param {string} token Specifies the auth token. + * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. + * @summary Retrieve player by token. + * @param {OAuthProvider} provider OAuth provider + * @param {OAuthRequest} oAuthRequest * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AuthenticationApi */ - public verifyAuthToken(token: string, options?: AxiosRequestConfig) { - return AuthenticationApiFp(this.configuration).verifyAuthToken(token, options).then((request) => request(this.axios, this.basePath)); + public verifyOAuth(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig) { + return AuthenticationApiFp(this.configuration).verifyOAuth(provider, oAuthRequest, options).then((request) => request(this.axios, this.basePath)); } /** @@ -11110,22 +11292,8 @@ export class AuthenticationApi extends BaseAPI { * @throws {RequiredError} * @memberof AuthenticationApi */ - public verifyOAuth(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig) { - return AuthenticationApiFp(this.configuration).verifyOAuth(authenticateOAuthRequest, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * The endpoint verifies the token generated by OAuth provider and retrieves a corresponding player. - * @summary Retrieve player by token. - * @param {OAuthProvider} provider OAuth provider - * @param {OAuthRequest} oAuthRequest - * @param {*} [options] Override http request option. - * @deprecated - * @throws {RequiredError} - * @memberof AuthenticationApi - */ - public verifyOAuthToken(provider: OAuthProvider, oAuthRequest: OAuthRequest, options?: AxiosRequestConfig) { - return AuthenticationApiFp(this.configuration).verifyOAuthToken(provider, oAuthRequest, options).then((request) => request(this.axios, this.basePath)); + public verifyOAuthToken(authenticateOAuthRequest: AuthenticateOAuthRequest, options?: AxiosRequestConfig) { + return AuthenticationApiFp(this.configuration).verifyOAuthToken(authenticateOAuthRequest, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/src/index.ts b/src/index.ts index f5a2b9c..ead132b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,7 @@ import Openfort from "./openfort"; -export * from "./key-pair"; export * from "./openfort"; -export * from "./openfortAuth"; -export * from "./signer/signer"; -export * from "./signer/session.signer"; -export * from "./signer/embedded.signer"; +export * from "./recovery/passwordRecovery"; +export {OAuthProvider, TransactionIntentResponse, SessionResponse} from "./generated/api"; export default Openfort; diff --git a/src/openfort.ts b/src/openfort.ts index f457584..8145124 100644 --- a/src/openfort.ts +++ b/src/openfort.ts @@ -1,55 +1,146 @@ import { Configuration, + OAuthProvider, SessionResponse, SessionsApi, TransactionIntentResponse, TransactionIntentsApi, } from "./generated"; -import {Signer} from "./signer/signer"; +import {ISigner, SignerType} from "./signer/signer"; +import {Auth, InitAuthResponse, OpenfortAuth} from "./openfortAuth"; +import {AuthTokenStorageKey, IStorage, PlayerIDStorageKey, RefreshTokenStorageKey} from "./storage/storage"; +import {LocalStorage} from "./storage/local-storage"; +import {SessionSigner} from "./signer/session.signer"; +import {EmbeddedSigner} from "./signer/embedded.signer"; +import {IRecovery} from "./recovery/recovery"; export default class Openfort { - private readonly _configuration: Configuration; + private _signer?: ISigner; + private readonly _publishableKey: string; + private readonly _openfortAuth: OpenfortAuth; + private readonly _storage: IStorage; + private _sessionsApi?: SessionsApi; private _transactionsApi?: TransactionIntentsApi; - private readonly _signer?: Signer; - constructor(publishableKey: string, signer: Signer = undefined, basePath: string = undefined) { - this._configuration = new Configuration({accessToken: publishableKey, basePath}); + constructor(publishableKey: string, basePath: string = undefined) { + this._publishableKey = publishableKey; + this._openfortAuth = new OpenfortAuth(publishableKey, basePath); + this._storage = new LocalStorage(); + const configuration = new Configuration({accessToken: publishableKey, basePath}); + this._sessionsApi = new SessionsApi(configuration); + this._transactionsApi = new TransactionIntentsApi(configuration); + } + + public async logout(): Promise { + if (this.isAuthenticated()) { + await this._openfortAuth.logout(this._storage.get(RefreshTokenStorageKey)); + } + this._storage.remove(AuthTokenStorageKey); + this._storage.remove(RefreshTokenStorageKey); + this._storage.remove(PlayerIDStorageKey); + if (this._signer) { + await this._signer.logout(); + } + } + + public configureSessionKey(): SessionKey { + const signer = new SessionSigner(this._storage); this._signer = signer; + + const publicKey = signer.loadKeys(); + if (!publicKey) { + const newPublicKey = signer.generateKeys(); + return {publicKey: newPublicKey, isRegistered: false}; + } + + return {publicKey, isRegistered: true}; } - protected get sessionsApi(): SessionsApi { - if (!this._sessionsApi) { - this._sessionsApi = new SessionsApi(this._configuration); + public configureEmbeddedSigner(chainId: number, iframeURL: string = undefined): void { + if (!this.isAuthenticated()) { + throw new NotLoggedIn("Must be logged in to configure embedded signer"); + } + + const signer = new EmbeddedSigner(chainId, this._publishableKey, this._storage, iframeURL); + this._signer = signer; + + if (!signer.isLoaded()) { + throw new MissingRecoveryMethod( + "This device has not been configured, in order to recover your account or create a new one you must provide recovery method", + ); } - return this._sessionsApi; } - protected get transactionsApi(): TransactionIntentsApi { - if (!this._transactionsApi) { - this._transactionsApi = new TransactionIntentsApi(this._configuration); + public async configureEmbeddedSignerRecovery(recovery: IRecovery): Promise { + if (!this._signer) { + throw new EmbeddedNotConfigured("No embedded signer configured"); } - return this._transactionsApi; + + if (this._signer.getSingerType() !== SignerType.EMBEDDED) { + throw new EmbeddedNotConfigured("Signer must be embedded signer"); + } + + const embeddedSigner = this._signer as EmbeddedSigner; + embeddedSigner.setRecovery(recovery); + + await this.validateAndRefreshToken(); + await embeddedSigner.ensureEmbeddedAccount(); + } + + public async loginWithEmailPassword(email: string, password: string): Promise { + const result = await this._openfortAuth.authorizeWithEmailPassword(email, password); + this.storeCredentials(result); + return result.accessToken; + } + + public async loginWithOAuthToken(provider: OAuthProvider, token: string): Promise { + const result = await this._openfortAuth.authorizeWithOAuthToken(provider, token); + this.storeCredentials(result); + return result.accessToken; + } + + public async signUpWithEmailPassword(email: string, password: string): Promise { + const result = await this._openfortAuth.signUp(email, password); + this.storeCredentials(result); + return result.accessToken; + } + + public async initOAuth(provider: OAuthProvider): Promise { + return await this._openfortAuth.getAuthenticationURL(provider); + } + + public async authenticateOAuth(provider: OAuthProvider, key: string): Promise { + const result = await this._openfortAuth.GetTokenAfterSocialLogin(provider, key); + this.storeCredentials(result); + return result.accessToken; + } + + private storeCredentials(auth: Auth): void { + this._storage.save(AuthTokenStorageKey, auth.accessToken); + this._storage.save(RefreshTokenStorageKey, auth.refreshToken); + this._storage.save(PlayerIDStorageKey, auth.player); } public async sendSignatureTransactionIntentRequest( transactionIntentId: string, - userOp?: string, - signature?: string, - optimistic?: boolean, + userOp: string = null, + signature: string = null, ): Promise { - if (!signature && userOp) { + if (!signature) { + if (!userOp) { + throw new NothingToSign("No user operation or signature provided"); + } + if (!this._signer) { - throw new Error("No signer nor signature provided"); + throw new NoSignerConfigured("In order to sign a transaction intent, a signer must be configured"); } + await this.validateAndRefreshToken(); signature = await this._signer.sign(userOp); } - if (!signature) { - throw new Error("No signature provided"); - } - const result = await this.transactionsApi.signature(transactionIntentId, {signature, optimistic}); + const result = await this._transactionsApi.signature(transactionIntentId, {signature}); return result.data; } @@ -65,7 +156,92 @@ export default class Openfort { signature = await this._signer.sign(sessionId); } - const result = await this.sessionsApi.signatureSession(sessionId, {signature, optimistic}); + const result = await this._sessionsApi.signatureSession(sessionId, {signature, optimistic}); return result.data; } + + public isAuthenticated() { + const token = this._storage.get(AuthTokenStorageKey); + const refreshToken = this._storage.get(RefreshTokenStorageKey); + const playerId = this._storage.get(PlayerIDStorageKey); + return token && refreshToken && playerId; + } + + public getAccessToken(): string { + return this._storage.get(AuthTokenStorageKey); + } + + public isLoaded(): boolean { + if (!this._openfortAuth.jwks) { + return false; + } + + if (this._signer && this._signer.getSingerType() === SignerType.EMBEDDED) { + return (this._signer as EmbeddedSigner).iFrameLoaded(); + } + + return true; + } + + private async validateAndRefreshToken() { + if (!this.isAuthenticated()) { + return; + } + + const auth = await this._openfortAuth.verifyAndRefreshToken( + this._storage.get(AuthTokenStorageKey), + this._storage.get(RefreshTokenStorageKey), + ); + if (auth.accessToken !== this._storage.get(AuthTokenStorageKey)) { + this.storeCredentials(auth); + } + if (this._signer && this._signer.useCredentials()) { + this._signer.updateAuthentication(); + } + } +} + +export type SessionKey = { + publicKey: string; + isRegistered: boolean; +}; + +class NotLoggedIn extends Error { + constructor(message: string) { + super(message); + this.name = "NotLoggedIn"; + Object.setPrototypeOf(this, NotLoggedIn.prototype); + } +} + +class MissingRecoveryMethod extends Error { + constructor(message: string) { + super(message); + this.name = "MissingRecoveryMethod"; + Object.setPrototypeOf(this, MissingRecoveryMethod.prototype); + } +} + +class EmbeddedNotConfigured extends Error { + constructor(message: string) { + super(message); + this.name = "EmbeddedNotConfigured"; + Object.setPrototypeOf(this, EmbeddedNotConfigured.prototype); + } +} + +class NoSignerConfigured extends Error { + constructor(message: string) { + super(message); + this.name = "NoSignerConfigured"; + Object.setPrototypeOf(this, NoSignerConfigured.prototype); + } +} + +class NothingToSign extends Error { + constructor(message: string) { + super(message); + this.name = "NothingToSign"; + Object.setPrototypeOf(this, NothingToSign.prototype); + } } diff --git a/src/openfortAuth.ts b/src/openfortAuth.ts index a4fc127..c966551 100644 --- a/src/openfortAuth.ts +++ b/src/openfortAuth.ts @@ -1,5 +1,5 @@ -import {Configuration, OAuthProvider, AuthenticationApi, AuthResponse} from "./generated"; -import {errors, importJWK, jwtVerify} from "jose"; +import {Configuration, OAuthProvider, AuthenticationApi} from "./generated"; +import {errors, importJWK, jwtVerify, KeyLike} from "jose"; export type Auth = { player: string; @@ -7,30 +7,83 @@ export type Auth = { refreshToken: string; }; +export type InitAuthResponse = { + url: string; + key: string; +}; + export class OpenfortAuth { private readonly _configuration: Configuration; private _oauthApi?: AuthenticationApi; private readonly _publishableKey: string; + private _jwks: KeyLike | Uint8Array; constructor(publishableKey: string, basePath?: string) { this._configuration = new Configuration({accessToken: publishableKey, basePath}); this._oauthApi = new AuthenticationApi(this._configuration); this._publishableKey = publishableKey; + + this.getJwks().then((jwtks) => { + this._jwks = jwtks; + }); + } + + public get jwks(): KeyLike | Uint8Array { + return this._jwks; } - public async authorizeWithOAuthToken(provider: OAuthProvider, token: string): Promise { + public async authorizeWithOAuthToken(provider: OAuthProvider, token: string): Promise { const result = await this._oauthApi.authenticateOAuth({provider, token}); - return result.data; + return { + player: result.data.player.id, + accessToken: result.data.token, + refreshToken: result.data.refreshToken, + }; } - public async verifyAndRefreshToken(token: string, refreshToken: string): Promise { + public async getAuthenticationURL(provider: OAuthProvider): Promise { + const result = await this._oauthApi.initOAuth({token: "", provider: provider}); + return { + url: result.data.url, + key: result.data.key, + }; + } + + public async GetTokenAfterSocialLogin(provider: OAuthProvider, key: string): Promise { + const result = await this._oauthApi.authenticateOAuth({provider: provider, token: key}); + return { + player: result.data.player.id, + accessToken: result.data.token, + refreshToken: result.data.refreshToken, + }; + } + + public async authorizeWithEmailPassword(email: string, password: string): Promise { + const result = await this._oauthApi.loginEmailPassword({email, password}); + return { + player: result.data.player.id, + accessToken: result.data.token, + refreshToken: result.data.refreshToken, + }; + } + + public async signUp(email: string, password: string): Promise { + const result = await this._oauthApi.signupEmailPassword({name: "", email, password}); + return { + player: result.data.player.id, + accessToken: result.data.token, + refreshToken: result.data.refreshToken, + }; + } + + public async getJwks(): Promise { const jwtks = await this._oauthApi.getJwks(this._publishableKey); if (jwtks.data.keys.length === 0) { throw new Error("No keys found"); } const jwtKey = jwtks.data.keys[0]; - const ecPublicKey = await importJWK( + return await importJWK( { kty: jwtKey.kty, crv: jwtKey.crv, @@ -39,8 +92,11 @@ export class OpenfortAuth { }, jwtKey.alg, ); + } + + public async verifyAndRefreshToken(token: string, refreshToken: string): Promise { try { - const verification = await jwtVerify(token, ecPublicKey); + const verification = await jwtVerify(token, this._jwks); return { player: verification.payload.sub, accessToken: token, @@ -59,4 +115,8 @@ export class OpenfortAuth { } } } + + async logout(refreshToken: string) { + await this._oauthApi.logout({refreshToken}); + } } diff --git a/src/recovery/passwordRecovery.ts b/src/recovery/passwordRecovery.ts new file mode 100644 index 0000000..4bf929e --- /dev/null +++ b/src/recovery/passwordRecovery.ts @@ -0,0 +1,11 @@ +import {IRecovery} from "./recovery"; + +export class PasswordRecovery implements IRecovery { + private readonly recoveryPassword: string; + constructor(recoveryPassword: string) { + this.recoveryPassword = recoveryPassword; + } + public getRecoveryPassword(): string { + return this.recoveryPassword; + } +} diff --git a/src/recovery/recovery.ts b/src/recovery/recovery.ts new file mode 100644 index 0000000..53f1f50 --- /dev/null +++ b/src/recovery/recovery.ts @@ -0,0 +1,3 @@ +export interface IRecovery { + getRecoveryPassword(): string; +} diff --git a/src/signer/embedded.signer.ts b/src/signer/embedded.signer.ts index 71f3a24..08a473e 100644 --- a/src/signer/embedded.signer.ts +++ b/src/signer/embedded.signer.ts @@ -1,21 +1,50 @@ -import {IframeClient} from "../utils/iframe-client"; +import {IframeClient} from "../clients/iframe-client"; import {Bytes} from "@ethersproject/bytes"; -import {Signer} from "./signer"; +import {ISigner, SignerType} from "./signer"; +import {AuthTokenStorageKey, IStorage} from "../storage/storage"; +import {IRecovery} from "../recovery/recovery"; -export class EmbeddedSigner implements Signer { - private readonly _iframeClient: IframeClient; - private readonly _recoverySharePassword?: string; +export class EmbeddedSigner implements ISigner { + private _iframeClient: IframeClient; private _deviceID: string | null = null; + private readonly _publishableKey: string; + private _chainId: number; + private readonly _iframeURL: string | null; + private readonly _storage: IStorage; + private _recovery: IRecovery; - constructor( - chainId: number, - publishableKey: string, - accessToken: string, - recoverySharePassword?: string, - iframeURL?: string, - ) { - this._iframeClient = new IframeClient(publishableKey, accessToken, chainId, iframeURL); - this._recoverySharePassword = recoverySharePassword; + constructor(chainId: number, publishableKey: string, storage: IStorage, iframeURL?: string) { + this._storage = storage; + this._publishableKey = publishableKey; + this._chainId = chainId; + this._iframeURL = iframeURL; + this.configureIframeClient(); + } + + async logout(): Promise { + await this.dispose(); + this._deviceID = null; + this._chainId = 0; + } + useCredentials(): boolean { + return true; + } + updateAuthentication(): void { + this.dispose(); + this.configureIframeClient(); + } + + private configureIframeClient(): void { + this._iframeClient = new IframeClient( + this._publishableKey, + this._storage.get(AuthTokenStorageKey), + this._chainId, + this._iframeURL, + ); + } + + getSingerType(): SignerType { + return SignerType.EMBEDDED; } public async ensureEmbeddedAccount(): Promise { @@ -28,17 +57,30 @@ export class EmbeddedSigner implements Signer { return this._deviceID; } - return await this._iframeClient.createAccount(this._recoverySharePassword); + if (!this._recovery) { + throw new Error("Recovery is not set"); + } + return await this._iframeClient.createAccount(this._recovery.getRecoveryPassword()); } public async sign(message: Bytes | string): Promise { - console.log("Signing message", message); await this.ensureEmbeddedAccount(); - console.log("Signing message after account creation", message); return await this._iframeClient.sign(message as string); } - public dispose(): void { - this._iframeClient.dispose(); + public async dispose(): Promise { + await this._iframeClient.dispose(); + } + + public setRecovery(recovery: IRecovery): void { + this._recovery = recovery; + } + + async isLoaded(): Promise { + return this._deviceID !== null || (await this._iframeClient.getCurrentDevice()) !== ""; + } + + iFrameLoaded(): boolean { + return this._iframeClient.isLoaded(); } } diff --git a/src/signer/session.signer.ts b/src/signer/session.signer.ts index c41e791..7e6d9a9 100644 --- a/src/signer/session.signer.ts +++ b/src/signer/session.signer.ts @@ -1,16 +1,14 @@ -import {KeyPair} from "../key-pair"; +import {KeyPair} from "../crypto/key-pair"; import {Bytes} from "@ethersproject/bytes"; -import {Signer} from "./signer"; +import {ISigner, SignerType} from "./signer"; +import {IStorage, SessionKeyStorageKey} from "../storage/storage"; -export class SessionSigner implements Signer { - private readonly _sessionKey: KeyPair; +export class SessionSigner implements ISigner { + private _sessionKey: KeyPair; + private readonly _storage: IStorage; - constructor() { - this._sessionKey = KeyPair.load(); - if (!this._sessionKey) { - this._sessionKey = new KeyPair(); - this._sessionKey.save(); - } + constructor(storage: IStorage) { + this._storage = storage; } public sign(message: Bytes | string): Promise { @@ -18,4 +16,40 @@ export class SessionSigner implements Signer { resolve(this._sessionKey.sign(message)); }); } + + logout(): Promise { + this._storage.remove(SessionKeyStorageKey); + this._sessionKey = null; + return Promise.resolve(); + } + loadKeys(): string { + if (this._sessionKey !== null) { + return this._sessionKey.getPublicKey(); + } + + const sessionKey = this._storage.get(SessionKeyStorageKey); + if (!sessionKey) { + return null; + } + + this._sessionKey = KeyPair.load(sessionKey); + return this._sessionKey.getPublicKey(); + } + + generateKeys(): string { + this._sessionKey = new KeyPair(); + this._storage.save(SessionKeyStorageKey, this._sessionKey.getPrivateKey()); + return this._sessionKey.getPublicKey(); + } + + getSingerType(): SignerType { + return SignerType.SESSION; + } + + useCredentials(): boolean { + return false; + } + updateAuthentication(): void { + return; + } } diff --git a/src/signer/signer.ts b/src/signer/signer.ts index 0e49f18..82153fa 100644 --- a/src/signer/signer.ts +++ b/src/signer/signer.ts @@ -1,5 +1,14 @@ import {Bytes} from "@ethersproject/bytes"; -export interface Signer { +export enum SignerType { + EMBEDDED = "embedded", + SESSION = "session", +} + +export interface ISigner { sign(message: Bytes | string): Promise; + logout(): Promise; + useCredentials(): boolean; + updateAuthentication(): void; + getSingerType(): SignerType; } diff --git a/src/storage/base-storage.ts b/src/storage/base-storage.ts deleted file mode 100644 index 6a6a97e..0000000 --- a/src/storage/base-storage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {StorageKeys} from "./storage-keys"; - -export interface BaseStorage { - get(key: StorageKeys): string; - save(key: StorageKeys, value: string): void; - remove(key: StorageKeys): void; -} diff --git a/src/storage/local-storage.ts b/src/storage/local-storage.ts index 50925fd..605a609 100644 --- a/src/storage/local-storage.ts +++ b/src/storage/local-storage.ts @@ -1,42 +1,23 @@ -import {BaseStorage} from "./base-storage"; -import {StorageKeys} from "./storage-keys"; +import {IStorage} from "./storage"; -export class LocalStorage implements BaseStorage { - private static readonly _prefix = "OPENFORT"; - private static readonly _separator = "/"; - - public constructor(private readonly name?: string) {} - - public static get isAvailable(): boolean { - return "localStorage" in global && !!global.localStorage; - } - - private formatKey(key: StorageKeys): string { - return [LocalStorage._prefix, this.name, key].filter((n) => n).join(LocalStorage._separator); - } - - private static get localStorage(): LocalStorageInterface { - if (LocalStorage.isAvailable) { - return global.localStorage as LocalStorageInterface; +export class LocalStorage implements IStorage { + private validateLocalStorage(): void { + if (!("localStorage" in global && !!global.localStorage)) { + throw new Error("Local storage is not available"); } - throw Error("Local storage is not available in the current context"); } - - public get(key: StorageKeys): string { - return LocalStorage.localStorage.getItem(this.formatKey(key)); + public get(key: string): string { + this.validateLocalStorage(); + return localStorage.getItem(key); } - public save(key: StorageKeys, value: string): void { - LocalStorage.localStorage.setItem(this.formatKey(key), value); + public save(key: string, value: string): void { + this.validateLocalStorage(); + localStorage.setItem(key, value); } - public remove(key: StorageKeys): void { - LocalStorage.localStorage.removeItem(this.formatKey(key)); + public remove(key: string): void { + this.validateLocalStorage(); + localStorage.removeItem(key); } } - -interface LocalStorageInterface { - getItem(key: string): string | null; - setItem(key: string, value: string): void; - removeItem(key: string): void; -} diff --git a/src/storage/storage-keys.ts b/src/storage/storage-keys.ts deleted file mode 100644 index 73ce9c9..0000000 --- a/src/storage/storage-keys.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum StorageKeys { - SESSION_KEY = "SESSION-KEY", - SESSION_ID = "SESSION-ID", -} diff --git a/src/storage/storage.ts b/src/storage/storage.ts new file mode 100644 index 0000000..3acbf92 --- /dev/null +++ b/src/storage/storage.ts @@ -0,0 +1,10 @@ +export const AuthTokenStorageKey = "openfort.auth_token"; +export const RefreshTokenStorageKey = "openfort.refresh_token"; +export const PlayerIDStorageKey = "openfort.player_id"; +export const SessionKeyStorageKey = "openfort.session_key"; + +export interface IStorage { + get(key: string): string; + save(key: string, value: string): void; + remove(key: string): void; +} diff --git a/src/version.ts b/src/version.ts index 8d168bd..24ef830 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const VERSION = "0.4.6"; +export const VERSION = "0.5.0"; export const PACKAGE = "@openfort/openfort-js";