Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
38 changes: 38 additions & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@imtbl/auth",
"version": "0.0.0",
"description": "Authentication SDK for Immutable",
"main": "dist/node/index.js",
"module": "dist/node/index.mjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": {
"browser": "./dist/browser/index.mjs",
"default": "./dist/node/index.mjs"
},
"require": "./dist/node/index.js"
}
},
"scripts": {
"build": "pnpm transpile && pnpm typegen",
"transpile": "tsup src/index.ts --config ../../tsup.config.js",
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types"
},
"dependencies": {
"@imtbl/config": "workspace:*",
"@imtbl/metrics": "workspace:*",
"axios": "^1.6.2",
"jwt-decode": "^3.1.2",
"localforage": "^1.10.0",
"oidc-client-ts": "^2.4.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@imtbl/toolkit": "workspace:*",
"tsup": "^8.3.0",
"typescript": "^5.6.2"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import localForage from 'localforage';
import DeviceCredentialsManager from './storage/device_credentials_manager';
import logger from './utils/logger';
import { isAccessTokenExpiredOrExpiring } from './utils/token';
import { PassportError, PassportErrorType, withPassportError } from './errors/passportError';
import { AuthError, AuthErrorType, withAuthError } from './errors';
import {
DirectLoginOptions,
PassportMetadata,
Expand All @@ -27,7 +27,7 @@ import {
UserImx,
isUserImx,
} from './types';
import { PassportConfiguration } from './config';
import { IAuthConfiguration } from './config';
import ConfirmationOverlay from './overlay/confirmationOverlay';
import { LocalForageAsyncStorage } from './storage/LocalForageAsyncStorage';
import { EmbeddedLoginPrompt } from './confirmation';
Expand All @@ -48,12 +48,12 @@ const getLogoutEndpointPath = (crossSdkBridgeEnabled: boolean): string => (
crossSdkBridgeEnabled ? crossSdkBridgeLogoutEndpoint : logoutEndpoint
);

const getAuthConfiguration = (config: PassportConfiguration): UserManagerSettings => {
const getAuthConfiguration = (config: IAuthConfiguration): UserManagerSettings => {
const { authenticationDomain, oidcConfiguration } = config;

let store;
if (config.crossSdkBridgeEnabled) {
store = new LocalForageAsyncStorage('ImmutableSDKPassport', localForage.INDEXEDDB);
store = new LocalForageAsyncStorage('ImmutableSDKAuth', localForage.INDEXEDDB);
} else if (typeof window !== 'undefined') {
store = window.localStorage;
} else {
Expand Down Expand Up @@ -110,7 +110,7 @@ export default class AuthManager {

private deviceCredentialsManager: DeviceCredentialsManager;

private readonly config: PassportConfiguration;
private readonly config: IAuthConfiguration;

private readonly embeddedLoginPrompt: EmbeddedLoginPrompt;

Expand All @@ -121,7 +121,7 @@ export default class AuthManager {
*/
private refreshingPromise: Promise<User | null> | null = null;

constructor(config: PassportConfiguration, embeddedLoginPrompt: EmbeddedLoginPrompt) {
constructor(config: IAuthConfiguration, embeddedLoginPrompt: EmbeddedLoginPrompt) {
this.config = config;
this.userManager = new UserManager(getAuthConfiguration(config));
this.deviceCredentialsManager = new DeviceCredentialsManager();
Expand Down Expand Up @@ -221,13 +221,13 @@ export default class AuthManager {

public async loginWithRedirect(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Promise<void> {
await this.userManager.clearStaleState();
return withPassportError<void>(async () => {
return withAuthError<void>(async () => {
const extraQueryParams = this.buildExtraQueryParams(anonymousId, directLoginOptions);

await this.userManager.signinRedirect({
extraQueryParams,
});
}, PassportErrorType.AUTHENTICATION_ERROR);
}, AuthErrorType.AUTHENTICATION_ERROR);
}

/**
Expand All @@ -239,19 +239,21 @@ export default class AuthManager {
* @param directLoginOptions.email Required when directLoginMethod is 'email'
*/
public async login(anonymousId?: string, directLoginOptions?: DirectLoginOptions): Promise<User> {
return withPassportError<User>(async () => {
return withAuthError<User>(async () => {
// If directLoginOptions are provided, then the consumer has rendered their own initial login screen.
// If not, display the embedded login prompt and pass the returned direct login options and imPassportTraceId to the login popup.
let directLoginOptionsToUse: DirectLoginOptions | undefined;
let imPassportTraceId: string | undefined;
if (directLoginOptions) {
directLoginOptionsToUse = directLoginOptions;
} else if (!this.config.popupOverlayOptions.disableHeadlessLoginPromptOverlay) {
} else if (!this.config.popupOverlayOptions?.disableHeadlessLoginPromptOverlay) {
const {
imPassportTraceId: embeddedLoginPromptImPassportTraceId,
...embeddedLoginPromptDirectLoginOptions
} = await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt(anonymousId);
directLoginOptionsToUse = embeddedLoginPromptDirectLoginOptions;
} = this.embeddedLoginPrompt
? await this.embeddedLoginPrompt.displayEmbeddedLoginPrompt(anonymousId)
: { imPassportTraceId: undefined };
directLoginOptionsToUse = (embeddedLoginPromptDirectLoginOptions as any).directLoginMethod ? embeddedLoginPromptDirectLoginOptions as DirectLoginOptions : undefined;
imPassportTraceId = embeddedLoginPromptImPassportTraceId;
}

Expand Down Expand Up @@ -314,7 +316,7 @@ export default class AuthManager {

// Popup was blocked; append the blocked popup overlay to allow the user to try again.
let popupHasBeenOpened: boolean = false;
const overlay = new ConfirmationOverlay(this.config.popupOverlayOptions, true);
const overlay = new ConfirmationOverlay(this.config.popupOverlayOptions || {}, true);
overlay.append(
async () => {
try {
Expand Down Expand Up @@ -346,7 +348,7 @@ export default class AuthManager {
);
});
});
}, PassportErrorType.AUTHENTICATION_ERROR);
}, AuthErrorType.AUTHENTICATION_ERROR);
}

public async getUserOrLogin(): Promise<User> {
Expand Down Expand Up @@ -378,7 +380,7 @@ export default class AuthManager {
}

public async loginCallback(): Promise<undefined | User> {
return withPassportError<undefined | User>(async () => {
return withAuthError<undefined | User>(async () => {
// ID-3950: https://github.com/authts/oidc-client-ts/issues/2043
// When using `signinPopup` to initiate a login, call the `signinPopupCallback` method and
// set the `keepOpen` flag to `true`, as the `login` method is now responsible for closing the popup.
Expand All @@ -393,7 +395,7 @@ export default class AuthManager {
}

return AuthManager.mapOidcUserToDomainModel(oidcUser);
}, PassportErrorType.AUTHENTICATION_ERROR);
}, AuthErrorType.AUTHENTICATION_ERROR);
}

public async getPKCEAuthorizationUrl(
Expand Down Expand Up @@ -448,7 +450,7 @@ export default class AuthManager {
}

public async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise<User> {
return withPassportError<User>(async () => {
return withAuthError<User>(async () => {
const pkceData = this.deviceCredentialsManager.getPKCEData();
if (!pkceData) {
throw new Error('No code verifier or state for PKCE');
Expand All @@ -464,7 +466,7 @@ export default class AuthManager {
await this.userManager.storeUser(oidcUser);

return user;
}, PassportErrorType.AUTHENTICATION_ERROR);
}, AuthErrorType.AUTHENTICATION_ERROR);
}

private async getPKCEToken(authorizationCode: string, codeVerifier: string): Promise<DeviceTokenResponse> {
Expand All @@ -484,25 +486,25 @@ export default class AuthManager {
}

public async storeTokens(tokenResponse: DeviceTokenResponse): Promise<User> {
return withPassportError<User>(async () => {
return withAuthError<User>(async () => {
const oidcUser = AuthManager.mapDeviceTokenResponseToOidcUser(tokenResponse);
const user = AuthManager.mapOidcUserToDomainModel(oidcUser);
await this.userManager.storeUser(oidcUser);

return user;
}, PassportErrorType.AUTHENTICATION_ERROR);
}, AuthErrorType.AUTHENTICATION_ERROR);
}

public async logout(): Promise<void> {
return withPassportError<void>(async () => {
return withAuthError<void>(async () => {
await this.userManager.revokeTokens(['refresh_token']);

if (this.logoutMode === 'silent') {
await this.userManager.signoutSilent();
} else {
await this.userManager.signoutRedirect();
}
}, PassportErrorType.LOGOUT_ERROR);
}, AuthErrorType.LOGOUT_ERROR);
}

public async logoutSilentCallback(url: string): Promise<void> {
Expand Down Expand Up @@ -554,16 +556,16 @@ export default class AuthManager {
}
resolve(null);
} catch (err) {
let passportErrorType = PassportErrorType.AUTHENTICATION_ERROR;
let passportErrorType = AuthErrorType.AUTHENTICATION_ERROR;
let errorMessage = 'Failed to refresh token';
let removeUser = true;

if (err instanceof ErrorTimeout) {
passportErrorType = PassportErrorType.SILENT_LOGIN_ERROR;
passportErrorType = AuthErrorType.SILENT_LOGIN_ERROR;
errorMessage = `${errorMessage}: ${err.message}`;
removeUser = false;
} else if (err instanceof ErrorResponse) {
passportErrorType = PassportErrorType.NOT_LOGGED_IN_ERROR;
passportErrorType = AuthErrorType.NOT_LOGGED_IN_ERROR;
errorMessage = `${errorMessage}: ${err.message || err.error_description}`;
} else if (err instanceof Error) {
errorMessage = `${errorMessage}: ${err.message}`;
Expand All @@ -581,7 +583,7 @@ export default class AuthManager {
}
}

reject(new PassportError(errorMessage, passportErrorType));
reject(new AuthError(errorMessage, passportErrorType));
} finally {
this.refreshingPromise = null; // Reset the promise after completion
}
Expand Down
67 changes: 67 additions & 0 deletions packages/auth/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
OidcConfiguration,
AuthModuleConfiguration,
PopupOverlayOptions,
} from './types';
import { AuthError, AuthErrorType } from './errors';

const validateConfiguration = <T>(
configuration: T,
requiredKeys: Array<keyof T>,
prefix?: string,
) => {
const missingKeys = requiredKeys
.map((key) => !configuration[key] && key)
.filter((n) => n)
.join(', ');
if (missingKeys !== '') {
const errorMessage = prefix
? `${prefix} - ${missingKeys} cannot be null`
: `${missingKeys} cannot be null`;
throw new AuthError(
errorMessage,
AuthErrorType.INVALID_CONFIGURATION,
);
}
};

/**
* Interface that any configuration must implement to work with AuthManager
*/
export interface IAuthConfiguration {
readonly authenticationDomain: string;
readonly passportDomain: string;
readonly oidcConfiguration: OidcConfiguration;
readonly crossSdkBridgeEnabled: boolean;
readonly popupOverlayOptions?: PopupOverlayOptions;
}

export class AuthConfiguration implements IAuthConfiguration {
readonly authenticationDomain: string;
readonly passportDomain: string;
readonly oidcConfiguration: OidcConfiguration;
readonly crossSdkBridgeEnabled: boolean;
readonly popupOverlayOptions?: PopupOverlayOptions;

constructor({
authenticationDomain,
passportDomain,
crossSdkBridgeEnabled,
popupOverlayOptions,
...oidcConfiguration
}: AuthModuleConfiguration) {
validateConfiguration(oidcConfiguration, [
'clientId',
'redirectUri',
]);

this.oidcConfiguration = oidcConfiguration;
this.crossSdkBridgeEnabled = crossSdkBridgeEnabled || false;
this.popupOverlayOptions = popupOverlayOptions;

// Default to production auth domain if not provided
this.authenticationDomain = authenticationDomain || 'https://auth.immutable.com';
this.passportDomain = passportDomain || 'https://passport.immutable.com';
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ConfirmationSendMessage,
} from './types';
import { openPopupCenter } from './popup';
import { PassportConfiguration } from '../config';
import { IAuthConfiguration } from '../config';
import ConfirmationOverlay from '../overlay/confirmationOverlay';

const CONFIRMATION_WINDOW_TITLE = 'Confirm this transaction';
Expand All @@ -24,7 +24,7 @@ type MessageHandler = (arg0: MessageEvent) => void;
type MessageType = 'erc191' | 'eip712';

export default class ConfirmationScreen {
private config: PassportConfiguration;
private config: IAuthConfiguration;

private confirmationWindow: Window | undefined;

Expand All @@ -36,7 +36,7 @@ export default class ConfirmationScreen {

private timer: NodeJS.Timeout | undefined;

constructor(config: PassportConfiguration) {
constructor(config: IAuthConfiguration) {
this.config = config;
this.overlayClosed = false;
}
Expand Down Expand Up @@ -194,12 +194,12 @@ export default class ConfirmationScreen {
width: popupOptions?.width || CONFIRMATION_WINDOW_WIDTH,
height: popupOptions?.height || CONFIRMATION_WINDOW_HEIGHT,
});
this.overlay = new ConfirmationOverlay(this.config.popupOverlayOptions);
this.overlay = new ConfirmationOverlay(this.config.popupOverlayOptions || {});
} catch (error) {
// If an error is thrown here then the popup is blocked
const errorMessage = error instanceof Error ? error.message : String(error);
trackError('passport', 'confirmationPopupDenied', new Error(errorMessage));
this.overlay = new ConfirmationOverlay(this.config.popupOverlayOptions, true);
this.overlay = new ConfirmationOverlay(this.config.popupOverlayOptions || {}, true);
}

this.overlay.append(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
EmbeddedLoginPromptResult,
EmbeddedLoginPromptReceiveMessage,
} from './types';
import { PassportConfiguration } from '../config';
import { IAuthConfiguration } from '../config';
import EmbeddedLoginPromptOverlay from '../overlay/embeddedLoginPromptOverlay';

const LOGIN_PROMPT_WINDOW_HEIGHT = 660;
Expand All @@ -14,9 +14,9 @@ const LOGIN_PROMPT_KEYFRAME_STYLES_ID = 'passport-embedded-login-keyframes';
const LOGIN_PROMPT_IFRAME_ID = 'passport-embedded-login-iframe';

export default class EmbeddedLoginPrompt {
private config: PassportConfiguration;
private config: IAuthConfiguration;

constructor(config: PassportConfiguration) {
constructor(config: IAuthConfiguration) {
this.config = config;
}

Expand Down
Loading
Loading