keycloak-demo/utils/sso/index.js

226 lines
5.9 KiB
JavaScript

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
}