@omneedia/auth-js
Version:
Isomorphic Auth client
1,070 lines (1,069 loc) • 53.1 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const GoTrueAdminApi_1 = __importDefault(require("./GoTrueAdminApi"));
const constants_1 = require("./lib/constants");
const errors_1 = require("./lib/errors");
const fetch_1 = require("./lib/fetch");
const helpers_1 = require("./lib/helpers");
const local_storage_1 = __importDefault(require("./lib/local-storage"));
const polyfills_1 = require("./lib/polyfills");
(0, polyfills_1.polyfillGlobalThis)(); // Make "globalThis" available
const DEFAULT_OPTIONS = {
url: constants_1.GOTRUE_URL,
storageKey: constants_1.STORAGE_KEY,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
headers: constants_1.DEFAULT_HEADERS,
};
class GoTrueClient {
/**
* Create a new client for use in the browser.
*/
constructor(options) {
this.stateChangeEmitters = new Map();
this.networkRetries = 0;
this.refreshingDeferred = null;
/**
* Keeps track of the async client initialization.
* When null or not yet resolved the auth state is `unknown`
* Once resolved the the auth state is known and it's save to call any further client methods.
* Keep extra care to never reject or throw uncaught errors
*/
this.initializePromise = null;
this.detectSessionInUrl = true;
const settings = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options);
this.inMemorySession = null;
this.storageKey = settings.storageKey;
this.autoRefreshToken = settings.autoRefreshToken;
this.persistSession = settings.persistSession;
this.storage = settings.storage || local_storage_1.default;
this.admin = new GoTrueAdminApi_1.default({
url: settings.url,
headers: settings.headers,
fetch: settings.fetch,
});
this.url = settings.url;
this.headers = settings.headers;
this.fetch = (0, helpers_1.resolveFetch)(settings.fetch);
this.detectSessionInUrl = settings.detectSessionInUrl;
this.initialize();
this.mfa = {
verify: this._verify.bind(this),
enroll: this._enroll.bind(this),
unenroll: this._unenroll.bind(this),
challenge: this._challenge.bind(this),
listFactors: this._listFactors.bind(this),
challengeAndVerify: this._challengeAndVerify.bind(this),
getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
};
}
/**
* Initializes the client session either from the url or from storage.
* This method is automatically called when instantiating the client, but should also be called
* manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
*/
initialize() {
if (!this.initializePromise) {
this.initializePromise = this._initialize();
}
return this.initializePromise;
}
/**
* IMPORTANT:
* 1. Never throw in this method, as it is called from the constructor
* 2. Never return a session from this method as it would be cached over
* the whole lifetime of the client
*/
_initialize() {
return __awaiter(this, void 0, void 0, function* () {
if (this.initializePromise) {
return this.initializePromise;
}
try {
if (this.detectSessionInUrl && this._isImplicitGrantFlow()) {
const { data, error } = yield this._getSessionFromUrl();
if (error) {
// failed login attempt via url,
// remove old session as in verifyOtp, signUp and signInWith*
yield this._removeSession();
return { error };
}
const { session, redirectType } = data;
yield this._saveSession(session);
this._notifyAllSubscribers('SIGNED_IN', session);
if (redirectType === 'recovery') {
this._notifyAllSubscribers('PASSWORD_RECOVERY', session);
}
return { error: null };
}
// no login attempt via callback url try to recover session from storage
yield this._recoverAndRefresh();
return { error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { error };
}
return {
error: new errors_1.AuthUnknownError('Unexpected error during initialization', error),
};
}
finally {
this._handleVisibilityChange();
}
});
}
/**
* Creates a new user.
* @returns A logged-in session if the server has "autoconfirm" ON
* @returns A user if the server has "autoconfirm" OFF
*/
signUp(credentials) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
try {
yield this._removeSession();
let res;
if ('email' in credentials) {
const { email, password, options } = credentials;
res = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/signup`, {
headers: this.headers,
redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo,
body: {
email,
password,
data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {},
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
xform: fetch_1._sessionResponse,
});
}
else if ('phone' in credentials) {
const { phone, password, options } = credentials;
res = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/signup`, {
headers: this.headers,
body: {
phone,
password,
data: (_b = options === null || options === void 0 ? void 0 : options.data) !== null && _b !== void 0 ? _b : {},
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
xform: fetch_1._sessionResponse,
});
}
else {
throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a password');
}
const { data, error } = res;
if (error || !data) {
return { data: { user: null, session: null }, error: error };
}
const session = data.session;
const user = data.user;
if (data.session) {
yield this._saveSession(data.session);
this._notifyAllSubscribers('SIGNED_IN', session);
}
return { data: { user, session }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null, session: null }, error };
}
throw error;
}
});
}
/**
* Log in an existing user with an email and password or phone and password.
*/
signInWithPassword(credentials) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
try {
yield this._removeSession();
let res;
if ('email' in credentials) {
const { email, password, options } = credentials;
res = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
headers: this.headers,
body: {
email,
password,
data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {},
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
xform: fetch_1._sessionResponse,
});
}
else if ('phone' in credentials) {
const { phone, password, options } = credentials;
res = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
headers: this.headers,
body: {
phone,
password,
data: (_b = options === null || options === void 0 ? void 0 : options.data) !== null && _b !== void 0 ? _b : {},
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
xform: fetch_1._sessionResponse,
});
}
else {
throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number and a password');
}
const { data, error } = res;
if (error || !data)
return { data: { user: null, session: null }, error };
if (data.session) {
yield this._saveSession(data.session);
this._notifyAllSubscribers('SIGNED_IN', data.session);
}
return { data, error };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null, session: null }, error };
}
throw error;
}
});
}
/**
* Log in an existing user via a third-party provider.
*/
signInWithOAuth(credentials) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
yield this._removeSession();
return this._handleProviderSignIn(credentials.provider, {
redirectTo: (_a = credentials.options) === null || _a === void 0 ? void 0 : _a.redirectTo,
scopes: (_b = credentials.options) === null || _b === void 0 ? void 0 : _b.scopes,
queryParams: (_c = credentials.options) === null || _c === void 0 ? void 0 : _c.queryParams,
});
});
}
/**
* Log in a user using magiclink or a one-time password (OTP).
* If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
* If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
* If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
*/
signInWithOtp(credentials) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function* () {
try {
yield this._removeSession();
if ('email' in credentials) {
const { email, options } = credentials;
const { error } = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/otp`, {
headers: this.headers,
body: {
email,
data: (_a = options === null || options === void 0 ? void 0 : options.data) !== null && _a !== void 0 ? _a : {},
create_user: (_b = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _b !== void 0 ? _b : true,
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo,
});
return { data: { user: null, session: null }, error };
}
if ('phone' in credentials) {
const { phone, options } = credentials;
const { error } = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/otp`, {
headers: this.headers,
body: {
phone,
data: (_c = options === null || options === void 0 ? void 0 : options.data) !== null && _c !== void 0 ? _c : {},
create_user: (_d = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _d !== void 0 ? _d : true,
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
},
});
return { data: { user: null, session: null }, error };
}
throw new errors_1.AuthInvalidCredentialsError('You must provide either an email or phone number.');
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null, session: null }, error };
}
throw error;
}
});
}
/**
* Log in a user given a User supplied OTP received via mobile.
*/
verifyOtp(params) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
try {
yield this._removeSession();
const { data, error } = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/verify`, {
headers: this.headers,
body: Object.assign(Object.assign({}, params), { gotrue_meta_security: { captcha_token: (_a = params.options) === null || _a === void 0 ? void 0 : _a.captchaToken } }),
redirectTo: (_b = params.options) === null || _b === void 0 ? void 0 : _b.redirectTo,
xform: fetch_1._sessionResponse,
});
if (error) {
throw error;
}
if (!data) {
throw 'An error occurred on token verification.';
}
const session = data.session;
const user = data.user;
if (session === null || session === void 0 ? void 0 : session.access_token) {
yield this._saveSession(session);
this._notifyAllSubscribers('SIGNED_IN', session);
}
return { data: { user, session }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null, session: null }, error };
}
throw error;
}
});
}
/**
* Attempts a single-sign on using an enterprise Identity Provider. A
* successful SSO attempt will redirect the current page to the identity
* provider authorization page. The redirect URL is implementation and SSO
* protocol specific.
*
* You can use it by providing a SSO domain. Typically you can extract this
* domain by asking users for their email address. If this domain is
* registered on the Auth instance the redirect will use that organization's
* currently active SSO Identity Provider for the login.
*
* If you have built an organization-specific login page, you can use the
* organization's SSO Identity Provider UUID directly instead.
*
* This API is experimental and availability is conditional on correct
* settings on the Auth service.
*
* @experimental
*/
signInWithSSO(params) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
try {
yield this._removeSession();
return yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/sso`, {
body: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, ('providerId' in params ? { provider_id: params.providerId } : null)), ('domain' in params ? { domain: params.domain } : null)), { redirect_to: (_b = (_a = params.options) === null || _a === void 0 ? void 0 : _a.redirectTo) !== null && _b !== void 0 ? _b : undefined }), (((_c = params === null || params === void 0 ? void 0 : params.options) === null || _c === void 0 ? void 0 : _c.captchaToken)
? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
: null)), { skip_http_redirect: true }),
headers: this.headers,
xform: fetch_1._ssoResponse,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };
}
throw error;
}
});
}
/**
* Returns the session, refreshing it if necessary.
* The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out.
*/
getSession() {
return __awaiter(this, void 0, void 0, function* () {
// make sure we've read the session from the url if there is one
// save to just await, as long we make sure _initialize() never throws
yield this.initializePromise;
let currentSession = null;
if (this.persistSession) {
const maybeSession = yield (0, helpers_1.getItemAsync)(this.storage, this.storageKey);
if (maybeSession !== null) {
if (this._isValidSession(maybeSession)) {
currentSession = maybeSession;
}
else {
yield this._removeSession();
}
}
}
else {
currentSession = this.inMemorySession;
}
if (!currentSession) {
return { data: { session: null }, error: null };
}
const hasExpired = currentSession.expires_at
? currentSession.expires_at <= Date.now() / 1000
: false;
if (!hasExpired) {
return { data: { session: currentSession }, error: null };
}
const { session, error } = yield this._callRefreshToken(currentSession.refresh_token);
if (error) {
return { data: { session: null }, error };
}
return { data: { session }, error: null };
});
}
/**
* Gets the current user details if there is an existing session.
* @param jwt Takes in an optional access token jwt. If no jwt is provided, getUser() will attempt to get the jwt from the current session.
*/
getUser(jwt) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
try {
if (!jwt) {
const { data, error } = yield this.getSession();
if (error) {
throw error;
}
// Default to Authorization header if there is no existing session
jwt = (_b = (_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token) !== null && _b !== void 0 ? _b : undefined;
}
return yield (0, fetch_1._request)(this.fetch, 'GET', `${this.url}/user`, {
headers: this.headers,
jwt: jwt,
xform: fetch_1._userResponse,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null }, error };
}
throw error;
}
});
}
/**
* Updates user data, if there is a logged in user.
*/
updateUser(attributes) {
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: sessionData, error: sessionError } = yield this.getSession();
if (sessionError) {
throw sessionError;
}
if (!sessionData.session) {
throw new errors_1.AuthSessionMissingError();
}
const session = sessionData.session;
const { data, error: userError } = yield (0, fetch_1._request)(this.fetch, 'PUT', `${this.url}/user`, {
headers: this.headers,
body: attributes,
jwt: session.access_token,
xform: fetch_1._userResponse,
});
if (userError)
throw userError;
session.user = data.user;
yield this._saveSession(session);
this._notifyAllSubscribers('USER_UPDATED', session);
return { data: { user: session.user }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null }, error };
}
throw error;
}
});
}
/**
* Decodes a JWT (without performing any validation).
*/
_decodeJWT(jwt) {
return (0, helpers_1.decodeJWTPayload)(jwt);
}
/**
* Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
* If the refresh token or access token in the current session is invalid, an error will be thrown.
* @param currentSession The current session that minimally contains an access token and refresh token.
*/
setSession(currentSession) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (!currentSession.access_token || !currentSession.refresh_token) {
throw new errors_1.AuthSessionMissingError();
}
const timeNow = Date.now() / 1000;
let expiresAt = timeNow;
let hasExpired = true;
let session = null;
const payload = (0, helpers_1.decodeJWTPayload)(currentSession.access_token);
if (payload.exp) {
expiresAt = payload.exp;
hasExpired = expiresAt <= timeNow;
}
if (hasExpired) {
const { session: refreshedSession, error } = yield this._callRefreshToken(currentSession.refresh_token);
if (error) {
return { data: { user: null, session: null }, error: error };
}
if (!refreshedSession) {
return { data: { user: null, session: null }, error: null };
}
session = refreshedSession;
}
else {
const { data, error } = yield this.getUser(currentSession.access_token);
if (error) {
throw error;
}
session = {
access_token: currentSession.access_token,
refresh_token: currentSession.refresh_token,
user: data.user,
token_type: 'bearer',
expires_in: expiresAt - timeNow,
expires_at: expiresAt,
};
yield this._saveSession(session);
}
return { data: { user: session.user, session }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { session: null, user: null }, error };
}
throw error;
}
});
}
/**
* Returns a new session, regardless of expiry status.
* Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
* If the current session's refresh token is invalid, an error will be thrown.
* @param currentSession The current session. If passed in, it must contain a refresh token.
*/
refreshSession(currentSession) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
if (!currentSession) {
const { data, error } = yield this.getSession();
if (error) {
throw error;
}
currentSession = (_a = data.session) !== null && _a !== void 0 ? _a : undefined;
}
if (!(currentSession === null || currentSession === void 0 ? void 0 : currentSession.refresh_token)) {
throw new errors_1.AuthSessionMissingError();
}
const { session, error } = yield this._callRefreshToken(currentSession.refresh_token);
if (error) {
return { data: { user: null, session: null }, error: error };
}
if (!session) {
return { data: { user: null, session: null }, error: null };
}
return { data: { user: session.user, session }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { user: null, session: null }, error };
}
throw error;
}
});
}
/**
* Gets the session data from a URL string
*/
_getSessionFromUrl() {
return __awaiter(this, void 0, void 0, function* () {
try {
if (!(0, helpers_1.isBrowser)())
throw new errors_1.AuthImplicitGrantRedirectError('No browser detected.');
if (!this._isImplicitGrantFlow()) {
throw new errors_1.AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.');
}
const error_description = (0, helpers_1.getParameterByName)('error_description');
if (error_description) {
const error_code = (0, helpers_1.getParameterByName)('error_code');
if (!error_code)
throw new errors_1.AuthImplicitGrantRedirectError('No error_code detected.');
const error = (0, helpers_1.getParameterByName)('error');
if (!error)
throw new errors_1.AuthImplicitGrantRedirectError('No error detected.');
throw new errors_1.AuthImplicitGrantRedirectError(error_description, { error, code: error_code });
}
const provider_token = (0, helpers_1.getParameterByName)('provider_token');
const provider_refresh_token = (0, helpers_1.getParameterByName)('provider_refresh_token');
const access_token = (0, helpers_1.getParameterByName)('access_token');
if (!access_token)
throw new errors_1.AuthImplicitGrantRedirectError('No access_token detected.');
const expires_in = (0, helpers_1.getParameterByName)('expires_in');
if (!expires_in)
throw new errors_1.AuthImplicitGrantRedirectError('No expires_in detected.');
const refresh_token = (0, helpers_1.getParameterByName)('refresh_token');
if (!refresh_token)
throw new errors_1.AuthImplicitGrantRedirectError('No refresh_token detected.');
const token_type = (0, helpers_1.getParameterByName)('token_type');
if (!token_type)
throw new errors_1.AuthImplicitGrantRedirectError('No token_type detected.');
const timeNow = Math.round(Date.now() / 1000);
const expires_at = timeNow + parseInt(expires_in);
const { data, error } = yield this.getUser(access_token);
if (error)
throw error;
const user = data.user;
const session = {
provider_token,
provider_refresh_token,
access_token,
expires_in: parseInt(expires_in),
expires_at,
refresh_token,
token_type,
user,
};
const redirectType = (0, helpers_1.getParameterByName)('type');
// Remove tokens from URL
window.location.hash = '';
return { data: { session, redirectType }, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { session: null, redirectType: null }, error };
}
throw error;
}
});
}
/**
* Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
*/
_isImplicitGrantFlow() {
return ((0, helpers_1.isBrowser)() &&
(Boolean((0, helpers_1.getParameterByName)('access_token')) ||
Boolean((0, helpers_1.getParameterByName)('error_description'))));
}
/**
* Inside a browser context, `signOut()` will remove the logged in user from the browser session
* and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
*
* For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
* There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
*/
signOut() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const { data, error: sessionError } = yield this.getSession();
if (sessionError) {
return { error: sessionError };
}
const accessToken = (_a = data.session) === null || _a === void 0 ? void 0 : _a.access_token;
if (accessToken) {
const { error } = yield this.admin.signOut(accessToken);
if (error) {
// ignore 404s since user might not exist anymore
// ignore 401s since an invalid or expired JWT should sign out the current session
if (!((0, errors_1.isAuthApiError)(error) && (error.status === 404 || error.status === 401))) {
return { error };
}
}
}
yield this._removeSession();
this._notifyAllSubscribers('SIGNED_OUT', null);
return { error: null };
});
}
/**
* Receive a notification every time an auth event happens.
* @param callback A callback function to be invoked when an auth event happens.
*/
onAuthStateChange(callback) {
const id = (0, helpers_1.uuid)();
const subscription = {
id,
callback,
unsubscribe: () => {
this.stateChangeEmitters.delete(id);
},
};
this.stateChangeEmitters.set(id, subscription);
return { data: { subscription } };
}
/**
* Sends a password reset request to an email address.
* @param email The email address of the user.
* @param options.redirectTo The URL to send the user to after they click the password reset link.
* @param options.captchaToken Verification token received when the user completes the captcha on the site.
*/
resetPasswordForEmail(email, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/recover`, {
body: { email, gotrue_meta_security: { captcha_token: options.captchaToken } },
headers: this.headers,
redirectTo: options.redirectTo,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };
}
throw error;
}
});
}
/**
* Generates a new JWT.
* @param refreshToken A valid refresh token that was returned on login.
*/
_refreshAccessToken(refreshToken) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
body: { refresh_token: refreshToken },
headers: this.headers,
xform: fetch_1._sessionResponse,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: { session: null, user: null }, error };
}
throw error;
}
});
}
_isValidSession(maybeSession) {
const isValidSession = typeof maybeSession === 'object' &&
maybeSession !== null &&
'access_token' in maybeSession &&
'refresh_token' in maybeSession &&
'expires_at' in maybeSession;
return isValidSession;
}
_handleProviderSignIn(provider, options = {}) {
const url = this._getUrlForProvider(provider, {
redirectTo: options.redirectTo,
scopes: options.scopes,
queryParams: options.queryParams,
});
// try to open on the browser
if ((0, helpers_1.isBrowser)()) {
window.location.href = url;
}
return { data: { provider, url }, error: null };
}
/**
* Recovers the session from LocalStorage and refreshes
* Note: this method is async to accommodate for AsyncStorage e.g. in React native.
*/
_recoverAndRefresh() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
const currentSession = yield (0, helpers_1.getItemAsync)(this.storage, this.storageKey);
if (!this._isValidSession(currentSession)) {
if (currentSession !== null) {
yield this._removeSession();
}
return;
}
const timeNow = Math.round(Date.now() / 1000);
if (((_a = currentSession.expires_at) !== null && _a !== void 0 ? _a : Infinity) < timeNow + constants_1.EXPIRY_MARGIN) {
if (this.autoRefreshToken && currentSession.refresh_token) {
this.networkRetries++;
const { error } = yield this._callRefreshToken(currentSession.refresh_token);
if (error) {
console.log(error.message);
if (error instanceof errors_1.AuthRetryableFetchError &&
this.networkRetries < constants_1.NETWORK_FAILURE.MAX_RETRIES) {
if (this.refreshTokenTimer)
clearTimeout(this.refreshTokenTimer);
this.refreshTokenTimer = setTimeout(() => this._recoverAndRefresh(), Math.pow(constants_1.NETWORK_FAILURE.RETRY_INTERVAL, this.networkRetries) * 100 // exponential backoff
);
return;
}
yield this._removeSession();
}
this.networkRetries = 0;
}
else {
yield this._removeSession();
}
}
else {
if (this.persistSession) {
yield this._saveSession(currentSession);
}
this._notifyAllSubscribers('SIGNED_IN', currentSession);
}
}
catch (err) {
console.error(err);
return;
}
});
}
_callRefreshToken(refreshToken) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
// refreshing is already in progress
if (this.refreshingDeferred) {
return this.refreshingDeferred.promise;
}
try {
this.refreshingDeferred = new helpers_1.Deferred();
if (!refreshToken) {
throw new errors_1.AuthSessionMissingError();
}
const { data, error } = yield this._refreshAccessToken(refreshToken);
if (error)
throw error;
if (!data.session)
throw new errors_1.AuthSessionMissingError();
yield this._saveSession(data.session);
this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
const result = { session: data.session, error: null };
this.refreshingDeferred.resolve(result);
return result;
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
const result = { session: null, error };
(_a = this.refreshingDeferred) === null || _a === void 0 ? void 0 : _a.resolve(result);
return result;
}
(_b = this.refreshingDeferred) === null || _b === void 0 ? void 0 : _b.reject(error);
throw error;
}
finally {
this.refreshingDeferred = null;
}
});
}
_notifyAllSubscribers(event, session) {
this.stateChangeEmitters.forEach((x) => x.callback(event, session));
}
/**
* set currentSession and currentUser
* process to _startAutoRefreshToken if possible
*/
_saveSession(session) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.persistSession) {
this.inMemorySession = session;
}
const expiresAt = session.expires_at;
if (expiresAt) {
const timeNow = Math.round(Date.now() / 1000);
const expiresIn = expiresAt - timeNow;
const refreshDurationBeforeExpires = expiresIn > constants_1.EXPIRY_MARGIN ? constants_1.EXPIRY_MARGIN : 0.5;
this._startAutoRefreshToken((expiresIn - refreshDurationBeforeExpires) * 1000);
}
if (this.persistSession && session.expires_at) {
yield this._persistSession(session);
}
});
}
_persistSession(currentSession) {
return (0, helpers_1.setItemAsync)(this.storage, this.storageKey, currentSession);
}
_removeSession() {
return __awaiter(this, void 0, void 0, function* () {
if (this.persistSession) {
yield (0, helpers_1.removeItemAsync)(this.storage, this.storageKey);
}
else {
this.inMemorySession = null;
}
if (this.refreshTokenTimer) {
clearTimeout(this.refreshTokenTimer);
}
});
}
/**
* Clear and re-create refresh token timer
* @param value time intervals in milliseconds.
* @param session The current session.
*/
_startAutoRefreshToken(value) {
if (this.refreshTokenTimer)
clearTimeout(this.refreshTokenTimer);
if (value <= 0 || !this.autoRefreshToken)
return;
this.refreshTokenTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
this.networkRetries++;
const { data: { session }, error: sessionError, } = yield this.getSession();
if (!sessionError && session) {
const { error } = yield this._callRefreshToken(session.refresh_token);
if (!error)
this.networkRetries = 0;
if (error instanceof errors_1.AuthRetryableFetchError &&
this.networkRetries < constants_1.NETWORK_FAILURE.MAX_RETRIES)
this._startAutoRefreshToken(Math.pow(constants_1.NETWORK_FAILURE.RETRY_INTERVAL, this.networkRetries) * 100); // exponential backoff
}
}), value);
if (typeof this.refreshTokenTimer.unref === 'function')
this.refreshTokenTimer.unref();
}
_handleVisibilityChange() {
if (!(0, helpers_1.isBrowser)() || !(window === null || window === void 0 ? void 0 : window.addEventListener)) {
return false;
}
try {
window === null || window === void 0 ? void 0 : window.addEventListener('visibilitychange', () => __awaiter(this, void 0, void 0, function* () {
if (document.visibilityState === 'visible') {
yield this.initializePromise;
yield this._recoverAndRefresh();
}
}));
}
catch (error) {
console.error('_handleVisibilityChange', error);
}
}
/**
* Generates the relevant login URL for a third-party provider.
* @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
* @param options.scopes A space-separated list of scopes granted to the OAuth application.
* @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
*/
_getUrlForProvider(provider, options) {
const urlParams = [`provider=${encodeURIComponent(provider)}`];
if (options === null || options === void 0 ? void 0 : options.redirectTo) {
urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`);
}
if (options === null || options === void 0 ? void 0 : options.scopes) {
urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`);
}
if (options === null || options === void 0 ? void 0 : options.queryParams) {
const query = new URLSearchParams(options.queryParams);
urlParams.push(query.toString());
}
return `${this.url}/authorize?${urlParams.join('&')}`;
}
_unenroll(params) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: sessionData, error: sessionError } = yield this.getSession();
if (sessionError) {
return { data: null, error: sessionError };
}
return yield (0, fetch_1._request)(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
headers: this.headers,
jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };
}
throw error;
}
});
}
/**
* Enrolls a factor
* @param friendlyName Human readable name assigned to a device
* @param factorType device which we're validating against. Can only be TOTP for now.
* @param issuer domain which the user is enrolling with
*/
_enroll(params) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: sessionData, error: sessionError } = yield this.getSession();
if (sessionError) {
return { data: null, error: sessionError };
}
const { data, error } = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors`, {
body: {
friendly_name: params.friendlyName,
factor_type: params.factorType,
issuer: params.issuer,
},
headers: this.headers,
jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token,
});
if (error) {
return { data: null, error };
}
if ((_b = data === null || data === void 0 ? void 0 : data.totp) === null || _b === void 0 ? void 0 : _b.qr_code) {
data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`;
}
return { data, error: null };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };
}
throw error;
}
});
}
/**
* Validates a device as part of the enrollment step.
* @param factorId System assigned identifier for authenticator device as returned by enroll
* @param code Code Generated by an authenticator device
*/
_verify(params) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: sessionData, error: sessionError } = yield this.getSession();
if (sessionError) {
return { data: null, error: sessionError };
}
const { data, error } = yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors/${params.factorId}/verify`, {
body: { code: params.code, challenge_id: params.challengeId },
headers: this.headers,
jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token,
});
if (error) {
return { data: null, error };
}
yield this._saveSession(Object.assign({ expires_at: Math.round(Date.now() / 1000) + data.expires_in }, data));
this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data);
return { data, error };
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };
}
throw error;
}
});
}
/**
* Creates a challenge which a user can verify against
* @param factorId System assigned identifier for authenticator device as returned by enroll
*/
_challenge(params) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
const { data: sessionData, error: sessionError } = yield this.getSession();
if (sessionError) {
return { data: null, error: sessionError };
}
return yield (0, fetch_1._request)(this.fetch, 'POST', `${this.url}/factors/${params.factorId}/challenge`, {
headers: this.headers,
jwt: (_a = sessionData === null || sessionData === void 0 ? void 0 : sessionData.session) === null || _a === void 0 ? void 0 : _a.access_token,
});
}
catch (error) {
if ((0, errors_1.isAuthError)(error)) {
return { data: null, error };