Skip to content

Commit

Permalink
add loginWithPopup method
Browse files Browse the repository at this point in the history
  • Loading branch information
pjhul committed Jan 31, 2022
1 parent f387416 commit 5174f33
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 27 deletions.
2 changes: 1 addition & 1 deletion dist/auth0-web-extension.production.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/auth0-web-extension.production.esm.js.map

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion dist/types/Auth0Client.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Auth0ClientOptions, CacheLocation, GetTokenSilentlyOptions, GetTokenSilentlyResult, GetTokenSilentlyVerboseResult, User, GetUserOptions, IdToken, GetIdTokenClaimsOptions, RedirectLoginOptions } from './global';
import { Auth0ClientOptions, CacheLocation, GetTokenSilentlyOptions, GetTokenSilentlyResult, GetTokenSilentlyVerboseResult, User, GetUserOptions, IdToken, GetIdTokenClaimsOptions, RedirectLoginOptions, PopupLoginOptions, PopupConfigOptions } from './global';
/**
* Auth0 SDK for Background Scripts in a Web Extension
*/
Expand Down Expand Up @@ -48,6 +48,7 @@ export default class Auth0Client {
*/
getIdTokenClaims(options?: GetIdTokenClaimsOptions): Promise<IdToken | undefined>;
loginWithNewTab<TAppState = any>(options?: RedirectLoginOptions<TAppState>): Promise<GetTokenSilentlyResult>;
loginWithPopup(options?: PopupLoginOptions, config?: PopupConfigOptions): Promise<GetTokenSilentlyResult>;
private _handleAuthorizeResponse;
/**
* ```js
Expand Down
15 changes: 15 additions & 0 deletions dist/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ export interface RedirectLoginOptions<TAppState = any> extends BaseLoginOptions
*/
redirectMethod?: 'replace' | 'assign';
}
export interface RedirectLoginResult<TAppState = any> {
/**
* State stored when the redirect request was made
*/
appStat?: TAppState;
}
export interface PopupLoginOptions extends BaseLoginOptions {
}
export interface PopupConfigOptions {
/**
* The number of seconds to wait for a popup response before
* throwing a timeout error. Defaults to 60s
*/
timeoutInSeconds?: number;
}
export interface GetUserOptions {
/**
* The scope that was used in the authentication request
Expand Down
9 changes: 7 additions & 2 deletions dist/types/messenger.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import browser from 'webextension-polyfill';
import { GenericError } from './errors';
import { AuthenticationResult } from './global';
export declare type AuthStartMessage = {
type: 'auth-start';
Expand All @@ -16,11 +17,15 @@ export declare type AuthResultMessage = {
type: 'auth-result';
payload: AuthenticationResult;
};
export declare type Message = AuthStartMessage | AuthParamsMessage | AuthCleanUpMessage | AuthAckMessage | AuthResultMessage;
export declare type AuthErrorMessage = {
type: 'auth-error';
error: GenericError;
};
export declare type Message = AuthStartMessage | AuthParamsMessage | AuthCleanUpMessage | AuthAckMessage | AuthErrorMessage | AuthResultMessage;
export declare type MessageResponse<M extends Message> = M extends AuthStartMessage ? void : M extends AuthParamsMessage ? {
authorizeUrl: string;
domainUrl: string;
} : M extends AuthCleanUpMessage ? void : M extends AuthResultMessage ? void : M extends AuthAckMessage ? 'ack' : never;
} : M extends AuthCleanUpMessage ? void : M extends AuthResultMessage ? void : M extends AuthAckMessage ? 'ack' : M extends AuthErrorMessage ? void : never;
export default class Messenger {
sendTabMessage<M extends Message>(tabId: number, message: M): Promise<MessageResponse<M>>;
sendRuntimeMessage<M extends Message>(message: M): Promise<MessageResponse<M>>;
Expand Down
1 change: 1 addition & 0 deletions dist/types/transaction-manager.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Transaction {
organizationId?: string;
state?: string;
callback: (authResult: GetTokenSilentlyResult) => void;
errorCallback: (error: any) => void;
}
export default class TransactionManager {
private storage;
Expand Down
117 changes: 105 additions & 12 deletions src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import {
IdToken,
GetIdTokenClaimsOptions,
RedirectLoginOptions,
PopupLoginOptions,
PopupConfigOptions,
} from './global';

import { AuthenticationError, TimeoutError } from './errors';
Expand Down Expand Up @@ -164,7 +166,23 @@ export default class Auth0Client {
this._handleAuthorizeResponse(message.payload);
break;

case 'auth-params':
case 'auth-error': {
const transaction = this.transactionManager.get();

if (transaction) {
transaction.errorCallback(message.error);
}

if (sender.tab?.id) {
this.messenger.sendTabMessage(sender.tab.id, {
type: 'auth-cleanup',
});
}

break;
}

case 'auth-params': {
const transaction = this.transactionManager.get();

if (this.options.debug) {
Expand All @@ -179,7 +197,9 @@ export default class Auth0Client {
domainUrl: transaction.domainUrl,
};
}

break;
}

default:
throw new Error(`Invalid message type ${message.type}`);
Expand Down Expand Up @@ -357,7 +377,7 @@ export default class Auth0Client {
) {
try {
const result = await new Promise<GetTokenSilentlyResult>(
async resolve => {
async (resolve, reject) => {
this.transactionManager.create({
authorizeUrl,
domainUrl: this.domainUrl,
Expand All @@ -369,6 +389,7 @@ export default class Auth0Client {
redirect_uri: params.redirect_uri,
state: stateIn,
callback: resolve,
errorCallback: reject,
});

await browser.tabs.create({ url });
Expand All @@ -387,6 +408,85 @@ export default class Auth0Client {
}
}

public async loginWithPopup(
options?: PopupLoginOptions,
config?: PopupConfigOptions
) {
options = options || {};
config = config || {};

const { ...authorizeOptions } = options;
const stateIn = encode(createSecureRandomString());
const nonceIn = encode(createSecureRandomString());
const codeVerifier = createSecureRandomString();
const codeChallengeBuffer = await sha256(codeVerifier);
const codeChallenge = bufferToBase64UrlEncoded(codeChallengeBuffer);

const params = this._getParams(
authorizeOptions,
stateIn,
nonceIn,
codeChallenge,
this.options.redirect_uri
);

const url = this._authorizeUrl({
...params,
response_mode: 'query',
});

const width = 400;
const height = 600;

if (
await retryPromise(
() => lock.acquireLock(GET_TOKEN_SILENTLY_LOCK_KEY, 5000),
10
)
) {
try {
return new Promise<GetTokenSilentlyResult>(async (resolve, reject) => {
const popup = await browser.windows.create({
width,
height,
type: 'popup',
url,
});

const removeWindow =
(func: (...args: any[]) => void) =>
(...args: any[]) => {
if (popup.id) {
browser.windows.remove(popup.id);
}

func(args);
};

this.transactionManager.create({
authorizeUrl: url,
domainUrl: this.domainUrl,
nonce: nonceIn,
code_verifier: codeVerifier,
scope: params.scope,
audience: params.audience || 'default',
redirect_uri: params.redirect_uri,
state: stateIn,
callback: removeWindow(resolve),
errorCallback: removeWindow(reject),
});
});
} finally {
await lock.releaseLock(GET_TOKEN_SILENTLY_LOCK_KEY);

if (this.options.debug)
console.log('[auth0-web-extension] - lock released');
}
} else {
throw new TimeoutError();
}
}

private async _handleAuthorizeResponse(authResult: AuthenticationResult) {
const { error = '', error_description = '', state, code } = authResult;

Expand Down Expand Up @@ -542,9 +642,6 @@ export default class Auth0Client {
const { ignoreCache, ...getTokenOptions } = options;

if (!ignoreCache && getTokenOptions.scope) {
if (this.options.debug)
console.log('[auth0-web-extension] - checking for cached auth token');

const entry = await this._getEntryFromCache({
scope: getTokenOptions.scope,
audience: getTokenOptions.audience || 'default',
Expand All @@ -560,7 +657,7 @@ export default class Auth0Client {
}

if (this.options.debug)
console.log('[auth0-web-extension] - no hit, continuing');
console.log('[auth0-web-extension] - no cache hit, continuing');
}

if (this.options.debug)
Expand All @@ -577,11 +674,6 @@ export default class Auth0Client {

try {
if (!ignoreCache && getTokenOptions.scope) {
if (this.options.debug)
console.log(
'[auth0-web-extension] - checking cache again to make sure it was not populated while waiting for lock'
);

const entry = await this._getEntryFromCache({
scope: getTokenOptions.scope,
audience: getTokenOptions.audience || 'default',
Expand Down Expand Up @@ -692,7 +784,7 @@ export default class Auth0Client {
type: 'auth-start',
});

return new Promise<GetTokenSilentlyResult>(resolve => {
return new Promise<GetTokenSilentlyResult>((resolve, reject) => {
this.transactionManager.create({
authorizeUrl: url,
domainUrl: this.domainUrl,
Expand All @@ -703,6 +795,7 @@ export default class Auth0Client {
redirect_uri: params.redirect_uri,
state: stateIn,
callback: resolve,
errorCallback: reject,
});
});
} catch (e) {
Expand Down
17 changes: 17 additions & 0 deletions src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,23 @@ export interface RedirectLoginOptions<TAppState = any>
redirectMethod?: 'replace' | 'assign';
}

export interface RedirectLoginResult<TAppState = any> {
/**
* State stored when the redirect request was made
*/
appStat?: TAppState;
}

export interface PopupLoginOptions extends BaseLoginOptions {}

export interface PopupConfigOptions {
/**
* The number of seconds to wait for a popup response before
* throwing a timeout error. Defaults to 60s
*/
timeoutInSeconds?: number;
}

export interface GetUserOptions {
/**
* The scope that was used in the authentication request
Expand Down
25 changes: 15 additions & 10 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export async function handleTokenRequest(
payload: results,
});

window.close();
if (window.opener) {
window.close();
}
} else {
const { authorizeUrl, domainUrl } = await messenger.sendRuntimeMessage({
type: 'auth-params',
Expand All @@ -40,16 +42,19 @@ export async function handleTokenRequest(
console.log('[auth0-web-extension] Creating /authorize url IFrame');
}

const codeResult = await runIFrame(authorizeUrl, domainUrl, 60, debug);

if (debug) {
console.log('[auth0-web-extension] Returning results');
try {
const codeResult = await runIFrame(authorizeUrl, domainUrl, 60, debug);

await messenger.sendRuntimeMessage({
type: 'auth-result',
payload: codeResult,
});
} catch (error) {
await messenger.sendRuntimeMessage({
type: 'auth-error',
error: error as GenericError,
});
}

await messenger.sendRuntimeMessage({
type: 'auth-result',
payload: codeResult,
});
}
} else {
let iframe: HTMLIFrameElement;
Expand Down
10 changes: 10 additions & 0 deletions src/messenger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import browser from 'webextension-polyfill';

import { GenericError } from './errors';

import { AuthenticationResult } from './global';

export type AuthStartMessage = {
Expand All @@ -23,11 +25,17 @@ export type AuthResultMessage = {
payload: AuthenticationResult;
};

export type AuthErrorMessage = {
type: 'auth-error';
error: GenericError;
};

export type Message =
| AuthStartMessage
| AuthParamsMessage
| AuthCleanUpMessage
| AuthAckMessage
| AuthErrorMessage
| AuthResultMessage;

export type MessageResponse<M extends Message> = M extends AuthStartMessage
Expand All @@ -43,6 +51,8 @@ export type MessageResponse<M extends Message> = M extends AuthStartMessage
? void
: M extends AuthAckMessage
? 'ack'
: M extends AuthErrorMessage
? void
: never;

type WrappedMessage = Message & {
Expand Down
1 change: 1 addition & 0 deletions src/transaction-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Transaction {
organizationId?: string;
state?: string;
callback: (authResult: GetTokenSilentlyResult) => void;
errorCallback: (error: any) => void;
}

export default class TransactionManager {
Expand Down

0 comments on commit 5174f33

Please sign in to comment.