const joi = require('joi') const url = require('url') const querystring = require('querystring') const got = require('got') const config = require('src/config/index.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)}` } /** * @param {string} token * @return {Promise<{username: string, display_name: string, email: string, groups: string[]}>} */ mod.getUserInfo = async (token) => { /** * @type {{ * client_id: string, * userinfo_endpoint: string, * token: string, * }} */ const input = await joi .object({ client_id: joi.string().required(), client_secret: joi.string().required(), userinfo_endpoint: joi.string().required(), token: joi.string().required() }) .unknown() .validateAsync({ ...config.sso, token }) try { const resp = await got.default.get(input.userinfo_endpoint, { responseType: 'json', headers: { Authorization: `Bearer ${input.token}` } }) const body = await joi.object({ name: joi.string(), email: joi.string().email().required(), groups: joi.array().items(joi.string()).default([]), family_name: joi.string(), given_name: joi.string(), preferred_username: joi.string() }).unknown().validateAsync(resp.body) const displayName = body.name || body.preferred_username || body.given_name || '' return { display_name: displayName, email: body.email, groups: body.groups, username: body.preferred_username || '' } } catch (err) { console.log(err) if (err instanceof got.HTTPError) { if (err.code === 401) { // try refresh token return null } } throw err } } mod.refreshToken = async (token) => { /** * @type {{ * token_endpoint: string, * client_id: string, * client_secret: string, * token: string, * }} */ const input = await joi.object({ token_endpoint: joi.string().required(), client_id: joi.string().required(), client_secret: joi.string().required(), token: joi.string().required() }).unknown().validateAsync({ ...config.sso, token }) const qs = { client_id: input.client_id, client_secret: input.client_secret, grant_type: 'refresh_token', refresh_token: input.token } const resp = await got.default.post(input.token_endpoint, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: querystring.stringify(qs), responseType: 'json' }) const body = await joi.object({ access_token: joi.string().required(), refresh_token: joi.string().required() }).unknown().validateAsync(resp.body) return { access_token: body.access_token, refresh_token: body.refresh_token } } /** * @typedef SSOAccount * @property {string} access_token * @property {string} refresh_token * @property {string} username * @property {string} display_name * @property {string} email * @property {string[]} groups */ 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 { access_token: accessToken, refresh_token: refreshToken } = body const userInfo = await mod.getUserInfo(accessToken) if (!userInfo) throw new Error('user info get fail') // decode access token console.log('user info ::: ', userInfo) console.log('body ::: ', body) /** @type {SSOAccount} */ const ssoAccount = { access_token: accessToken, refresh_token: refreshToken, ...userInfo } return ssoAccount }