diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/.env.example | 2 | ||||
-rw-r--r-- | src/aggietime.js | 23 | ||||
-rw-r--r-- | src/axios_client.js | 2 | ||||
-rw-r--r-- | src/constants.js | 20 | ||||
-rw-r--r-- | src/main.js | 6 | ||||
-rw-r--r-- | src/session.js | 221 |
6 files changed, 98 insertions, 176 deletions
diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..beac898 --- /dev/null +++ b/src/.env.example @@ -0,0 +1,2 @@ +A_NUMBER=A12345671 +PASSWORD=
\ No newline at end of file diff --git a/src/aggietime.js b/src/aggietime.js index 2cc69e3..b887e36 100644 --- a/src/aggietime.js +++ b/src/aggietime.js @@ -9,6 +9,7 @@ import { OPEN_SHIFT_EXP_SEC, } from "./constants.js"; +import { with_exponential_retry } from "./exponential_retry.js"; import { client } from "./axios_client.js"; import expireCache from "expire-cache"; @@ -34,16 +35,18 @@ const get_user_position_or_specified = async (position) => { export const get_user_info = async () => { if (!expireCache.get("user")) { - const user = await aggietime.get(USER_PATH).then(({ data, config }) => { - const csrf_token = config.jar - .toJSON() - .cookies.find( - ({ domain, key }) => - domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN" - ).value; - expireCache.set("aggietime-csrf", csrf_token); - return data; - }); + const user = await with_exponential_retry(() => + aggietime.get(USER_PATH).then(({ data, config }) => { + const csrf_token = config.jar + .toJSON() + .cookies.find( + ({ domain, key }) => + domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN" + ).value; + expireCache.set("aggietime-csrf", csrf_token); + return data; + }) + ); expireCache.set("user", user, USER_CACHE_EXP_SEC); } diff --git a/src/axios_client.js b/src/axios_client.js index fd7325c..4decab0 100644 --- a/src/axios_client.js +++ b/src/axios_client.js @@ -2,5 +2,5 @@ import { wrapper } from "axios-cookiejar-support"; import { CookieJar } from "tough-cookie"; import axios from "axios"; -const jar = new CookieJar(); +export const jar = new CookieJar(); export const client = wrapper(axios.create({ jar, withCredentials: true })); diff --git a/src/constants.js b/src/constants.js index 1f8a8e0..b2e3f9a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,28 +3,24 @@ export const KILL_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT"]; export const AGGIETIME_DOMAIN = "aggietimeultra.usu.edu"; export const AGGIETIME_URI = `https://${AGGIETIME_DOMAIN}`; +export const AGGIETIME_AUTH_COOKIE_NAME = "access_token_cookie"; +export const AGGIETIME_URL_CONTAINS_SIGNIFIES_AUTH_COMPLETE = "employee"; + export const REFRESH_JWT_MS = 5 * 1000 * 60; export const LOGIN_PATH = "api/v1/auth/login"; -export const LOGOUT_PATH = "api/v1/auth/logout"; export const CLOCKIN_PATH = "api/v1/positions/:position/clock_in"; export const CLOCKOUT_PATH = "api/v1/positions/:position/clock_out"; export const USER_PATH = "api/v1/auth/get_user_info"; export const OPEN_SHIFT_PATH = "api/v1/users/:anumber/open_shift"; export const OPEN_SHIFT_EXP_SEC = 60; -export const EXECUTION_SELECTOR = "input[type=hidden][name=execution]"; -export const DUO_IFRAME_SELECTOR = "#duo_iframe"; -export const DUO_FACTOR = "Duo Push"; -export const DUO_INPUT_FIELD_SELECTORS = [ - "input[type=hidden][name=sid]", - "input[type=hidden][name=out_of_date]", - "input[type=hidden][name=days_out_of_date]", - "input[type=hidden][name=days_to_block]", - "input[type=hidden][name=preferred_device]", -]; - export const USER_CACHE_EXP_SEC = 30; +export const SAML_SIGN_IN_TITLE = "Sign in to your account"; +export const SAML_SUBMIT_SELECTOR = "input[type=submit]"; +export const SAML_EMAIL_SELECTOR = "input[type=email]"; +export const SAML_PASSWORD_SELECTOR = "input[type=password]"; + export const MAX_DEFAULT_RETRY_AMOUNT = 3; export const WAIT_MS = 2000; export const RETRY_EXPONENT = 1.2; diff --git a/src/main.js b/src/main.js index c81a471..4b8a1bf 100644 --- a/src/main.js +++ b/src/main.js @@ -5,7 +5,6 @@ import { KILL_SIGNALS, REFRESH_JWT_MS, } from "./constants.js"; -import { with_exponential_retry } from "./exponential_retry.js"; import * as actions from "./actions.js"; import * as session from "./session.js"; import * as argparse from "argparse"; @@ -87,9 +86,8 @@ specify another socket path with --socket_path` process.exit(1); } - await with_exponential_retry(() => - session.login(process.env.A_NUMBER, process.env.PASSWORD) - ); + await session.login(process.env.A_NUMBER, process.env.PASSWORD); + session.refresh_jwt(); setInterval(session.refresh_jwt, REFRESH_JWT_MS); diff --git a/src/session.js b/src/session.js index a127ac2..aee49c2 100644 --- a/src/session.js +++ b/src/session.js @@ -1,112 +1,19 @@ +import { Builder, Browser, By, Key, until } from "selenium-webdriver"; +import { Cookie } from "tough-cookie"; + import { + AGGIETIME_AUTH_COOKIE_NAME, + AGGIETIME_DOMAIN, AGGIETIME_URI, + AGGIETIME_URL_CONTAINS_SIGNIFIES_AUTH_COMPLETE, LOGIN_PATH, - LOGOUT_PATH, - USER_PATH, - DUO_IFRAME_SELECTOR, - DUO_FACTOR, - DUO_INPUT_FIELD_SELECTORS, - EXECUTION_SELECTOR, + SAML_SIGN_IN_TITLE, + SAML_SUBMIT_SELECTOR, + SAML_EMAIL_SELECTOR, + SAML_PASSWORD_SELECTOR, } from "./constants.js"; +import { jar } from "./axios_client.js"; import * as aggietime from "./aggietime.js"; -import { client } from "./axios_client.js"; - -import { parse } from "node-html-parser"; - -const make_auth_params = (username, password, execution) => - new URLSearchParams({ - username, - password, - execution, - _eventId: "submit", - geolocation: "", - }); - -const make_duo_push_params = ( - sid, - out_of_date, - days_out_of_date, - days_to_block, - device -) => - new URLSearchParams({ - sid, - out_of_date, - days_out_of_date, - days_to_block, - device, - factor: DUO_FACTOR, - }); - -const push_duo_get_cookie = async ( - duo_iframe_obj, - response_url, - username, - password, - execution -) => { - const [duo_host, duo_sig, duo_src] = [ - "data-host", - "data-sig-request", - "src", - ].map((attr) => duo_iframe_obj.getAttribute(attr)); - - const duo = client.create({ - baseURL: `https://${duo_host}`, - }); - const transaction_id = duo_sig.split(":").at(0); - const app = duo_sig.split(":APP").at(-1); - - console.log("Retrieving DUO frame DOM for this transaction..."); - const duo_frame = await duo - .post( - `/frame/web/v1/auth?tx=${transaction_id}&parent=${response_url}&v=2.6` - ) - .then(({ data }) => parse(data)); - - const push_param_list = DUO_INPUT_FIELD_SELECTORS.map((selector) => - duo_frame.querySelector(selector).getAttribute("value") - ); - let [sid, _] = push_param_list; - - const { - response: { txid }, - } = await duo - .post("/frame/prompt", make_duo_push_params.apply(null, push_param_list)) - .then(({ data }) => data); - - console.log("Waiting for approval..."); - const { cookie, parent } = await wait_approve_duo_cookie_resp(duo, sid, txid); - return { duo_signed_resp: cookie + ":APP" + app, parent }; -}; - -const wait_approve_duo_cookie_resp = async (duo, sid, txid) => { - // First status to confirm device was pushed to, - // Second to create a long-poll connection-alive socket for approval status :3 - const status_params = new URLSearchParams({ - sid, - txid, - }); - const { - response: { result_url }, - } = await duo.post("/frame/status", status_params).then(async ({ data }) => { - if (data.stat === "OK" && data.response.status_code === "pushed") - return await duo - .post("/frame/status", status_params) - .then(({ data }) => data); - return data; - }); - - const { - response: { cookie, parent }, - } = await duo - .post(result_url, new URLSearchParams({ sid })) - .then(({ data }) => data); - - if (!cookie) throw "Unable to retrieve signed cookie from DUO"; - - return { cookie, parent }; -}; export const refresh_jwt = () => { console.log("Refreshing JWT..."); @@ -116,51 +23,67 @@ export const refresh_jwt = () => { export const logout = () => client.get(`${AGGIETIME_URI}/${LOGOUT_PATH}`); -export const login = async (username, password) => { - const login_page_promise = client.get(`${AGGIETIME_URI}/${LOGIN_PATH}`); - console.log("Retreiving login page..."); - - const { - request: { - res: { responseUrl: response_url }, - }, - } = await login_page_promise; - - let cas_root = await login_page_promise.then(({ data }) => parse(data)); - - console.log("Parsing DOM for spring execution token..."); - const login_execution = cas_root - .querySelector(EXECUTION_SELECTOR) - .getAttribute("value"); - - console.log("Sending CAS credentials..."); - cas_root = await client - .post(response_url, make_auth_params(username, password, login_execution)) - .then(({ data }) => parse(data)); - - console.log("Parsing DOM for authenticated spring execution token..."); - const authed_execution = cas_root - .querySelector(EXECUTION_SELECTOR) - .getAttribute("value"); - - const duo_iframe_obj = cas_root.querySelector(DUO_IFRAME_SELECTOR); - console.log("Starting DUO authentication..."); - const { duo_signed_resp, parent: signed_response_url } = - await push_duo_get_cookie( - duo_iframe_obj, - response_url, - username, - password, - login_execution +export const login = async (a_number, password) => { + const driver = await new Builder().forBrowser(Browser.CHROME).build(); + let cookie; + + try { + console.log("Navigating to login path..."); + await driver.get(`${AGGIETIME_URI}/${LOGIN_PATH}`); + + if (a_number && password) { + console.log("Waiting until we eventually redirect to SAML..."); + await driver.wait(until.titleIs(SAML_SIGN_IN_TITLE)); + + console.log("Waiting until email field is located..."); + await driver.wait(until.elementLocated(By.css(SAML_EMAIL_SELECTOR))); + + console.log("Filling email field..."); + await driver + .findElement(By.css(SAML_EMAIL_SELECTOR)) + .sendKeys(`${a_number}@usu.edu`); + await driver.findElement(By.css(SAML_SUBMIT_SELECTOR)).click(); + + console.log("Waiting until password field is located..."); + await Promise.all( + [SAML_PASSWORD_SELECTOR, SAML_SUBMIT_SELECTOR].map((selector) => + driver.wait(until.elementLocated(By.css(selector))) + ) + ); + + console.log("Filling password..."); + await driver + .findElement(By.css(SAML_PASSWORD_SELECTOR)) + .sendKeys(password); + + console.log("Debouncing a bit..."); + await new Promise((res) => setTimeout(res, 500)); + + console.log("Submit!"); + await driver.findElement(By.css(SAML_SUBMIT_SELECTOR)).click(); + } + + console.log( + "Waiting for aggietime response (potential DUO required here)..." + ); + await driver.wait( + until.urlContains(AGGIETIME_URL_CONTAINS_SIGNIFIES_AUTH_COMPLETE) + ); + + console.log("Retrieving cookie..."); + cookie = await driver.manage().getCookie(AGGIETIME_AUTH_COOKIE_NAME); + + await jar.setCookie( + new Cookie({ + ...cookie, + key: cookie.name, + }), + AGGIETIME_URI ); + console.log("Got it!"); + } finally { + await driver.quit(); + } - console.log("Sending DUO signed response back to CAS..."); - return await client.post( - signed_response_url, - new URLSearchParams({ - execution: authed_execution, - signedDuoResponse: duo_signed_resp, - _eventId: "submit", - }) - ); + return cookie; }; |