@salte-auth/salte-auth
Version:
Authentication for the modern web!
308 lines (251 loc) • 9.48 kB
text/typescript
import { AuthMixinGenerator, SalteAuthMixedIn, Constructor } from './mixins/auth';
import { Shared } from './base/core/shared';
import * as Generic from './generic';
import * as Utils from './utils';
import { Common, Events, Interceptors, URL, IDToken, Logger } from './utils';
import { Provider } from './base/core/provider';
import { SalteAuthError } from './base/core/salte-auth-error';
import { Handler } from './base/handler';
export class SalteAuth extends Shared {
public logger: Logger;
public mixin: (base: Constructor) => SalteAuthMixedIn;
public constructor(config: SalteAuth.Config) {
super(config);
this.required('providers', 'handlers');
this.config = Common.defaults(this.config, {
validation: true,
level: 'warn'
});
this.logger = new Logger(`@salte-auth/salte-auth:core`, this.config.level);
Common.forEach(this.config.providers, (provider) => {
provider.connected && provider.connected();
provider.on('login', (error, data) => {
this.emit('login', error, {
provider: provider.$name,
data: data
});
});
provider.on('logout', (error) => {
this.emit('logout', error, {
provider: provider.$name
});
});
});
const action = this.storage.get('action');
const provider = action ? this.provider(this.storage.get('provider')) : null;
const handlerName = action ? this.storage.get('handler') : null;
if (!Common.includes([undefined, null, 'login', 'logout'], action)) {
throw new SalteAuthError({
code: 'unknown_action',
message: `Unable to finish redirect due to an unknown action! (${action})`,
});
}
Common.forEach(this.config.handlers, (handler) => {
if (!handler.connected) return;
const responsible = handler.$name === handlerName;
if (responsible) {
provider.dedupe(action, async () => {
this.logger.trace(`[constructor]: wrapping up authentication for ${handler.$name}...`);
await new Promise((resolve) => setTimeout(resolve));
const parsed = handler.connected({ action });
if (action === 'login') {
provider.validate(parsed);
this.logger.info('[constructor]: login complete');
} else {
provider.storage.clear();
provider.sync();
provider.emit('logout');
this.logger.info('[constructor]: logout complete');
}
});
} else {
handler.connected({ action: null });
}
});
this.storage.delete('action');
this.storage.delete('provider');
this.storage.delete('handler');
Interceptors.Fetch.add(async (request) => {
for (let i = 0; i < this.config.providers.length; i++) {
const provider = this.config.providers[i];
if (URL.match(request.url, provider.config.endpoints)) {
await this.$secure(provider, request);
}
}
});
Interceptors.XHR.add(async (request) => {
for (let i = 0; i < this.config.providers.length; i++) {
const provider = this.config.providers[i];
if (URL.match(request.$url, provider.config.endpoints)) {
await this.$secure(provider, request);
}
}
});
Events.route(async () => {
for (let i = 0; i < this.config.providers.length; i++) {
const provider = this.config.providers[i];
if (URL.match(location.href, provider.config.routes)) {
await this.$secure(provider);
}
}
});
this.mixin = AuthMixinGenerator(this);
}
/**
* Login to the specified provider.
*
* @param options - the authentication options
*/
public async login(options: SalteAuth.AuthOptions): Promise<void>;
/**
* Login to the specified provider.
*
* @param provider - the provider to login with
*/
public async login(provider: string): Promise<void>;
public async login(options: SalteAuth.AuthOptions | string): Promise<void> {
const normalizedOptions: SalteAuth.AuthOptions = typeof(options) === 'string' ? { provider: options } : options;
const provider = this.provider(normalizedOptions.provider);
return provider.dedupe('login', async () => {
const handler = this.handler(normalizedOptions.handler);
try {
this.storage.set('action', 'login');
this.storage.set('provider', provider.$name);
this.storage.set('handler', handler.$name);
this.logger.info(`[login]: logging in with ${provider.$name} via ${handler.$name}...`);
const params = await handler.open({
redirectUrl: provider.redirectUrl('login'),
url: provider.$login(),
});
this.logger.trace(`[login]: validating response...`, params);
provider.validate(params);
this.logger.info('[login]: login complete');
} finally {
this.storage.delete('action');
this.storage.delete('provider');
this.storage.delete('handler');
}
});
}
/**
* Logout of the specified provider.
*
* @param options - the authentication options
*/
public async logout(options: SalteAuth.AuthOptions): Promise<void>;
/**
* Logout of the specified provider.
*
* @param provider - the provider to logout of
*/
public async logout(provider: string): Promise<void>;
public async logout(options: SalteAuth.AuthOptions | string): Promise<void> {
const normalizedOptions: SalteAuth.AuthOptions = typeof(options) === 'string' ? { provider: options } : options;
const provider = this.provider(normalizedOptions.provider);
return provider.dedupe('logout', async () => {
try {
const handler = this.handler(normalizedOptions.handler);
this.storage.set('action', 'logout');
this.storage.set('provider', provider.$name);
this.storage.set('handler', handler.$name);
this.logger.info(`[logout]: logging out with ${provider.$name} via ${handler.$name}...`);
await handler.open({
redirectUrl: provider.redirectUrl('logout'),
url: URL.url(provider.logout, provider.config.queryParams && provider.config.queryParams('logout')),
});
provider.storage.clear();
provider.sync();
provider.emit('logout');
this.logger.info('[logout]: logout complete');
} catch (error) {
provider.emit('logout', error);
throw error;
} finally {
this.storage.delete('action');
this.storage.delete('provider');
this.storage.delete('handler');
}
});
}
/**
* Returns a provider that matches the given name.
* @param name - the name of the provider
* @returns the provider with the given name.
*/
public provider(name?: string): Provider {
const provider = Common.find(this.config.providers, (provider) => provider.$name === name);
if (!provider) {
throw new SalteAuthError({
code: 'invalid_provider',
message: `Unable to locate provider with the given name. (${name})`,
});
}
return provider;
}
/**
* Returns a handler that matches the given name.
* @param name - the name of the handler
* @returns the handler with the given name, if no name is specified then the default handler.
*/
public handler(name?: string): Handler {
const handler = name === undefined ?
Common.find(this.config.handlers, (handler) => Boolean(handler.config.default)) :
Common.find(this.config.handlers, (handler) => handler.$name === name)
if (!handler) {
throw new SalteAuthError({
code: 'invalid_handler',
message: `Unable to locate handler with the given name. (${name})`,
});
}
return handler;
}
private async $secure(provider: Provider, request?: Utils.Interceptors.XHR.ExtendedXMLHttpRequest | Request) {
const handler = this.handler();
let response: string | boolean = null;
while (response !== true) {
response = await provider.secure(request);
if (response === 'login') {
if (!handler.auto) {
throw new SalteAuthError({
code: 'auto_unsupported',
message: `The default handler doesn't support automatic authentication! (${handler.$name})`,
});
}
await this.login({
provider: provider.$name,
handler: handler.$name
});
}
}
}
}
export interface SalteAuth {
config: SalteAuth.Config;
on(name: 'login', listener: (error?: Error, data?: SalteAuth.EventWrapper) => void): void;
on(name: 'logout', listener: (error?: Error, data?: SalteAuth.EventWrapper) => void): void;
emit(name: 'login'|'logout', error?: Error, data?: SalteAuth.EventWrapper): void;
}
export declare namespace SalteAuth {
interface Config extends Shared.Config {
providers: Provider[];
handlers: Handler[];
/**
* Determines the level of verbosity of the logs.
*
* @defaultValue 'warn'
*/
level?: ('error'|'warn'|'info'|'trace');
}
interface EventWrapper {
provider: string;
data?: IDToken.UserInfo | string;
}
interface AuthOptions {
provider: string;
handler?: string;
}
}
export { SalteAuthError } from './base/core/salte-auth-error';
export { OAuth2Provider } from './base/provider-oauth2';
export { OpenIDProvider } from './base/provider-openid';
export { Provider, Handler, Utils, Generic };