@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
227 lines (198 loc) • 7.48 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ConfigError } from 'util/Errors';
import Util from 'util/Util';
import Logger from 'util/Logger';
import getAuthClient from 'widget/getAuthClient';
import buildRenderOptions from 'widget/buildRenderOptions';
import createRouter from 'widget/createRouter';
import Hooks from 'models/Hooks';
import {
WidgetOptions,
WidgetOktaAuthConstructor,
WidgetOktaAuthInterface,
OktaSignInAPI,
RenderSuccessCallback,
RenderErrorCallback,
RenderResult,
RenderOptions,
HookFunction,
RenderResultSuccess,
EventCallback,
EventCallbackWithError,
EventName,
RouterClassFactory,
AbstractRouter,
RouterConstructor,
OktaData,
} from '../types';
import { Tokens } from '@okta/okta-auth-js';
const EVENTS_LIST = ['ready', 'afterError', 'afterRender'];
declare global {
interface Window {
oktaData?: OktaData;
}
}
export function createOktaSignIn
(
authClientConstructor: WidgetOktaAuthConstructor,
routerClassFactory: RouterClassFactory
)
{
return class OktaSignIn implements OktaSignInAPI {
Router: RouterConstructor;
options: WidgetOptions;
hooks: Hooks;
router: AbstractRouter;
authClient: WidgetOktaAuthInterface;
constructor(options: WidgetOptions) {
Util.debugMessage(`
The Okta Sign-In Widget is running in development mode.
When you are ready to publish your app, embed the minified version to turn on production mode.
See: https://developer.okta.com/code/javascript/okta_sign-in_widget#cdn
`);
if (!options.stateToken) {
// need to set stateToken here vs in Settings to determine Router.
// oktaData is only available to SIW on custom domains.
// set stateToken in case user wipes out stateToken when overriding options.
options.stateToken = window?.oktaData?.signIn?.stateToken;
}
this.options = options;
this.authClient = getAuthClient(authClientConstructor, options);
// validate authClient configuration against widget options
if (options.useClassicEngine !== true && this.authClient.options.clientId && this.authClient.isPKCE() === false) {
throw new ConfigError(
'OAuth2 with interaction code flow requires PKCE to be enabled on the authClient.'
);
}
// Hooks can be modified before or after render
this.hooks = new Hooks({
hooks: options.hooks
});
this.Router = routerClassFactory(options);
// Triggers the event up the chain so it is available to the consumers of the widget.
this.Router.prototype.Events.listenTo.call(this, this.Router.prototype, 'all', this.trigger);
// On the first afterRender event (usually when the Widget is ready) - emit a 'ready' event
this.once('afterRender', function(context) {
this.trigger('ready', context);
});
}
/**
* Render the sign in widget to an element. Returns a promise that will resolve on success or reject on error.
* @param options - options for the signin widget.
* Must have an el or $el property to render the widget to.
* @param success - success callback function
* @param error - error callback function
*/
renderEl(renderOptions: RenderOptions, successFn?: RenderSuccessCallback, errorFn?: RenderErrorCallback): Promise<RenderResult> {
if (this.router) {
throw new Error('An instance of the widget has already been rendered. Call remove() first.');
}
const res = createRouter(this.Router, this.options, renderOptions, this.authClient, successFn, errorFn, this.hooks);
this.router = res.router;
return res.promise;
}
hide(): void {
if (this.router) {
this.router.hide();
}
}
show(): void {
if (this.router) {
this.router.show();
}
}
remove(): void {
if (this.router) {
this.router.remove();
this.router = undefined;
}
}
/**
* Renders the Widget and returns a promise that resolves to OAuth tokens
* @param options - options for the signin widget
*/
showSignInToGetTokens(options?: RenderOptions): Promise<Tokens> {
const renderOptions = Object.assign(buildRenderOptions(this.options, options), {
redirect: 'never'
});
const promise = this.renderEl(renderOptions).then(res => {
return (res as RenderResultSuccess).tokens;
});
const authClient = this.router.settings.getAuthClient();
if (authClient.isAuthorizationCodeFlow() && !authClient.isPKCE()) {
throw new ConfigError('"showSignInToGetTokens()" should not be used for authorization_code flow. ' +
'Use "showSignInAndRedirect()" instead');
}
return promise;
}
/**
* Renders the widget and redirects to the OAuth callback
* @param options - options for the signin widget
*/
showSignInAndRedirect(options?: RenderOptions): Promise<void> {
const renderOptions = Object.assign(buildRenderOptions(this.options, options), {
redirect: 'always'
});
return this.renderEl(renderOptions).then(() => {
// This method should never return, it will either redirect or reject with error
return;
});
}
/**
* Renders the widget. Either resolves the returned promise, or redirects.
* @param options - options for the signin widget
*/
showSignIn(options?: RenderOptions): Promise<RenderResult> {
const renderOptions = Object.assign(buildRenderOptions(this.options, options));
return this.renderEl(renderOptions);
}
// Hook convenience functions
before(formName: string, callbackFn: HookFunction): void {
this.hooks.mergeHook(formName, {
before: [
callbackFn
]
});
}
after(formName: string, callbackFn: HookFunction): void {
this.hooks.mergeHook(formName, {
after: [
callbackFn
]
});
}
getUser(): any {
return this.router?.appState?.getUser();
}
// Events API
on(event: EventName, callback: EventCallback | EventCallbackWithError): void {
// custom events listener on widget instance to trap third-party callback errors
if (EVENTS_LIST.includes(event)) {
const origCallback = callback;
callback = function(...callbackArgs) {
try {
origCallback.apply(this, callbackArgs);
} catch (err) {
Logger.error(`[okta-signin-widget] "${event}" event handler error:`, err);
}
};
}
this.Router.prototype.Events.on.call(this, event, callback);
}
off(event?: EventName, callback?: EventCallback | EventCallbackWithError): void {
this.Router.prototype.Events.off.call(this, event, callback);
}
once(event: EventName, callback: EventCallback): void {
this.Router.prototype.Events.once.call(this, event, callback);
}
stopListening(event?: EventName, callback?: EventCallback): void {
this.Router.prototype.Events.stopListening.call(this, event, callback);
}
listenTo(object: any, event: EventName, callback: EventCallback): void {
this.Router.prototype.Events.listenTo.call(this, object, event, callback);
}
trigger(event: EventName, ...args: any[]): void {
this.Router.prototype.Events.trigger.apply(this, [event, ...args]);
}
};
}