Skip to content

Integración PKCE

Generar el código de verificación.

javascript
const createRandomString = () => {
  const charset =
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.";
  let random = "";
  const randomValues = Array.from(
    getCrypto().getRandomValues(new Uint8Array(43))
  );
  // eslint-disable-next-line no-return-assign
  randomValues.forEach((v) => (random += charset[v % charset.length]));
  return random;
};
const createRandomString = () => {
  const charset =
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.";
  let random = "";
  const randomValues = Array.from(
    getCrypto().getRandomValues(new Uint8Array(43))
  );
  // eslint-disable-next-line no-return-assign
  randomValues.forEach((v) => (random += charset[v % charset.length]));
  return random;
};

Con ese código de verificación generamos el código de intercambio.

javascript
const sha256 = async (s) => {
  const digestOp = getCryptoSubtle().digest(
    { name: "SHA-256" },
    new TextEncoder().encode(s)
  );

  if (window.msCrypto) {
    return new Promise((res, rej) => {
      digestOp.oncomplete = (e) => {
        res(e.target.result);
      };

      digestOp.onerror = (e) => {
        rej(e.error);
      };

      digestOp.onabort = () => {
        rej(new Error("The digest operation was aborted"));
      };
    });
  }

  return digestOp;
};

const urlEncodeB64 = (input) => {
  const b64Chars = { "+": "-", "/": "_", "=": "" };
  return input.replace(/[+/=]/g, (m) => b64Chars[m]);
};

const bufferToBase64UrlEncoded = (input) => {
  const ie11SafeInput = new Uint8Array(input);
  return urlEncodeB64(
    window.btoa(String.fromCharCode(...Array.from(ie11SafeInput)))
  );
};
const sha256 = async (s) => {
  const digestOp = getCryptoSubtle().digest(
    { name: "SHA-256" },
    new TextEncoder().encode(s)
  );

  if (window.msCrypto) {
    return new Promise((res, rej) => {
      digestOp.oncomplete = (e) => {
        res(e.target.result);
      };

      digestOp.onerror = (e) => {
        rej(e.error);
      };

      digestOp.onabort = () => {
        rej(new Error("The digest operation was aborted"));
      };
    });
  }

  return digestOp;
};

const urlEncodeB64 = (input) => {
  const b64Chars = { "+": "-", "/": "_", "=": "" };
  return input.replace(/[+/=]/g, (m) => b64Chars[m]);
};

const bufferToBase64UrlEncoded = (input) => {
  const ie11SafeInput = new Uint8Array(input);
  return urlEncodeB64(
    window.btoa(String.fromCharCode(...Array.from(ie11SafeInput)))
  );
};

Redirigir a login para hacer la autenticación con los códigos generados, encriptar en base 64 el redirectUri para que los query params no los mezcle ni los pierda a la hora de hacer la redirección.

javascript
const redirectUri = stringToBase64(
  `http://app.com${window.location.pathname}${window.location.search}`
);
window.location.href = `https://login.nautilus.app/login?simpleReturn=1&app=${config.app}&redirect_uri=${redirectUri}&v2=1&code_challenge=${codeChallenge}&code_challenge_method=S256&response_type=code`;
const redirectUri = stringToBase64(
  `http://app.com${window.location.pathname}${window.location.search}`
);
window.location.href = `https://login.nautilus.app/login?simpleReturn=1&app=${config.app}&redirect_uri=${redirectUri}&v2=1&code_challenge=${codeChallenge}&code_challenge_method=S256&response_type=code`;

Una vez autenticado el usuario, el proveedor de autenticación nos redirigirá a la url indicada con un token temporal, lo podemos obtener de la siguiente manera.

javascript
const getTempToken = () => {
  if (window.location.hash) {
    const hash = window.location.hash.substring(1);
    const qsObject = new URLSearchParams(hash);
    const tempToken = qsObject.get("temp_token");
    history.replaceState(
      {},
      document.title,
      window.location.href.split("#")[0]
    );
    return tempToken;
  }
  return null;
};
const getTempToken = () => {
  if (window.location.hash) {
    const hash = window.location.hash.substring(1);
    const qsObject = new URLSearchParams(hash);
    const tempToken = qsObject.get("temp_token");
    history.replaceState(
      {},
      document.title,
      window.location.href.split("#")[0]
    );
    return tempToken;
  }
  return null;
};

Con ese token temporal necesitamos obtener el token de acceso y el de refresco para cuando el primer token caduque no perdamos la sesión.

javascript
const exchangeKeys = async (tempToken) => {
  const code_verifier = getCodeVerifier();
  if (!code_verifier) {
    return goToLogin();
  }
  cleanCodeVerifier();
  const body = {
    v2: true,
    code_verifier,
    access_token: tempToken,
    grant_type: "authorization_code",
  };
  const res = await fetch(`https://login.nautilus.app/token`, {
    credentials: "include",
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!res.ok) {
    return goToLogin();
  }

  // en result tendremos la propiedades (access_token y refresh_token)
  const result = await res.json();
};
const exchangeKeys = async (tempToken) => {
  const code_verifier = getCodeVerifier();
  if (!code_verifier) {
    return goToLogin();
  }
  cleanCodeVerifier();
  const body = {
    v2: true,
    code_verifier,
    access_token: tempToken,
    grant_type: "authorization_code",
  };
  const res = await fetch(`https://login.nautilus.app/token`, {
    credentials: "include",
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!res.ok) {
    return goToLogin();
  }

  // en result tendremos la propiedades (access_token y refresh_token)
  const result = await res.json();
};

Una vez obtenido el access_token lo debemos poner en la cabecera de la petición "Authorization" de la siguiente manera

javascript
req.headers.Authorization = `Bearer ${token}`;
req.headers.Authorization = `Bearer ${token}`;

Configuración del cliente http para gestionar el refresco del token

Ejemplo con axios de como configurar el cliente http

javascript
import axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";

const authAxiosClient = axios.create();

createAuthRefreshInterceptor(authAxiosClient, refreshAuth, {
  statusCodes: [401, 403, 444],
});

authAxiosClient.interceptors.request.use((req) => {
  req.headers.Authorization = `Bearer ${token}`;
  return req;
});

const refreshAuth = async (failedRequest) => {
  const body = {
    v2: true,
    access_token,
    refresh_token,
    grant_type: 'refresh_token',
  };
  const res = await fetch(`https://login.nautilus.app/token`, {
    credentials: "include",
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
    },
  });

  const result = await res.json();

  if (failedRequest) {
    failedRequest.response.config.headers.Authorization = `Bearer ${result.access_token}`;
  }
};
import axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";

const authAxiosClient = axios.create();

createAuthRefreshInterceptor(authAxiosClient, refreshAuth, {
  statusCodes: [401, 403, 444],
});

authAxiosClient.interceptors.request.use((req) => {
  req.headers.Authorization = `Bearer ${token}`;
  return req;
});

const refreshAuth = async (failedRequest) => {
  const body = {
    v2: true,
    access_token,
    refresh_token,
    grant_type: 'refresh_token',
  };
  const res = await fetch(`https://login.nautilus.app/token`, {
    credentials: "include",
    method: "POST",
    body: JSON.stringify(body),
    headers: {
      "Content-Type": "application/json",
    },
  });

  const result = await res.json();

  if (failedRequest) {
    failedRequest.response.config.headers.Authorization = `Bearer ${result.access_token}`;
  }
};