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: 'openid', 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)}`; }; /** * @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) 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; };