Issue #25 Generando un challenge de verdad para oauth

This commit is contained in:
Daniel Cortes
2020-07-12 23:07:05 -04:00
parent 4794406c27
commit 4ec4b8f026
3 changed files with 119 additions and 102 deletions

5
package-lock.json generated
View File

@@ -12402,6 +12402,11 @@
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
}, },
"sjcl": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
"integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ=="
},
"slash": { "slash": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",

View File

@@ -22,6 +22,7 @@
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-tabs": "^3.1.1", "react-tabs": "^3.1.1",
"sjcl": "^1.0.8",
"typescript": "^3.9.6" "typescript": "^3.9.6"
}, },
"scripts": { "scripts": {

View File

@@ -1,142 +1,153 @@
import axios from "axios"; import axios from "axios";
import sjcl from "sjcl";
import {getUser} from "./user_service"; import {getUser} from "./user_service";
const current_host = `${window.location.protocol}//${window.location.host}` const current_host = `${window.location.protocol}//${window.location.host}`
const oauth_url = `${process.env.REACT_APP_API_SERVER}/oauth`; const oauth_url = `${process.env.REACT_APP_API_SERVER}/oauth`;
const client_id = process.env["REACT_APP_CLIENT_ID"]; const client_id = process.env["REACT_APP_CLIENT_ID"];
const generate_challenge = () => { export const generate_challenge = () => {
return { const dictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
code: '5d2309e5bb73b864f989753887fe52f79ce5270395e25862da6940d5', let code = '';
challenge: 'MChCW5vD-3h03HMGFZYskOSTir7II_MMTb8a9rJNhnI',
method: 'S256', for (let i = 0; i < 128; i++) {
} let pos = Math.floor(Math.random() * dictionary.length);
code += dictionary.substring(pos, pos + 1);
}
let challenge = sjcl.codec.base64url.fromBits(sjcl.hash.sha256.hash(code));
return {
code: code,
challenge: challenge,
method: 'S256',
}
} }
export const redirect_to_code = () => { export const redirect_to_code = () => {
const challenge = generate_challenge() const challenge = generate_challenge()
const params = { const params = {
response_type: 'code', response_type: 'code',
client_id: client_id, client_id: client_id,
redirect_uri: `${current_host}/login`, redirect_uri: `${current_host}/login`,
scope: 'read write', scope: 'read write',
code_challenge: challenge.challenge, code_challenge: challenge.challenge,
code_challenge_method: challenge.method code_challenge_method: challenge.method
}; };
const url = `${oauth_url}/authorize/?${new URLSearchParams(params).toString()}`; const url = `${oauth_url}/authorize/?${new URLSearchParams(params).toString()}`;
window.localStorage.setItem('code_verifier', challenge.code); window.localStorage.setItem('code_verifier', challenge.code);
window.location.href = url; window.location.href = url;
return null; return null;
} }
export const do_auth = async (code, code_verifier) => { export const do_auth = async (code, code_verifier) => {
const params = { const params = {
code: code, code: code,
code_verifier: code_verifier, code_verifier: code_verifier,
grant_type: 'authorization_code', grant_type: 'authorization_code',
redirect_uri: `${current_host}/login`, redirect_uri: `${current_host}/login`,
client_id: client_id, client_id: client_id,
}; };
const url = `${oauth_url}/token/` const url = `${oauth_url}/token/`
const response = await axios.post(url, new URLSearchParams(params)); const response = await axios.post(url, new URLSearchParams(params));
return response.data return response.data
} }
export const do_refresh = async (token) => { export const do_refresh = async (token) => {
const params = { const params = {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: token, refresh_token: token,
client_id: client_id, client_id: client_id,
}; };
const url = `${oauth_url}/token/` const url = `${oauth_url}/token/`
const response = await axios.post(url, new URLSearchParams(params)); const response = await axios.post(url, new URLSearchParams(params));
return response.data; return response.data;
} }
export const auth = async (params) => { export const auth = async (params) => {
// Como response se puede obtener desde refresh o desde un auth regular, se almacena al inicio // Como response se puede obtener desde refresh o desde un auth regular, se almacena al inicio
let response; let response;
// Preámbulo, puede existir un refresh_token usable asi que debe hacer el flow de renew // Preámbulo, puede existir un refresh_token usable asi que debe hacer el flow de renew
if (window.localStorage.getItem('refresh_token')) { if (window.localStorage.getItem('refresh_token')) {
const refresh_token = window.localStorage.getItem('refresh_token'); const refresh_token = window.localStorage.getItem('refresh_token');
response = await do_refresh(refresh_token); response = await do_refresh(refresh_token);
}
if (!response) {
// Primera fase, obtener código
// Si es que no se tiene se tiene que redirigir a la pagina de oauth que entregara el código
// a la pagina de redirect como parámetro GET, este trabajo lo hace redirect y se volverá a
// ejecutar este método cuando se entre a esa ruta
if (!window.localStorage.getItem('code_verifier')) {
redirect_to_code();
return {status: 'redirect_to_code'};
} }
if (!response) { // Segunda fase, se llama a auth con los parámetros de la ruta.
// Primera fase, obtener código // Estos parámetros puede contener un error, en ese caso se elimina el code_verifier del storage
// Si es que no se tiene se tiene que redirigir a la pagina de oauth que entregara el código // porque no sera util y fallo la request.
// a la pagina de redirect como parámetro GET, este trabajo lo hace redirect y se volverá a if (params.error) {
// ejecutar este método cuando se entre a esa ruta window.localStorage.clear()
if (!window.localStorage.getItem('code_verifier')) { return {status: 'code_error', value: params.error};
redirect_to_code();
return {status: 'redirect_to_code'};
}
// Segunda fase, se llama a auth con los parámetros de la ruta.
// Estos parámetros puede contener un error, en ese caso se elimina el code_verifier del storage
// porque no sera util y fallo la request.
if (params.error) {
window.localStorage.clear()
return {status: 'code_error', value: params.error};
}
// Teniendo el código en los parámetros se intenta obtener el código de autorización.
const code = params.code;
const code_verifier = window.localStorage.getItem('code_verifier');
response = await do_auth(code, code_verifier)
} }
// Puede que la respuesta sea errónea por varias razones // Teniendo el código en los parámetros se intenta obtener el código de autorización.
if (response.error) { const code = params.code;
return {status: response.error} const code_verifier = window.localStorage.getItem('code_verifier');
} response = await do_auth(code, code_verifier)
}
// Una vez se tiene la respuesta se almacena el refresh_token y el expires_in // Puede que la respuesta sea errónea por varias razones
// en localstorage para ser utilizado mas tarde para renovar el access_token if (response.error) {
window.localStorage.clear() return {status: response.error}
}
const access_token = response.access_token; // Una vez se tiene la respuesta se almacena el refresh_token y el expires_in
const refresh = response.refresh_token; // en localstorage para ser utilizado mas tarde para renovar el access_token
const expires = new Date(new Date().getTime() + ((response.expires_in) * 1000)) window.localStorage.clear()
window.localStorage.setItem('refresh_token', refresh); const access_token = response.access_token;
window.localStorage.setItem('expires', expires); const refresh = response.refresh_token;
const expires = new Date(new Date().getTime() + ((response.expires_in) * 1000))
// Almacenar el usuario en localStorage window.localStorage.setItem('refresh_token', refresh);
const user = await getUser(access_token); window.localStorage.setItem('expires', expires);
window.localStorage.setItem('user', JSON.stringify(user))
// Finalmente se retorna el access_token para ser utilizado en el estado de la app // Almacenar el usuario en localStorage
return {status: 'done', access_token: access_token}; const user = await getUser(access_token);
window.localStorage.setItem('user', JSON.stringify(user))
// Finalmente se retorna el access_token para ser utilizado en el estado de la app
return {status: 'done', access_token: access_token};
} }
export const logout = async (access_token) => { export const logout = async (access_token) => {
// Para hacer logout de un usuario es necesario eliminar el refresh token de su localStorage // Para hacer logout de un usuario es necesario eliminar el refresh token de su localStorage
// y se llama a revocar los tokens en el servidor oauth // y se llama a revocar los tokens en el servidor oauth
const revoke_access_params = { const revoke_access_params = {
token: access_token, token: access_token,
client_id: client_id, client_id: client_id,
token_type_hint: 'access_token' token_type_hint: 'access_token'
} }
const revoke_refresh_params = { const revoke_refresh_params = {
token: window.localStorage.getItem('refresh_token'), token: window.localStorage.getItem('refresh_token'),
client_id: client_id, client_id: client_id,
token_type_hint: 'refresh_token' token_type_hint: 'refresh_token'
} }
window.localStorage.clear(); window.localStorage.clear();
const url = `${oauth_url}/revoke_token/`; const url = `${oauth_url}/revoke_token/`;
const response_access_revoke = await axios.post(url, new URLSearchParams(revoke_access_params)); const response_access_revoke = await axios.post(url, new URLSearchParams(revoke_access_params));
const response_refresh_revoke = await axios.post(url, new URLSearchParams(revoke_refresh_params)); const response_refresh_revoke = await axios.post(url, new URLSearchParams(revoke_refresh_params));
return { return {
access_revoke: response_access_revoke, access_revoke: response_access_revoke,
refresh_revoke: response_refresh_revoke, refresh_revoke: response_refresh_revoke,
} }
} }