Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement agent connect authentication #282

Merged
merged 19 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ AFTER_MEETING_SURVEY_URL=https://startupdetat.typeform.com/to/CQhQfpVU
ENCRYPT_SECRET=un_secret_avec_exactement_64_bit

# Use OIDC auth instead of audioconf's magiclink auth
#FEATURE_OIDC=true
#OIDC_PROVIDER_URL=
#OIDC_CLIENT_ID=
#OIDC_CLIENT_SECRET=
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ jobs:
SECRET: fakesessionsecret
ENCRYPT_SECRET: un_secret_avec_exactement_64_bit
FEATURE_WEB_ACCESS: true
OIDC_ACR_VALUES: eidas1
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
AFTER_MEETING_SURVEY_URL: https://startupdetat.typeform.com/to/R5uC1b0k
4 changes: 3 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ config.FEATURE_JOB_COMPUTE_STATS = process.env.FEATURE_JOB_COMPUTE_STATS === "tr
config.FEATURE_JOB_ANONYMIZE_EMAILS = process.env.FEATURE_JOB_ANONYMIZE_EMAILS === "true" || false
config.FEATURE_JOB_CALLS_STATS = process.env.FEATURE_JOB_CALLS_STATS === "true" || false
config.FEATURE_WEB_ACCESS = process.env.FEATURE_WEB_ACCESS === "true" || false
config.FEATURE_OIDC = process.env.FEATURE_OIDC === "true" || false

config.ANNOUNCEMENTS = process.env.ANNOUNCEMENTS ? process.env.ANNOUNCEMENTS.split("|") : []

Expand All @@ -112,6 +111,9 @@ config.STATS_EXTERNAL_DASHBOARD_URL = process.env.STATS_EXTERNAL_DASHBOARD_URL
config.OIDC_PROVIDER_URL = process.env.OIDC_PROVIDER_URL
config.OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID
config.OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET
config.OIDC_ACR_VALUES = process.env.OIDC_ACR_VALUES
config.OIDC_ID_TOKEN_SIGNED_ALG = process.env.OIDC_ID_TOKEN_SIGNED_ALG
config.OIDC_USER_INFO_SIGNED_ALG = process.env.OIDC_USER_INFO_SIGNED_ALG

config.RIZOMO_URI = process.env.RIZOMO_URI

Expand Down
7 changes: 2 additions & 5 deletions controllers/createConfController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const config = require("../config")
const db = require("../lib/db")
const emailer = require("../lib/emailer")
const format = require("../lib/format")
const magicLinkAuth = require("../lib/magicLinkAuth")
const oidcAuth = require("../lib/oidcAuth")
const urls = require("../urls")
const { isAcceptedEmail } = require("../lib/emailChecker")
Expand Down Expand Up @@ -45,9 +44,7 @@ const createConfWithDay = async (email, conferenceDay, userTimezoneOffset) => {
}

module.exports.createConf = async (req, res) => {
const confData = await (config.FEATURE_OIDC ?
oidcAuth.finishAuth(req) :
magicLinkAuth.finishAuth(req))
const confData = await oidcAuth.finishAuth(req)

const { email, durationInMinutes, conferenceDay, userTimezoneOffset } = confData

Expand Down Expand Up @@ -146,4 +143,4 @@ module.exports.cancelConf = async (req, res) => {

function shouldSendWebAccessMail(email) {
return isAcceptedEmail(email, config.EMAIL_WEB_ACCESS_WHITELIST) && config.FEATURE_WEB_ACCESS
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
const config = require("../config.js")
const magicLinkAuth = require("../lib/magicLinkAuth")
const oidcAuth = require("../lib/oidcAuth")

const urls = require("../urls")

module.exports.startAuth = async (req, res) => {
const userTimezoneOffset = req.body.userTimezoneOffset
const email = req.body.email
const conferenceDurationInMinutes = req.body.durationInMinutes
const conferenceDayString = req.body.day
if (typeof conferenceDayString === 'undefined' && typeof conferenceDurationInMinutes === 'undefined') {
throw new Error('Both conferenceDayString and conferenceDurationInMinutes are undefined. This should not happen.')
}

const authRequest = await (
config.FEATURE_OIDC ?
oidcAuth.startAuth(email, conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset) :
magicLinkAuth.startAuth(email, conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset)
)
const authRequest = await oidcAuth.startAuth(conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset)

if (authRequest.error) {
console.log("Error in authentication", authRequest.error)
Expand All @@ -26,3 +19,15 @@ module.exports.startAuth = async (req, res) => {

res.redirect(authRequest.redirectUrl)
}

module.exports.logout = async (req, res) => {
const user = req.session.user
if(!user){
return res.redirect(urls.landing)
}
const {id_token, state} = user
req.session.destroy()

const logoutUrl = await oidcAuth.getLogoutUrl({id_token_hint: id_token, state})
return res.redirect(logoutUrl)
}
8 changes: 6 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const dashboardController = require("./controllers/dashboardController")
const format = require("./lib/format")
const createConfController = require("./controllers/createConfController")
const landingController = require("./controllers/landingController")
const startAuthController = require("./controllers/startAuthController")
const userController = require("./controllers/userController")
const statusController = require("./controllers/statusController")
const stats = require("./lib/stats")
const urls = require("./urls")
Expand Down Expand Up @@ -70,12 +70,14 @@ app.use(function(req, res, next){
res.locals.urls = urls
res.locals.version = version
res.locals.siteUrl = config.HOSTNAME_WITH_PROTOCOL
res.locals.user = req.session.user
next()
})

app.get(urls.landing, landingController.getLanding)

app.post(urls.startAuth, startAuthController.startAuth)
app.post(urls.startAuth, userController.startAuth)
app.post(urls.logout, userController.logout)

app.get(urls.validationEmailSent, (req, res) => {
res.render("validationEmailSent", {
Expand Down Expand Up @@ -139,6 +141,8 @@ app.get(urls.faq, (req, res) => {
})
})

app.get(urls.logout, userController.logout)

app.get(urls.status, statusController.getStatus)

app.use(Sentry.Handlers.errorHandler())
Expand Down
84 changes: 0 additions & 84 deletions lib/magicLinkAuth.js

This file was deleted.

58 changes: 40 additions & 18 deletions lib/oidcAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const { generators, Issuer } = require("openid-client")
const config = require("../config.js")
const urls = require("../urls")
const db = require("../lib/db")
const format = require("../lib/format")

const urlCallback = urls.createConf

Expand All @@ -24,14 +23,15 @@ module.exports.getClient = async () => {
client_secret: config.OIDC_CLIENT_SECRET,
redirect_uris: [config.HOSTNAME_WITH_PROTOCOL + urlCallback],
response_types: ["code"],
// id_token_signed_response_alg (default "RS256")
// token_endpoint_auth_method (default "client_secret_basic")
id_token_signed_response_alg: config.OIDC_ID_TOKEN_SIGNED_ALG,
userinfo_signed_response_alg: config.OIDC_USER_INFO_SIGNED_ALG,
// token_endpoint_auth_method (default "client_secret_basic")
})

return client
}

module.exports.startAuth = async (email, conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset) => {
module.exports.startAuth = async (conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset) => {
const client = await this.getClient()

/* todo : store the code_verifier in DB. We don't use it for now.
Expand All @@ -49,20 +49,21 @@ module.exports.startAuth = async (email, conferenceDurationInMinutes, conference
const nonce = generators.random(128)

const redirectUrl = client.authorizationUrl({
scope: "openid",
scope: "openid uid email",
state,
acr_values: config.OIDC_ACR_VALUES,
/* todo add this back
code_challenge,
code_challenge_method: 'S256',
*/
nonce,
login_hint: email
// login_hint: email
})

// todo write test : null nonce fails
try {
await db.insertOidcRequest(state, nonce, conferenceDurationInMinutes, conferenceDayString, userTimezoneOffset)
console.log(`OIDC request créé pour ${format.hashForLogs(email)}`)
console.log(`OIDC request créé pour state ${state}`)
} catch(err) {
console.log("Error when inserting authrequest token in DB", err)
return { error: "Une erreur interne s'est produite, nous n'avons pas pu créer votre conférence." }
Expand Down Expand Up @@ -94,18 +95,34 @@ module.exports.finishAuth = async (req) => {
return { error: "L'identification a échoué. Entrez votre adresse mail ci-dessous pour recommencer." }
}

const tokenSet = await client.callback(
config.HOSTNAME_WITH_PROTOCOL + urlCallback,
params,
{
state: request.state,
nonce: request.nonce
// todo code_verifier: req.session.code_verifier
}
)
const claims = tokenSet.claims()
const email = claims.preferred_username
let tokenSet
try {
tokenSet = await client.callback(
config.HOSTNAME_WITH_PROTOCOL + urlCallback,
params,
{
state: request.state,
nonce: request.nonce
// todo code_verifier: req.session.code_verifier
}
)
} catch(error){
console.error("error when requesting token from OIDC", error)
return { error: "L'identification a échoué. Entrez votre adresse mail ci-dessous pour recommencer." }
}

let userinfo
try {
userinfo = await client.userinfo(tokenSet)
} catch(error){
console.error("error when requesting userinfo from OIDC", error)
return { error: "L'identification a échoué. Entrez votre adresse mail ci-dessous pour recommencer." }
}
const email = userinfo.email
const user = {id_token: tokenSet.id_token, state: request.state}

req.session.user = user

return {
email,
durationInMinutes: request.durationInMinutes,
Expand All @@ -114,3 +131,8 @@ module.exports.finishAuth = async (req) => {
}
}

module.exports.getLogoutUrl = async({state, id_token_hint}) => {
const client = await this.getClient()

return client.endSessionUrl({id_token_hint,post_logout_redirect_uri: `${config.HOSTNAME_WITH_PROTOCOL}${urls.landing}`,state})
}
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"homepage": "https://github.com/betagouv/conferences#readme",
"dependencies": {
"@gouvfr/dsfr": "^1.0.0-rc1.0",
"@gouvfr/dsfr": "^1.11.2",
"@sentry/node": "^6.2.2",
"chart.js": "^2.9.4",
"connect-flash": "^0.1.1",
Expand Down
Loading
Loading