const joi = require("joi"); const url = require("url"); const querystring = require("querystring"); const got = require("got"); const config = require("src/config/index.js"); const { jwt } = require("src/utils/pkgs.js"); const mod = {}; module.exports = mod; /** * @return {string} */ mod.getAuthURL = (state) => { const input = joi .object({ authorized_endpoint: joi.string().required(), token_endpoint: joi.string().required(), client_id: joi.string().required(), client_secret: joi.string().required(), state: joi.string().allow("", null).default(""), }) .unknown() .validate({ ...config.sso, state }); if (input.error) throw new Error(input.error.message); /** * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}} */ const { value } = input; const redirectUri = new url.URL("/oauth/redirect", config.server.url); const qs = { client_id: value.client_id, scope: "offline_access", response_type: "code", redirect_uri: redirectUri.toString(), }; if (value.state) qs.state = state; return `${value.authorized_endpoint}?${querystring.stringify(qs)}`; }; /** * @return {string} */ mod.getLogoutURL = () => { const input = joi .object({ logout_endpoint: joi.string().required(), }) .unknown() .validate({ ...config.sso }); if (input.error) throw new Error(input.error.message); const redirectUri = new url.URL("/oauth/redirect", config.server.url); const qs = { state: "logout", redirect_uri: redirectUri.toString() }; return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`; }; mod.getUserInfo = async (token) => { const input = joi .object() .unknown() .validateAsync({ ...config.sso, token }); }; /** * @typedef SSOAccount * @property {string} access_token * @property {string} refresh_token * @property {string} user_id * @property {string} username * @property {string} display_name * @property {string} email */ mod.getToken = async (code, state) => { const input = joi .object({ authorized_endpoint: joi.string().required(), token_endpoint: joi.string().required(), client_id: joi.string().required(), client_secret: joi.string().required(), state: joi.string().required(), code: joi.string().required(), }) .unknown() .validate({ ...config.sso, state, code }); if (input.error) throw new Error(input.error.message); /** * @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}} */ const { value } = input; const redirectUri = new url.URL("/oauth/redirect", config.server.url); const qs = { client_id: value.client_id, client_secret: value.client_secret, redirect_uri: redirectUri.toString(), code: value.code, client_session_state: value.state, grant_type: "authorization_code", }; const resp = await got.default.post(value.token_endpoint, { headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: querystring.stringify(qs), responseType: "json", }); const { body } = resp; if (!body) throw new Error("resopnse body empty"); const { id_token: idToken, access_token: accessToken, refresh_token: refreshToken, } = body; // if (!idToken) throw new Error("get id token fail"); // const decoded = jwt.decode(idToken); // if (!decoded || typeof decoded !== "object") // throw new Error("jwt decode fail"); // console.log("decoded ::: ", decoded); const decoded = jwt.decode(accessToken); // decode access token console.log("token ::: ", jwt.decode(accessToken)); console.log("body ::: ", body); // @ts-ignore const { preferred_username: preferredUsername } = decoded; if (!preferredUsername) throw new Error("id token field missing"); const displayName = `${decoded.family_name ?? ""}${decoded.given_name ?? ""}`; /** @type {SSOAccount} */ const ssoAccount = { access_token: accessToken, refresh_token: refreshToken, user_id: decoded.sub, username: preferredUsername.toLowerCase(), display_name: displayName ?? preferredUsername, email: decoded.email ?? "", }; return ssoAccount; };