2021-06-28 02:46:04 +12:00
|
|
|
const env = require("../../environment")
|
|
|
|
const jwt = require("jsonwebtoken")
|
|
|
|
const database = require("../../db")
|
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
|
|
|
|
const {
|
|
|
|
StaticDatabases,
|
|
|
|
generateGlobalUserID,
|
|
|
|
ViewNames,
|
|
|
|
} = require("../../db/utils")
|
|
|
|
|
2021-07-08 00:45:33 +12:00
|
|
|
/**
|
|
|
|
* Attempt to parse the users email address.
|
|
|
|
*
|
|
|
|
* It is not guaranteed that the email will be returned by the user info endpoint (e.g. github connected account used in azure ad).
|
|
|
|
* Fallback to the id token where possible.
|
|
|
|
*
|
|
|
|
* @param {*} profile The structured profile created by passport using the user info endpoint
|
|
|
|
* @param {*} jwtClaims The raw claims returned in the id token
|
|
|
|
*/
|
|
|
|
function getEmail(profile, jwtClaims) {
|
|
|
|
if (profile._json.email) {
|
|
|
|
return profile._json.email
|
|
|
|
}
|
|
|
|
|
|
|
|
if (jwtClaims.email) {
|
|
|
|
return jwtClaims.email
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @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
|
|
|
|
* @returns
|
|
|
|
*/
|
2021-07-06 04:16:45 +12:00
|
|
|
async function authenticate(
|
|
|
|
issuer,
|
|
|
|
sub,
|
|
|
|
profile,
|
|
|
|
jwtClaims,
|
|
|
|
accessToken,
|
|
|
|
refreshToken,
|
|
|
|
idToken,
|
|
|
|
params,
|
|
|
|
done
|
|
|
|
) {
|
2021-06-28 02:46:04 +12:00
|
|
|
// Check the user exists in the instance DB by email
|
|
|
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
|
|
|
|
|
|
|
let dbUser
|
|
|
|
|
|
|
|
const userId = generateGlobalUserID(profile.id)
|
|
|
|
|
|
|
|
try {
|
2021-07-06 04:16:45 +12:00
|
|
|
// use the OIDC profile id
|
2021-06-28 02:46:04 +12:00
|
|
|
dbUser = await db.get(userId)
|
|
|
|
} catch (err) {
|
|
|
|
const user = {
|
|
|
|
_id: userId,
|
|
|
|
provider: profile.provider,
|
|
|
|
roles: {},
|
|
|
|
...profile._json,
|
|
|
|
}
|
|
|
|
|
2021-07-06 04:16:45 +12:00
|
|
|
// check if an account with the OIDC email address exists locally
|
2021-07-08 00:45:33 +12:00
|
|
|
const email = getEmail(profile, jwtClaims)
|
|
|
|
if (!email) {
|
|
|
|
return done(null, false, { message: "No email address found" })
|
|
|
|
}
|
|
|
|
|
2021-06-28 02:46:04 +12:00
|
|
|
const users = await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
2021-07-08 00:45:33 +12:00
|
|
|
key: email,
|
2021-06-28 02:46:04 +12:00
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
|
2021-07-06 04:16:45 +12:00
|
|
|
// OIDC user already exists by email
|
2021-06-28 02:46:04 +12:00
|
|
|
if (users.rows.length > 0) {
|
|
|
|
const existing = users.rows[0].doc
|
|
|
|
|
|
|
|
// remove the local account to avoid conflicts
|
|
|
|
await db.remove(existing._id, existing._rev)
|
|
|
|
|
|
|
|
// merge with existing account
|
|
|
|
user.roles = existing.roles
|
|
|
|
user.builder = existing.builder
|
|
|
|
user.admin = existing.admin
|
|
|
|
|
|
|
|
const response = await db.post(user)
|
|
|
|
dbUser = user
|
|
|
|
dbUser._rev = response.rev
|
|
|
|
} else {
|
2021-07-08 00:45:33 +12:00
|
|
|
return done(null, false, { message: "Email does not yet exist. You must set up your local budibase account first." })
|
2021-06-28 02:46:04 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// authenticate
|
|
|
|
const payload = {
|
|
|
|
userId: dbUser._id,
|
|
|
|
builder: dbUser.builder,
|
|
|
|
email: dbUser.email,
|
|
|
|
}
|
|
|
|
|
|
|
|
dbUser.token = jwt.sign(payload, env.JWT_SECRET, {
|
|
|
|
expiresIn: "1 day",
|
|
|
|
})
|
|
|
|
|
|
|
|
return done(null, dbUser)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
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-06 04:16:45 +12:00
|
|
|
exports.strategyFactory = async function (callbackUrl) {
|
2021-06-28 02:46:04 +12:00
|
|
|
try {
|
2021-07-06 04:16:45 +12:00
|
|
|
const configurationUrl =
|
|
|
|
"https://login.microsoftonline.com/2668c0dd-7ed2-4db3-b387-05b6f9204a70/v2.0/.well-known/openid-configuration"
|
|
|
|
const clientSecret = "g-ty~2iW.bo.88xj_QI6~hdc-H8mP2Xbnd"
|
|
|
|
const clientId = "bed2017b-2f53-42a9-8ef9-e58918935e07"
|
2021-06-28 02:46:04 +12:00
|
|
|
|
2021-07-06 04:16:45 +12:00
|
|
|
if (!clientId || !clientSecret || !callbackUrl || !configurationUrl) {
|
2021-06-28 02:46:04 +12:00
|
|
|
throw new Error(
|
2021-07-06 04:16:45 +12:00
|
|
|
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configurationUrl"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await fetch(configurationUrl)
|
2021-07-06 04:28:55 +12:00
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
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,
|
|
|
|
clientID: clientId,
|
|
|
|
clientSecret: clientSecret,
|
|
|
|
callbackURL: callbackUrl,
|
|
|
|
scope: "profile email",
|
|
|
|
},
|
|
|
|
authenticate
|
|
|
|
)
|
|
|
|
|
2021-06-28 02:46:04 +12:00
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
throw new Error("Error constructing OIDC authentication strategy", err)
|
|
|
|
}
|
|
|
|
}
|