Appearance
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}`;
}
};