5180 fichiers 38.5 Mo 0 téléchargements 202 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / api / auth / urlToken.ts
urlToken.ts 5.1 Ko Télécharger le fichier
import { ClearRememberLogin, GetRememberLogin, SetRememberLogin } from '../utils/RememberLogin';
import { clearAccessToken } from './accessToken';

const THIRTY_DAYS_SECONDS = 30 * 24 * 60 * 60;

export interface UrlLoginToken {
    token: string;
    expiresAt: number;
}

/**
 * Reads the login token a CMS puts in the address (`?token=` and `?token_exp=`) and takes both out of it, so the
 * token does not stay in the address bar, the history or the logs of whatever serves the next request.
 */
export const takeLoginTokenFromUrl = (location: Location = window.location, history: History = window.history): UrlLoginToken | null => {
    const url = new URL(location.href);
    const token = url.searchParams.get('token');
    const expiry = Number(url.searchParams.get('token_exp') || 0);

    if (!url.searchParams.has('token') && !url.searchParams.has('token_exp')) return null;

    url.searchParams.delete('token');
    url.searchParams.delete('token_exp');
    history.replaceState(history.state, '', url.toString());

    if (!token) return null;

    return { token, expiresAt: Number.isFinite(expiry) && expiry > 0 ? expiry : Math.floor(Date.now() / 1000) + THIRTY_DAYS_SECONDS };
};

/** A room or friend id from the address: a positive whole number, or 0 (a value such as ?room=abc used to become NaN). */
export const positiveIdFromSearch = (value: string | null): number => {
    const id = Number(value);

    return Number.isInteger(id) && id > 0 ? id : 0;
};

/** The account a remember token belongs to (its `sub`), or null when it is not a token this client can read. */
export const loginTokenSubject = (token: string | undefined): number | null => {
    const payload = token?.split('.')[1];

    if (!payload) return null;

    try {
        const json = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
        const subject = Number(json?.sub);

        return Number.isInteger(subject) && subject > 0 ? subject : null;
    } catch {
        return null;
    }
};

const SWITCH_ACCOUNT_QUESTION = 'This link signs you in to a different account from the one this browser remembers. Sign in with the link?';

/** The question asked before a link signs in a browser that remembers no login, naming the account when it can. */
export const signInFromLinkQuestion = (subject: number | null): string =>
    subject !== null ? `This link signs you in as account #${subject}. Sign in with the link?` : 'This link signs you in to an account. Sign in with the link?';

/**
 * Whether the client is shown in a frame by a page of its own site, as a CMS does on its hotel page (AtomCMS frames
 * the client with the ticket it just issued for the visitor's own session). A page of another site cannot be read
 * from here, so reading its origin throws and the answer is no; a client opened in a tab of its own is not framed.
 */
export const isFramedByOwnSite = (win: Window = window): boolean => {
    try {
        if (!win.parent || win.parent === win) return false;

        return win.parent.location.origin === win.location.origin;
    } catch {
        return false;
    }
};

/**
 * Whether a login secret from the address (a CMS token or an SSO ticket) may be used. A browser that remembers no
 * login used to take it at once, so a link built with the sender's own token signed the reader in to the sender's
 * account (login CSRF): it is asked first. A browser with a remembered login is not asked here, and neither is a
 * client framed by its own site: a link from someone else cannot put its ticket in the address of that frame.
 */
export const confirmSignInFromLink = (
    subject: number | null,
    confirmSignIn: (question: string) => boolean = (question) => window.confirm(question),
    framedByOwnSite: () => boolean = () => isFramedByOwnSite()
): boolean => (GetRememberLogin() || framedByOwnSite() ? true : confirmSignIn(signInFromLinkQuestion(subject)));

/**
 * Takes the CMS login token out of the address and makes it the remembered login. It wins over what this browser
 * remembered: the CMS just logged someone in, and keeping an older remember would enter the hotel as whoever was
 * remembered before. The server does not tie the token to the browser it was issued to, though, so a link built with
 * someone else's token would silently switch account: when both tokens name an account and they differ, the user is
 * asked first. Runs at boot, before the first fetch, so the token never reaches a Referer.
 */
export const adoptLoginTokenFromUrl = (
    location: Location = window.location,
    history: History = window.history,
    confirmSwitch: (question: string) => boolean = (question) => window.confirm(question)
): boolean => {
    const urlToken = takeLoginTokenFromUrl(location, history);

    if (!urlToken) return false;

    const remembered = loginTokenSubject(GetRememberLogin()?.token);
    const incoming = loginTokenSubject(urlToken.token);

    if (!confirmSignInFromLink(incoming, confirmSwitch)) return false;
    if (remembered !== null && incoming !== null && remembered !== incoming && !confirmSwitch(SWITCH_ACCOUNT_QUESTION)) return false;

    ClearRememberLogin();
    clearAccessToken();
    SetRememberLogin(urlToken);

    return true;
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser