Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
168 changes: 130 additions & 38 deletions packages/@magic-ext/oauth2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import {
OAuthPopupConfiguration,
OAuthVerificationConfiguration,
} from './types';
import { OAuthPopupEventEmit, OAuthPopupEventHandlers, OAuthPopupEventOnReceived } from '@magic-sdk/types';
import {
OAuthMFAEventEmit,
OAuthMFAEventOnReceived,
OAuthPopupEventEmit,
OAuthPopupEventHandlers,
OAuthPopupEventOnReceived,
OAuthGetResultEventHandlers,
} from '@magic-sdk/types';

declare global {
interface Window {
Expand Down Expand Up @@ -83,13 +90,15 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> {
const urlWithoutQuery = window.location.origin + window.location.pathname;
window.history.replaceState(null, '', urlWithoutQuery);

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

public loginWithPopup(configuration: OAuthPopupConfiguration) {
const { showMfaModal } = configuration;
const requestPayload = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Popup, [
{
...configuration,
showUI: showMfaModal,
returnTo: window.location.href,
apiKey: this.sdk.apiKey,
platform: 'web',
Expand All @@ -102,33 +111,145 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> {
requestPayload,
);

/**
* Attach Event listeners
*/
const redirectEvent = (event: MessageEvent) => {
this.createIntermediaryEvent(OAuthPopupEventEmit.PopupEvent, requestPayload.id as string)(event.data);
};

if (configuration.shouldReturnURI) {
if (configuration.shouldReturnURI && oauthPopupRequest) {
oauthPopupRequest.on(OAuthPopupEventOnReceived.PopupUrl, popupUrl => {
window.addEventListener('message', redirectEvent);
promiEvent.emit(OAuthPopupEventOnReceived.PopupUrl, popupUrl);
});
}

if (!showMfaModal) {
oauthPopupRequest.on(OAuthMFAEventOnReceived.MfaSentHandle, () => {
promiEvent.emit(OAuthMFAEventOnReceived.MfaSentHandle);
});
oauthPopupRequest.on(OAuthMFAEventOnReceived.InvalidMfaOtp, () => {
promiEvent.emit(OAuthMFAEventOnReceived.InvalidMfaOtp);
});
oauthPopupRequest.on(OAuthMFAEventOnReceived.RecoveryCodeSentHandle, () => {
promiEvent.emit(OAuthMFAEventOnReceived.RecoveryCodeSentHandle);
});
oauthPopupRequest.on(OAuthMFAEventOnReceived.InvalidRecoveryCode, () => {
promiEvent.emit(OAuthMFAEventOnReceived.InvalidRecoveryCode);
});
oauthPopupRequest.on(OAuthMFAEventOnReceived.RecoveryCodeSuccess, () => {
promiEvent.emit(OAuthMFAEventOnReceived.RecoveryCodeSuccess);
});
}

const result = await oauthPopupRequest;
window.removeEventListener('message', redirectEvent);

if ((result as OAuthRedirectError).error) {
reject(result);
const maybeResult = result as OAuthRedirectResult;
const maybeError = result as OAuthRedirectError;

if (maybeError.error) {
reject(
this.createError<OAuthErrorData>(maybeError.error, maybeError.error_description ?? 'An error occurred.', {
errorURI: maybeError.error_uri,
provider: maybeError.provider,
}),
);
} else {
resolve(result as OAuthRedirectResult);
resolve(maybeResult);
}
} catch (error) {
reject(error);
}
});

promiEvent.on(OAuthPopupEventEmit.Cancel, () => {
this.createIntermediaryEvent(OAuthPopupEventEmit.Cancel, requestPayload.id as string)();
});
if (!showMfaModal && promiEvent) {
promiEvent.on(OAuthMFAEventEmit.VerifyMFACode, (mfa: string) => {
this.createIntermediaryEvent(OAuthMFAEventEmit.VerifyMFACode, requestPayload.id as string)(mfa);
});
promiEvent.on(OAuthMFAEventEmit.LostDevice, () => {
this.createIntermediaryEvent(OAuthMFAEventEmit.LostDevice, requestPayload.id as string)();
});
promiEvent.on(OAuthMFAEventEmit.VerifyRecoveryCode, (recoveryCode: string) => {
this.createIntermediaryEvent(OAuthMFAEventEmit.VerifyRecoveryCode, requestPayload.id as string)(recoveryCode);
});
promiEvent.on(OAuthMFAEventEmit.Cancel, () => {
this.createIntermediaryEvent(OAuthMFAEventEmit.Cancel, requestPayload.id as string)();
});
}

return promiEvent;
}

private getResult(configuration: OAuthVerificationConfiguration, queryString: string) {
const { showMfaModal } = configuration;
const requestPayload = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [
{
authorizationResponseParams: queryString,
magicApiKey: this.sdk.apiKey,
platform: 'web',
showUI: showMfaModal,
...configuration,
},
]);

const promiEvent = this.utils.createPromiEvent<OAuthRedirectResult, OAuthGetResultEventHandlers>(
async (resolve, reject) => {
const getResultRequest = this.request<OAuthRedirectResult | OAuthRedirectError, OAuthGetResultEventHandlers>(
requestPayload,
);

if (!showMfaModal) {
getResultRequest.on(OAuthMFAEventOnReceived.MfaSentHandle, () => {
promiEvent.emit(OAuthMFAEventOnReceived.MfaSentHandle);
});
getResultRequest.on(OAuthMFAEventOnReceived.InvalidMfaOtp, () => {
promiEvent.emit(OAuthMFAEventOnReceived.InvalidMfaOtp);
});
getResultRequest.on(OAuthMFAEventOnReceived.RecoveryCodeSentHandle, () => {
promiEvent.emit(OAuthMFAEventOnReceived.RecoveryCodeSentHandle);
});
getResultRequest.on(OAuthMFAEventOnReceived.InvalidRecoveryCode, () => {
promiEvent.emit(OAuthMFAEventOnReceived.InvalidRecoveryCode);
});
getResultRequest.on(OAuthMFAEventOnReceived.RecoveryCodeSuccess, () => {
promiEvent.emit(OAuthMFAEventOnReceived.RecoveryCodeSuccess);
});
}

// Parse the result, which may contain an OAuth-formatted error.
const resultOrError = await getResultRequest;
const maybeResult = resultOrError as OAuthRedirectResult;
const maybeError = resultOrError as OAuthRedirectError;

if (maybeError.error) {
reject(
this.createError<OAuthErrorData>(maybeError.error, maybeError.error_description ?? 'An error occurred.', {
errorURI: maybeError.error_uri,
provider: maybeError.provider,
}),
);
}

resolve(maybeResult);
},
);

if (!showMfaModal && promiEvent) {
promiEvent.on(OAuthMFAEventEmit.VerifyMFACode, (mfa: string) => {
this.createIntermediaryEvent(OAuthMFAEventEmit.VerifyMFACode, requestPayload.id as string)(mfa);
});
promiEvent.on(OAuthMFAEventEmit.LostDevice, () => {
this.createIntermediaryEvent(OAuthMFAEventEmit.LostDevice, requestPayload.id as string)();
});
promiEvent.on(OAuthMFAEventEmit.VerifyRecoveryCode, (recoveryCode: string) => {
this.createIntermediaryEvent(OAuthMFAEventEmit.VerifyRecoveryCode, requestPayload.id as string)(recoveryCode);
});
promiEvent.on(OAuthMFAEventEmit.Cancel, () => {
this.createIntermediaryEvent(OAuthMFAEventEmit.Cancel, requestPayload.id as string)();
});
}

return promiEvent;
}
Expand Down Expand Up @@ -160,33 +281,4 @@ export class OAuthExtension extends Extension.Internal<'oauth2'> {
}
}

function getResult(this: OAuthExtension, configuration: OAuthVerificationConfiguration, queryString: string) {
return this.utils.createPromiEvent<OAuthRedirectResult>(async (resolve, reject) => {
const parseRedirectResult = this.utils.createJsonRpcRequestPayload(OAuthPayloadMethods.Verify, [
{
authorizationResponseParams: queryString,
magicApiKey: this.sdk.apiKey,
platform: 'web',
...configuration,
},
]);

// Parse the result, which may contain an OAuth-formatted error.
const resultOrError = await this.request<OAuthRedirectResult | OAuthRedirectError>(parseRedirectResult);
const maybeResult = resultOrError as OAuthRedirectResult;
const maybeError = resultOrError as OAuthRedirectError;

if (maybeError.error) {
reject(
this.createError<OAuthErrorData>(maybeError.error, maybeError.error_description ?? 'An error occurred.', {
errorURI: maybeError.error_uri,
provider: maybeError.provider,
}),
);
}

resolve(maybeResult);
});
}

export * from './types';
2 changes: 2 additions & 0 deletions packages/@magic-ext/oauth2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface OAuthVerificationConfiguration {
lifespan?: number;
optionalQueryString?: string;
skipDIDToken?: boolean;
showMfaModal?: boolean;
}

export interface OAuthPopupConfiguration {
Expand All @@ -117,6 +118,7 @@ export interface OAuthPopupConfiguration {
loginHint?: string;
providerParams?: Record<string, string | number | boolean>;
shouldReturnURI?: boolean;
showMfaModal?: boolean;
}

export enum OAuthErrorCode {
Expand Down
16 changes: 10 additions & 6 deletions packages/@magic-ext/wallet-kit/src/MagicWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { OAuthPendingView } from './views/OAuthPendingView';
import AdditionalProvidersView from './views/AdditionalProvidersView';
import { getExtensionInstance } from './extension';
import { EmailLoginProvider } from './context/EmailLoginContext';
import { OAuthLoginProvider } from './context/OAuthLoginContext';
import { WidgetConfigProvider } from './context/WidgetConfigContext';
import { EmailOTPView } from './views/EmailOTPView';
import { DeviceVerificationView } from './views/DeviceVerificationView';
Expand Down Expand Up @@ -79,6 +80,7 @@ function WidgetContent({
<OAuthPendingView
key={`oauth-${state.selectedProvider}`}
provider={state.selectedProvider as OAuthProvider}
state={state}
dispatch={dispatch}
/>
);
Expand Down Expand Up @@ -109,12 +111,14 @@ function WidgetContent({

return (
<EmailLoginProvider dispatch={dispatch}>
<Modal isWidget fullscreen={isModal && isMobile}>
<VStack width="full" minWidth="380px">
{renderView()}
<Footer showLogo={showFooterLogo} />
</VStack>
</Modal>
<OAuthLoginProvider dispatch={dispatch}>
<Modal isWidget fullscreen={isModal && isMobile}>
<VStack width="full" minWidth="380px">
{renderView()}
<Footer showLogo={showFooterLogo} />
</VStack>
</Modal>
</OAuthLoginProvider>
</EmailLoginProvider>
);
}
Expand Down
Loading