Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add lifespan parameter to login methods #771

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/@magic-ext/oauth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ 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
// malicious parties from parsing it before we have a chance to use it.
const urlWithoutQuery = window.location.origin + window.location.pathname;
window.history.replaceState(null, '', urlWithoutQuery);

return getResult.call(this, queryString);
return getResult.call(this, queryString, lifespan);
}
}

Expand All @@ -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 */
Expand All @@ -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 {
Expand All @@ -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<OAuthRedirectResult>(async (resolve, reject) => {
const json: string = (await this.utils.storage.getItem(OAUTH_REDIRECT_METADATA_KEY)) as string;

Expand All @@ -99,6 +100,7 @@ function getResult(this: OAuthExtension, queryString: string) {
queryString,
verifier,
state,
lifespan,
]);

// Parse the result, which may contain an OAuth-formatted error.
Expand Down
1 change: 1 addition & 0 deletions packages/@magic-ext/oauth/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration {
redirectURI: string;
scope?: string[];
loginHint?: string;
lifespan?: number;
}

export enum OAuthErrorCode {
Expand Down
7 changes: 4 additions & 3 deletions packages/@magic-ext/oauth2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,26 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> {
});
}

public getRedirectResult() {
public getRedirectResult(lifespan: number) {
otabek-magic marked this conversation as resolved.
Show resolved Hide resolved
const queryString = window.location.search;

// Remove the query from the redirect callback as a precaution to prevent
// malicious parties from parsing it before we have a chance to use it.
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) {
otabek-magic marked this conversation as resolved.
Show resolved Hide resolved
return this.utils.createPromiEvent<OAuthRedirectResult>(async (resolve, reject) => {
const parseRedirectResult = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [
{
authorizationResponseParams: queryString,
magicApiKey: this.sdk.apiKey,
platform: 'web',
lifespan,
},
]);

Expand Down
1 change: 1 addition & 0 deletions packages/@magic-ext/oauth2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface OAuthRedirectConfiguration {
redirectURI: string;
scope?: string[];
loginHint?: string;
lifespan?: string;
}

export enum OAuthErrorCode {
Expand Down
3 changes: 2 additions & 1 deletion packages/@magic-ext/react-native-bare-oauth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/@magic-ext/react-native-bare-oauth/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration {
redirectURI: string;
scope?: string[];
loginHint?: string;
lifespan?: string;
}

export enum OAuthErrorCode {
Expand Down
3 changes: 2 additions & 1 deletion packages/@magic-ext/react-native-expo-oauth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/@magic-ext/react-native-expo-oauth/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface OAuthRedirectConfiguration {
redirectURI: string;
scope?: string[];
loginHint?: string;
lifespan?: string;
}

export enum OAuthErrorCode {
Expand Down
18 changes: 10 additions & 8 deletions packages/@magic-sdk/provider/src/modules/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UpdateEmailEventHandlers,
UpdateEmailEventEmit,
RecencyCheckEventEmit,
LoginWithCredentialConfiguration,
} from '@magic-sdk/types';
import { BaseModule } from './base-module';
import { createJsonRpcRequestPayload } from '../core/json-rpc';
Expand Down Expand Up @@ -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<string | null, LoginWithMagicLinkEventHandlers>(requestPayload);
}
Expand All @@ -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<string | null>(requestPayload);
}
Expand All @@ -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<string | null, LoginWithEmailOTPEventHandlers>(requestPayload);
if (!deviceCheckUI && handle) {
Expand Down Expand Up @@ -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') {
Expand All @@ -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<string | null>(requestPayload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -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();
});
27 changes: 27 additions & 0 deletions packages/@magic-sdk/types/src/modules/auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@ export interface LoginWithMagicLinkConfiguration {
overrides?: {
variation?: string;
};

/**
* The number of seconds until the generated Decenteralized ID token will expire.
*/
lifespan?: number;
}

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 {
/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down
Loading