const cookie = require('cookie')
const UB = require('@unitybase/ub')
const App = UB.App
const oidcConst = require('./oidcConst')
const { OpenIdProvider } = require('./openIdProvider')
/**
* OpenIDConnect client for UnityBase
*
const openID = require('@unitybase/openid-connect')
let oIdEndPoint = openID.registerEndpoint('openIDConnect')
oIdEndPoint.registerProvider('Google', {
authUrl: 'https://accounts.google.com/o/oauth2/auth',
tokenUrl: 'https://accounts.google.com/o/oauth2/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
userInfoHTTPMethod: 'GET',
scope: 'openid',
nonce: '123',
response_type: 'code',
client_id: '350085411136-lpj0qvr87ce0r0ae0a3imcm25joj2t2o.apps.googleusercontent.com',
client_secret: 'dF4qmUxhHoBAj-E1R8YZUCqA',
// getOnFinishAction: function (response) {
// return 'opener.$App.onFinishOpenIDAuth(' + JSON.stringify(response) + '); close();'
// },
getUserID: function(userInfo) {
let uID = UB.Repository('uba_user').attrs(['ID'])
.where('[name]', '=', userInfo.id).selectScalar()
return uID || null
}
})
*
* @module @unitybase/openid-connect
* @tutorial security_openidconnect
*/
const endpoints = {}
const providers = {}
module.exports.registerEndpoint = registerOpenIDEndpoint
module.exports.registerProvider = registerOpenIDProvider
module.exports.provider = provider
/**
* Return provider by name
* @param {string} providerName
* @returns {OpenIdProvider|undefined}
*/
function provider (providerName) {
return providers[providerName]
}
/**
* OpenID endpoint. Able to register providers
* @typedef {object} openIDEndpoint
* @property {Function} registerProvider
*/
/**
* @typedef {object} ProviderConfig
* @property {string} providerConfig.name
* @property {string} providerConfig.authUrl Provider's authorisation url
* @property {string} providerConfig.tokenUrl Provider's token url
* @property {string} providerConfig.userInfoUrl provider's userinfo url
* @property {string} [providerConfig.logoutUrl] Logout url
* @property {string} [providerConfig.userInfoHTTPMethod='GET'] Http method for userinfo request. Default GET
* @property {string} providerConfig.scope Requested scopes delimited by '+' symbol
* @property {string} [providerConfig.resource] Requested resource (required for MS ADFS3 windows server 2012)
* @property {string} [providerConfig.nonce] nonce TODO - generate random and cache in GlobalCache with expire
* @property {string} providerConfig.response_type response type. Must contain code. This module use code responce type.
* @property {string} providerConfig.client_id client_id. Get it from provider
* @property {string} [providerConfig.client_secret] client_secret. Get it from provider (not needed for ADFS3 - windows server 2012)
* @property {string} [providerConfig.cert] id.gov.ua specific - a BASE64 key distribution protocol certificate
* to which the response will be encrypted by the authentication server. see https://id.gov.ua/downloads/IDInfoProcessingD.pdf
* @property {string} [providerConfig.fields] id.gov.ua specific - comma separated names of the requested user certificate fields.
* If the names of the fields are not specified, then all available certificate fields are returned
* with information about the user (certificate owner) and the issuer during call to `userInfoUrl` provider endpoint
* @property {Function} providerConfig.getCustomFABody Function, that returns custom text included to final html success/fail response
* @property {Function} [providerConfig.getAuthCustomHeaders]
* @property {string} [providerConfig.response_mode='form_post'] One of: form_post, fragment, query
* @property {Function} providerConfig.getOnFinishAction Function, that returns client-side code to be run after success/fail response from OpenID provider.
* For example: `opener.$App.onFinishOpenIDAuth`. In case of success will be called with `{success: true, data: userData, secretWord: secretWord}`
* In case of fail `{success: false}`
* @property {Function} providerConfig.getUserID Called with one 1 parameter - provider's response for userInfo request. Must return user ID from uba_user entity if user is authorised or null else
* @memberOf openIDEndpoint
*/
/**
* Register openID connect endpoint. In case endpoint already registered - return existed
*
* @function
* @param {string} endpointName
* @returns {openIDEndpoint} endpoint
*/
function registerOpenIDEndpoint (endpointName) {
if (endpoints[endpointName]) {
return endpoints[endpointName]
}
App.registerEndpoint(endpointName, openIDConnectEp, false)
endpoints[endpointName] = {
/**
* Register OpenID provider
* @param {string} name
* @param {ProviderConfig} providerConfig
*/
registerProvider: function (name, providerConfig) {
registerOpenIDProvider(name, providerConfig)
},
getProviderList: function () {
return Object.keys(providers)
},
/**
* Get provider by name
* @param {string} providerName
* @returns {OpenIdProvider|null}
*/
getProvider: function (providerName) {
return provider(providerName)
}
}
return endpoints[endpointName]
}
/**
* Register OpenID provider
* @param {string} name
* @param {ProviderConfig} providerConfig
*/
function registerOpenIDProvider (name, providerConfig) {
if (providers[name]) {
console.info(`OIDC: provider ${name} already registered`)
} else {
providers[name] = new OpenIdProvider(name, providerConfig)
}
}
/**
* OpenID `Authorization Code Flow` (user interaction) endpoint implementation
* - https://tools.ietf.org/html/rfc6749#section-4.1
* - https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow
*
* If called as /endpoint - return a list of registered openIDConnect providers,
* If called as /endpoint/provider:
* - without parameters or with mode === 'auth' - redirect to provider `authUrl`
* - with parameters `code` and `state` - call doProviderAuthHandshake method
* - with parameters `logout` - redirect to log out url
*
* @param {THTTPRequest} req
* @param {THTTPResponse} resp
* @protected
*/
function openIDConnectEp (req, resp) {
const providerName = req.uri
const url = req.url.split('?')[0]
const endpointUrl = providerName ? url.substr(0, url.length - providerName.length - 1) : url
const endpointName = endpointUrl.substr(endpointUrl.lastIndexOf('/') - endpointUrl.length + 1)
const endpoint = endpoints[endpointName]
if (!endpoint) {
return resp.badRequest(`OpenID endpoint '${endpointName}' is not registered`)
}
if (!providerName) {
resp.statusCode = 200
resp.writeEnd(JSON.stringify(endpoint.getProviderList()))
return
}
const provider = endpoint.getProvider(providerName)
if (!provider) {
return resp.badRequest(`OpenID provider '${providerName}' is not registered`)
}
const redirectUrl = App.externalURL + (App.externalURL.endsWith('/') ? '' : '/') + endpointName + '/' + providerName
const params = (req.method === 'GET') ? req.parsedParameters : req.json()
if (!Object.keys(params).length || params.mode === 'auth') {
provider.redirectToProviderAuth(req, resp, redirectUrl, params)
} else if (params.code && params.state) {
const origin = req.getHeader('origin') || ''
// validate session ID from cookies match state
const cookiesStr = req.getHeader('Cookie') || ''
console.debug('Cookie header value:', cookiesStr)
const cookies = cookie.parse(cookiesStr)
const ssid = cookies[oidcConst.OIDC_SESSID_COOKIE]
let err = ''
let ssidKey
if (!ssid) {
// App.globalCachePut(OIDC_SESSID_COOKIE + randomSid, randomState)
// resp.writeHead(`Set-Cookie: ${OIDC_SESSID_COOKIE}=${randomSid}; Secure; HttpOnly`)
err = `OIDC: Cookie ${oidcConst.OIDC_SESSID_COOKIE} is required`
} else {
ssidKey = oidcConst.OIDC_SESSID_COOKIE + ssid
const sessionState = App.globalCacheGet(ssidKey)
if (!sessionState) {
err = `OIDC: state for ssid ${ssidKey} is not found`
} else if (sessionState !== params.state) {
err = `OIDC: initial state for ssid ${ssidKey} is ${sessionState} but got ${params.state}`
}
}
if (err) {
provider.notifyProviderError(resp, err)
} else {
App.globalCachePut(ssidKey, null) // remove key
provider.doProviderAuthHandshake(resp, params.code, params.state, redirectUrl, origin)
}
} else if (params.logout) {
provider.redirectToProviderLogout(req, resp, params)
} else {
provider.redirectToProviderAuth(req, resp, redirectUrl, params)
}
}