2021-07-06 04:16:45 +12:00
|
|
|
const fetch = require("node-fetch")
|
2021-06-28 02:46:04 +12:00
|
|
|
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
2021-07-08 22:12:34 +12:00
|
|
|
const { authenticateThirdParty } = require("./third-party-common")
|
2021-06-28 02:46:04 +12:00
|
|
|
|
2021-07-08 00:45:33 +12:00
|
|
|
/**
|
|
|
|
* @param {*} issuer The identity provider base URL
|
|
|
|
* @param {*} sub The user ID
|
|
|
|
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
|
|
|
* @param {*} jwtClaims The parsed id_token claims
|
|
|
|
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
|
|
|
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
|
|
|
* @param {*} idToken The id_token - always a JWT
|
|
|
|
* @param {*} params The response body from requesting an access_token
|
|
|
|
* @param {*} done The passport callback: err, user, info
|
|
|
|
*/
|
2021-07-06 04:16:45 +12:00
|
|
|
async function authenticate(
|
|
|
|
issuer,
|
|
|
|
sub,
|
|
|
|
profile,
|
|
|
|
jwtClaims,
|
|
|
|
accessToken,
|
|
|
|
refreshToken,
|
|
|
|
idToken,
|
|
|
|
params,
|
|
|
|
done
|
|
|
|
) {
|
2021-07-08 22:12:34 +12:00
|
|
|
const thirdPartyUser = {
|
|
|
|
// store the issuer info to enable sync in future
|
2021-07-09 00:12:25 +12:00
|
|
|
provider: issuer,
|
2021-07-08 22:12:34 +12:00
|
|
|
providerType: "oidc",
|
|
|
|
userId: profile.id,
|
|
|
|
profile: profile,
|
|
|
|
email: getEmail(profile, jwtClaims),
|
|
|
|
oauth2: {
|
|
|
|
accessToken: accessToken,
|
2021-07-09 00:12:25 +12:00
|
|
|
refreshToken: refreshToken,
|
|
|
|
},
|
2021-06-28 02:46:04 +12:00
|
|
|
}
|
|
|
|
|
2021-07-08 22:12:34 +12:00
|
|
|
return authenticateThirdParty(
|
2021-07-09 00:12:25 +12:00
|
|
|
thirdPartyUser,
|
2021-07-08 22:12:34 +12:00
|
|
|
false, // don't require local accounts to exist
|
2021-07-09 00:12:25 +12:00
|
|
|
done
|
|
|
|
)
|
2021-06-28 02:46:04 +12:00
|
|
|
}
|
|
|
|
|
2021-07-09 00:04:04 +12:00
|
|
|
/**
|
|
|
|
* @param {*} profile The structured profile created by passport using the user info endpoint
|
|
|
|
* @param {*} jwtClaims The claims returned in the id token
|
|
|
|
*/
|
|
|
|
function getEmail(profile, jwtClaims) {
|
|
|
|
// profile not guaranteed to contain email e.g. github connected azure ad account
|
|
|
|
if (profile._json.email) {
|
|
|
|
return profile._json.email
|
|
|
|
}
|
|
|
|
|
|
|
|
// fallback to id token email
|
|
|
|
if (jwtClaims.email) {
|
|
|
|
return jwtClaims.email
|
|
|
|
}
|
|
|
|
|
|
|
|
// fallback to id token preferred username
|
|
|
|
const username = jwtClaims.preferred_username
|
|
|
|
if (username && validEmail(username)) {
|
|
|
|
return username
|
|
|
|
}
|
|
|
|
|
2021-07-09 00:12:25 +12:00
|
|
|
return null
|
2021-07-09 00:04:04 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
function validEmail(value) {
|
|
|
|
return (
|
2021-07-09 00:12:25 +12:00
|
|
|
value &&
|
|
|
|
!!value.match(
|
|
|
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
|
|
)
|
2021-07-09 00:04:04 +12:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-06-28 02:46:04 +12:00
|
|
|
/**
|
2021-07-06 04:16:45 +12:00
|
|
|
* Create an instance of the oidc passport strategy. This wrapper fetches the configuration
|
2021-06-28 02:46:04 +12:00
|
|
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
2021-07-06 04:16:45 +12:00
|
|
|
* @returns Dynamically configured Passport OIDC Strategy
|
2021-06-28 02:46:04 +12:00
|
|
|
*/
|
2021-07-09 00:04:04 +12:00
|
|
|
exports.strategyFactory = async function (config, callbackUrl) {
|
2021-06-28 02:46:04 +12:00
|
|
|
try {
|
2021-07-10 03:21:42 +12:00
|
|
|
const { clientID, clientSecret, configUrl } = config
|
2021-06-28 02:46:04 +12:00
|
|
|
|
2021-07-10 03:21:42 +12:00
|
|
|
if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
|
2021-06-28 02:46:04 +12:00
|
|
|
throw new Error(
|
2021-07-09 00:04:04 +12:00
|
|
|
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl"
|
2021-07-06 04:16:45 +12:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-07-09 00:04:04 +12:00
|
|
|
const response = await fetch(configUrl)
|
2021-07-06 04:28:55 +12:00
|
|
|
|
|
|
|
if (!response.ok) {
|
2021-07-09 00:12:25 +12:00
|
|
|
throw new Error(
|
|
|
|
`Unexpected response when fetching openid-configuration: ${response.statusText}`
|
|
|
|
)
|
2021-06-28 02:46:04 +12:00
|
|
|
}
|
2021-07-06 04:28:55 +12:00
|
|
|
|
|
|
|
const body = await response.json()
|
|
|
|
|
|
|
|
return new OIDCStrategy(
|
|
|
|
{
|
|
|
|
issuer: body.issuer,
|
|
|
|
authorizationURL: body.authorization_endpoint,
|
|
|
|
tokenURL: body.token_endpoint,
|
|
|
|
userInfoURL: body.userinfo_endpoint,
|
2021-07-10 03:21:42 +12:00
|
|
|
clientID: clientID,
|
2021-07-06 04:28:55 +12:00
|
|
|
clientSecret: clientSecret,
|
2021-07-09 07:03:51 +12:00
|
|
|
callbackURL: callbackUrl,
|
2021-07-06 04:28:55 +12:00
|
|
|
},
|
|
|
|
authenticate
|
|
|
|
)
|
2021-06-28 02:46:04 +12:00
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
throw new Error("Error constructing OIDC authentication strategy", err)
|
|
|
|
}
|
|
|
|
}
|