2021-09-01 12:46:41 +00:00
|
|
|
const joi = require('joi')
|
|
|
|
const url = require('url')
|
|
|
|
const querystring = require('querystring')
|
|
|
|
const got = require('got')
|
|
|
|
const config = require('src/config/index.js')
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const mod = {}
|
|
|
|
module.exports = mod
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
2021-09-01 11:30:21 +00:00
|
|
|
mod.getAuthURL = (state) => {
|
2021-08-31 10:24:42 +00:00
|
|
|
const input = joi
|
|
|
|
.object({
|
|
|
|
authorized_endpoint: joi.string().required(),
|
|
|
|
token_endpoint: joi.string().required(),
|
|
|
|
client_id: joi.string().required(),
|
|
|
|
client_secret: joi.string().required(),
|
2021-09-01 12:46:41 +00:00
|
|
|
state: joi.string().allow('', null).default('')
|
2021-08-31 10:24:42 +00:00
|
|
|
})
|
|
|
|
.unknown()
|
2021-09-01 12:46:41 +00:00
|
|
|
.validate({ ...config.sso, state })
|
|
|
|
if (input.error) throw new Error(input.error.message)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string }}}
|
|
|
|
*/
|
2021-09-01 12:46:41 +00:00
|
|
|
const { value } = input
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
const qs = {
|
|
|
|
client_id: value.client_id,
|
2021-09-01 12:46:41 +00:00
|
|
|
scope: 'offline_access',
|
|
|
|
response_type: 'code',
|
|
|
|
redirect_uri: redirectUri.toString()
|
|
|
|
}
|
|
|
|
if (value.state) qs.state = state
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
return `${value.authorized_endpoint}?${querystring.stringify(qs)}`
|
|
|
|
}
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 07:20:53 +00:00
|
|
|
/**
|
|
|
|
* @return {string}
|
|
|
|
*/
|
2021-08-31 10:24:42 +00:00
|
|
|
mod.getLogoutURL = () => {
|
|
|
|
const input = joi
|
|
|
|
.object({
|
2021-09-01 12:46:41 +00:00
|
|
|
logout_endpoint: joi.string().required()
|
2021-08-31 10:24:42 +00:00
|
|
|
})
|
|
|
|
.unknown()
|
2021-09-01 12:46:41 +00:00
|
|
|
.validate({ ...config.sso })
|
|
|
|
if (input.error) throw new Error(input.error.message)
|
|
|
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const qs = { state: 'logout', redirect_uri: redirectUri.toString() }
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
return `${input.value.logout_endpoint}?${querystring.stringify(qs)}`
|
|
|
|
}
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
/**
|
|
|
|
* @param {string} token
|
|
|
|
* @return {Promise<{username: string, display_name: string, email: string, groups: string[]}>}
|
|
|
|
*/
|
2021-09-01 11:30:21 +00:00
|
|
|
mod.getUserInfo = async (token) => {
|
2021-09-01 12:46:41 +00:00
|
|
|
/**
|
|
|
|
* @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()
|
|
|
|
})
|
2021-09-01 11:30:21 +00:00
|
|
|
.unknown()
|
2021-09-01 12:46:41 +00:00
|
|
|
.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 }
|
|
|
|
}
|
2021-09-01 11:30:21 +00:00
|
|
|
|
2021-08-31 10:24:42 +00:00
|
|
|
/**
|
|
|
|
* @typedef SSOAccount
|
2021-09-01 07:20:53 +00:00
|
|
|
* @property {string} access_token
|
|
|
|
* @property {string} refresh_token
|
2021-08-31 10:24:42 +00:00
|
|
|
* @property {string} username
|
|
|
|
* @property {string} display_name
|
|
|
|
* @property {string} email
|
2021-09-01 12:46:41 +00:00
|
|
|
* @property {string[]} groups
|
2021-08-31 10:24:42 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
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(),
|
2021-09-01 12:46:41 +00:00
|
|
|
code: joi.string().required()
|
2021-08-31 10:24:42 +00:00
|
|
|
})
|
|
|
|
.unknown()
|
2021-09-01 12:46:41 +00:00
|
|
|
.validate({ ...config.sso, state, code })
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
if (input.error) throw new Error(input.error.message)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {{value: { authorized_endpoint: string, token_endpoint: string, client_id: string, client_secret: string, state: string, code: string }}}
|
|
|
|
*/
|
2021-09-01 12:46:41 +00:00
|
|
|
const { value } = input
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const redirectUri = new url.URL('/oauth/redirect', config.server.url)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
const qs = {
|
|
|
|
client_id: value.client_id,
|
|
|
|
client_secret: value.client_secret,
|
|
|
|
redirect_uri: redirectUri.toString(),
|
|
|
|
code: value.code,
|
|
|
|
client_session_state: value.state,
|
2021-09-01 12:46:41 +00:00
|
|
|
grant_type: 'authorization_code'
|
|
|
|
}
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
const resp = await got.default.post(value.token_endpoint, {
|
|
|
|
headers: {
|
2021-09-01 12:46:41 +00:00
|
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
2021-08-31 10:24:42 +00:00
|
|
|
},
|
|
|
|
body: querystring.stringify(qs),
|
2021-09-01 12:46:41 +00:00
|
|
|
responseType: 'json'
|
|
|
|
})
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const { body } = resp
|
|
|
|
if (!body) throw new Error('resopnse body empty')
|
2021-09-01 11:30:21 +00:00
|
|
|
|
|
|
|
const {
|
|
|
|
access_token: accessToken,
|
2021-09-01 12:46:41 +00:00
|
|
|
refresh_token: refreshToken
|
|
|
|
} = body
|
2021-09-01 11:30:21 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
const userInfo = await mod.getUserInfo(accessToken)
|
|
|
|
if (!userInfo) throw new Error('user info get fail')
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 11:30:21 +00:00
|
|
|
// decode access token
|
2021-09-01 12:46:41 +00:00
|
|
|
console.log('user info ::: ', userInfo)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
2021-09-01 12:46:41 +00:00
|
|
|
console.log('body ::: ', body)
|
2021-08-31 10:24:42 +00:00
|
|
|
|
|
|
|
/** @type {SSOAccount} */
|
|
|
|
const ssoAccount = {
|
2021-09-01 07:20:53 +00:00
|
|
|
access_token: accessToken,
|
|
|
|
refresh_token: refreshToken,
|
2021-09-01 12:46:41 +00:00
|
|
|
...userInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
return ssoAccount
|
|
|
|
}
|