diff --git a/packages/@magic-ext/oauth/src/index.ts b/packages/@magic-ext/oauth/src/index.ts index 958bd9c7c..73848baa9 100644 --- a/packages/@magic-ext/oauth/src/index.ts +++ b/packages/@magic-ext/oauth/src/index.ts @@ -31,7 +31,7 @@ export class OAuthExtension extends Extension.Internal<'oauth'> { }); } - public getRedirectResult() { + public getRedirectResult(lifespan?: number) { const queryString = window.location.search; // Remove the query from the redirect callback as a precaution to prevent @@ -39,7 +39,7 @@ export class OAuthExtension extends Extension.Internal<'oauth'> { const urlWithoutQuery = window.location.origin + window.location.pathname; window.history.replaceState(null, '', urlWithoutQuery); - return getResult.call(this, queryString); + return getResult.call(this, queryString, lifespan); } } @@ -50,7 +50,7 @@ async function createURI(this: OAuthExtension, configuration: OAuthRedirectConfi await this.utils.storage.removeItem(OAUTH_REDIRECT_METADATA_KEY); // Unpack configuration, generate crypto values, and persist to storage. - const { provider, redirectURI, scope, loginHint } = configuration; + const { provider, redirectURI, scope, loginHint, lifespan } = configuration; const { verifier, challenge, state } = await createCryptoChallenge(); /* Stringify for RN Async storage */ @@ -77,6 +77,7 @@ async function createURI(this: OAuthExtension, configuration: OAuthRedirectConfi scope && `scope=${encodeURIComponent(scope.join(' '))}`, redirectURI && `redirect_uri=${encodeURIComponent(redirectURI)}`, loginHint && `login_hint=${encodeURIComponent(loginHint)}`, + lifespan && `lifespan=${encodeURIComponent(lifespan)}`, ].reduce((prev, next) => (next ? `${prev}&${next}` : prev)); return { @@ -86,7 +87,7 @@ async function createURI(this: OAuthExtension, configuration: OAuthRedirectConfi }; } -function getResult(this: OAuthExtension, queryString: string) { +function getResult(this: OAuthExtension, queryString: string, lifespan?: number) { return this.utils.createPromiEvent(async (resolve, reject) => { const json: string = (await this.utils.storage.getItem(OAUTH_REDIRECT_METADATA_KEY)) as string; @@ -99,6 +100,7 @@ function getResult(this: OAuthExtension, queryString: string) { queryString, verifier, state, + lifespan, ]); // Parse the result, which may contain an OAuth-formatted error. diff --git a/packages/@magic-ext/oauth/src/types.ts b/packages/@magic-ext/oauth/src/types.ts index 490dbe138..c60d8d060 100644 --- a/packages/@magic-ext/oauth/src/types.ts +++ b/packages/@magic-ext/oauth/src/types.ts @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration { redirectURI: string; scope?: string[]; loginHint?: string; + lifespan?: number; } export enum OAuthErrorCode { diff --git a/packages/@magic-ext/oauth2/src/index.ts b/packages/@magic-ext/oauth2/src/index.ts index 3f5e8b280..5b9912750 100644 --- a/packages/@magic-ext/oauth2/src/index.ts +++ b/packages/@magic-ext/oauth2/src/index.ts @@ -54,7 +54,7 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { }); } - public getRedirectResult() { + public getRedirectResult(lifespan?: number) { const queryString = window.location.search; // Remove the query from the redirect callback as a precaution to prevent @@ -62,17 +62,18 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> { const urlWithoutQuery = window.location.origin + window.location.pathname; window.history.replaceState(null, '', urlWithoutQuery); - return getResult.call(this, queryString); + return getResult.call(this, queryString, lifespan); } } -function getResult(this: OAuthExtension, queryString: string) { +function getResult(this: OAuthExtension, queryString: string, lifespan?: number) { return this.utils.createPromiEvent(async (resolve, reject) => { const parseRedirectResult = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [ { authorizationResponseParams: queryString, magicApiKey: this.sdk.apiKey, platform: 'web', + lifespan, }, ]); diff --git a/packages/@magic-ext/oauth2/src/types.ts b/packages/@magic-ext/oauth2/src/types.ts index 8678044a0..22f73de9f 100644 --- a/packages/@magic-ext/oauth2/src/types.ts +++ b/packages/@magic-ext/oauth2/src/types.ts @@ -96,6 +96,7 @@ export interface OAuthRedirectConfiguration { redirectURI: string; scope?: string[]; loginHint?: string; + lifespan?: string; } export enum OAuthErrorCode { diff --git a/packages/@magic-ext/react-native-bare-oauth/src/index.ts b/packages/@magic-ext/react-native-bare-oauth/src/index.ts index 28c723709..d1b91eb8b 100644 --- a/packages/@magic-ext/react-native-bare-oauth/src/index.ts +++ b/packages/@magic-ext/react-native-bare-oauth/src/index.ts @@ -57,7 +57,7 @@ export async function createURI(this: OAuthExtension, configuration: OAuthRedire await this.utils.storage.removeItem(OAUTH_REDIRECT_METADATA_KEY); // Unpack configuration, generate crypto values, and persist to storage. - const { provider, redirectURI, scope, loginHint } = configuration; + const { provider, redirectURI, scope, loginHint, lifespan } = configuration; const { verifier, challenge, state } = await createCryptoChallenge(); const bundleId = getBundleId(); @@ -88,6 +88,7 @@ export async function createURI(this: OAuthExtension, configuration: OAuthRedire redirectURI && `redirect_uri=${encodeURIComponent(redirectURI)}`, loginHint && `login_hint=${encodeURIComponent(loginHint)}`, bundleId && `bundleId=${encodeURIComponent(bundleId)}`, + lifespan && `lifespan=${encodeURIComponent(lifespan)}`, ].reduce((prev, next) => (next ? `${prev}&${next}` : prev)); return { diff --git a/packages/@magic-ext/react-native-bare-oauth/src/types.ts b/packages/@magic-ext/react-native-bare-oauth/src/types.ts index 490dbe138..fe8f45f17 100644 --- a/packages/@magic-ext/react-native-bare-oauth/src/types.ts +++ b/packages/@magic-ext/react-native-bare-oauth/src/types.ts @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration { redirectURI: string; scope?: string[]; loginHint?: string; + lifespan?: string; } export enum OAuthErrorCode { diff --git a/packages/@magic-ext/react-native-expo-oauth/src/index.ts b/packages/@magic-ext/react-native-expo-oauth/src/index.ts index fe818090a..de59e12cf 100644 --- a/packages/@magic-ext/react-native-expo-oauth/src/index.ts +++ b/packages/@magic-ext/react-native-expo-oauth/src/index.ts @@ -57,7 +57,7 @@ export async function createURI(this: OAuthExtension, configuration: OAuthRedire await this.utils.storage.removeItem(OAUTH_REDIRECT_METADATA_KEY); // Unpack configuration, generate crypto values, and persist to storage. - const { provider, redirectURI, scope, loginHint } = configuration; + const { provider, redirectURI, scope, loginHint, lifespan } = configuration; const { verifier, challenge, state } = await createCryptoChallenge(); const bundleId = Application.applicationId; @@ -88,6 +88,7 @@ export async function createURI(this: OAuthExtension, configuration: OAuthRedire redirectURI && `redirect_uri=${encodeURIComponent(redirectURI)}`, loginHint && `login_hint=${encodeURIComponent(loginHint)}`, bundleId && `bundleId=${encodeURIComponent(bundleId)}`, + lifespan && `lifespan=${encodeURIComponent(lifespan)}`, ].reduce((prev, next) => (next ? `${prev}&${next}` : prev)); return { diff --git a/packages/@magic-ext/react-native-expo-oauth/src/types.ts b/packages/@magic-ext/react-native-expo-oauth/src/types.ts index 490dbe138..fe8f45f17 100644 --- a/packages/@magic-ext/react-native-expo-oauth/src/types.ts +++ b/packages/@magic-ext/react-native-expo-oauth/src/types.ts @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration { redirectURI: string; scope?: string[]; loginHint?: string; + lifespan?: string; } export enum OAuthErrorCode { diff --git a/packages/@magic-sdk/provider/src/modules/auth.ts b/packages/@magic-sdk/provider/src/modules/auth.ts index 91d90c639..83fdc9846 100644 --- a/packages/@magic-sdk/provider/src/modules/auth.ts +++ b/packages/@magic-sdk/provider/src/modules/auth.ts @@ -11,6 +11,7 @@ import { UpdateEmailEventHandlers, UpdateEmailEventEmit, RecencyCheckEventEmit, + LoginWithCredentialConfiguration, } from '@magic-sdk/types'; import { BaseModule } from './base-module'; import { createJsonRpcRequestPayload } from '../core/json-rpc'; @@ -51,11 +52,11 @@ export class AuthModule extends BaseModule { }).log(); } - const { email, showUI = true, redirectURI, overrides } = configuration; + const { email, showUI = true, redirectURI, overrides, lifespan } = configuration; const requestPayload = createJsonRpcRequestPayload( this.sdk.testMode ? MagicPayloadMethod.LoginWithMagicLinkTestMode : MagicPayloadMethod.LoginWithMagicLink, - [{ email, showUI, redirectURI, overrides }], + [{ email, showUI, redirectURI, overrides, lifespan }], ); return this.request(requestPayload); } @@ -66,10 +67,10 @@ export class AuthModule extends BaseModule { * of 15 minutes) */ public loginWithSMS(configuration: LoginWithSmsConfiguration) { - const { phoneNumber } = configuration; + const { phoneNumber, lifespan } = configuration; const requestPayload = createJsonRpcRequestPayload( this.sdk.testMode ? MagicPayloadMethod.LoginWithSmsTestMode : MagicPayloadMethod.LoginWithSms, - [{ phoneNumber, showUI: true }], + [{ phoneNumber, showUI: true, lifespan }], ); return this.request(requestPayload); } @@ -80,10 +81,10 @@ export class AuthModule extends BaseModule { * of 15 minutes) */ public loginWithEmailOTP(configuration: LoginWithEmailOTPConfiguration) { - const { email, showUI, deviceCheckUI, overrides } = configuration; + const { email, showUI, deviceCheckUI, overrides, lifespan } = configuration; const requestPayload = createJsonRpcRequestPayload( this.sdk.testMode ? MagicPayloadMethod.LoginWithEmailOTPTestMode : MagicPayloadMethod.LoginWithEmailOTP, - [{ email, showUI, deviceCheckUI, overrides }], + [{ email, showUI, deviceCheckUI, overrides, lifespan }], ); const handle = this.request(requestPayload); if (!deviceCheckUI && handle) { @@ -112,7 +113,8 @@ export class AuthModule extends BaseModule { * If no argument is provided, a credential is automatically parsed from * `window.location.search`. */ - public loginWithCredential(credentialOrQueryString?: string) { + public loginWithCredential(configuration?: LoginWithCredentialConfiguration) { + const { credentialOrQueryString, lifespan } = configuration || {}; let credentialResolved = credentialOrQueryString ?? ''; if (!credentialOrQueryString && SDKEnvironment.platform === 'web') { @@ -125,7 +127,7 @@ export class AuthModule extends BaseModule { const requestPayload = createJsonRpcRequestPayload( this.sdk.testMode ? MagicPayloadMethod.LoginWithCredentialTestMode : MagicPayloadMethod.LoginWithCredential, - [credentialResolved], + [credentialResolved, lifespan], ); return this.request(requestPayload); diff --git a/packages/@magic-sdk/provider/test/spec/modules/auth/loginWithCredential.spec.ts b/packages/@magic-sdk/provider/test/spec/modules/auth/loginWithCredential.spec.ts index 2d96c6eb3..8719cc628 100644 --- a/packages/@magic-sdk/provider/test/spec/modules/auth/loginWithCredential.spec.ts +++ b/packages/@magic-sdk/provider/test/spec/modules/auth/loginWithCredential.spec.ts @@ -11,19 +11,19 @@ beforeEach(() => { jest.restoreAllMocks(); }); -test('Generates JSON RPC request payload with the given parameter as the credential', async () => { +test('Generates JSON RPC request payload with the given parameters as the credential and lifespan', async () => { const magic = createMagicSDK({ platform: 'web' }); magic.auth.request = jest.fn(); - await magic.auth.loginWithCredential('helloworld'); + await magic.auth.loginWithCredential({ credentialOrQueryString: 'helloworld', lifespan: 900 }); const requestPayload = magic.auth.request.mock.calls[0][0]; expect(requestPayload.jsonrpc).toBe('2.0'); expect(requestPayload.method).toBe(MagicPayloadMethod.LoginWithCredential); - expect(requestPayload.params).toEqual(['helloworld']); + expect(requestPayload.params).toEqual(['helloworld', 900]); }); -test('If no parameter is given & platform target is "web", URL search string is included in the payload params', async () => { +test('If no parameters are given & platform target is "web", URL search string and default lifespan are included in the payload params', async () => { const magic = createMagicSDK({ platform: 'web' }); magic.auth.request = jest.fn(); @@ -40,10 +40,10 @@ test('If no parameter is given & platform target is "web", URL search string is const requestPayload = magic.auth.request.mock.calls[0][0]; expect(requestPayload.jsonrpc).toBe('2.0'); expect(requestPayload.method).toBe(MagicPayloadMethod.LoginWithCredential); - expect(requestPayload.params).toEqual(['?magic_credential=asdf']); + expect(requestPayload.params).toEqual(['?magic_credential=asdf', undefined]); }); -test('If no parameter is given & platform target is NOT "web", credential is empty string', async () => { +test('If no parameters are given & platform target is NOT "web", credential is empty string and default lifespan is included', async () => { const magic = createMagicSDK({ platform: 'react-native' }); magic.auth.request = jest.fn(); @@ -52,22 +52,22 @@ test('If no parameter is given & platform target is NOT "web", credential is emp const requestPayload = magic.auth.request.mock.calls[0][0]; expect(requestPayload.jsonrpc).toBe('2.0'); expect(requestPayload.method).toBe(MagicPayloadMethod.LoginWithCredential); - expect(requestPayload.params).toEqual(['']); + expect(requestPayload.params).toEqual(['', undefined]); }); -test('If `testMode` is enabled, testing-specific RPC method is used', async () => { +test('If `testMode` is enabled, testing-specific RPC method is used with given parameters', async () => { const magic = createMagicSDKTestMode({ platform: 'web' }); magic.auth.request = jest.fn(); - await magic.auth.loginWithCredential('helloworld'); + await magic.auth.loginWithCredential({ credentialOrQueryString: 'helloworld', lifespan: 900 }); const requestPayload = magic.auth.request.mock.calls[0][0]; expect(requestPayload.jsonrpc).toBe('2.0'); expect(requestPayload.method).toBe(MagicPayloadMethod.LoginWithCredentialTestMode); - expect(requestPayload.params).toEqual(['helloworld']); + expect(requestPayload.params).toEqual(['helloworld', 900]); }); test('method should return a PromiEvent', () => { const magic = createMagicSDK({ platform: 'web' }); - expect(isPromiEvent(magic.auth.loginWithCredential('asdf'))).toBeTruthy(); + expect(isPromiEvent(magic.auth.loginWithCredential({ credentialOrQueryString: 'asdf' }))).toBeTruthy(); }); diff --git a/packages/@magic-sdk/types/src/modules/auth-types.ts b/packages/@magic-sdk/types/src/modules/auth-types.ts index e69a3e23e..41c8e33e4 100644 --- a/packages/@magic-sdk/types/src/modules/auth-types.ts +++ b/packages/@magic-sdk/types/src/modules/auth-types.ts @@ -29,6 +29,11 @@ export interface LoginWithMagicLinkConfiguration { overrides?: { variation?: string; }; + + /** + * The number of seconds until the generated Decenteralized ID token will expire. + */ + lifespan?: number; } export interface LoginWithSmsConfiguration { @@ -36,6 +41,11 @@ export interface LoginWithSmsConfiguration { * Specify the phone number of the user attempting to login. */ phoneNumber: string; + + /** + * The number of seconds until the generated Decenteralized ID token will expire. + */ + lifespan?: number; } export interface LoginWithEmailOTPConfiguration { /** @@ -74,6 +84,23 @@ export interface LoginWithEmailOTPConfiguration { overrides?: { variation?: string; }; + + /** + * The number of seconds until the generated Decenteralized ID token will expire. + */ + lifespan?: number; +} + +export interface LoginWithCredentialConfiguration { + /** + * A credential token or a valid query string (prefixed with ? or #) + */ + credentialOrQueryString?: string; + + /** + * The number of seconds until the generated Decenteralized ID token will expire. + */ + lifespan?: number; } /**