Skip to content

Commit

Permalink
Add nonce and state (#11)
Browse files Browse the repository at this point in the history
* refator: factorize env var reading

* feat: add nonce and state by default

* refactor: more factorization

* feat: show agent connect button

* tests: upload video in case of failure
  • Loading branch information
rdubigny authored Mar 22, 2024
1 parent 98cbee8 commit 81602e1
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 81 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ jobs:
working-directory: ./e2e
start: npm start
wait-on: http://localhost:3000
# Store tests runs in case of failure
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
path: e2e/cypress/screenshots
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-videos
path: e2e/cypress/videos
2 changes: 2 additions & 0 deletions e2e/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export default defineConfig({
baseUrl: "http://localhost:3000",
specPattern: "**/*.feature",
setupNodeEvents,
video: true,
videoCompression: 32,
supportFile: false,
},
env: {
Expand Down
134 changes: 64 additions & 70 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import "dotenv/config";
import express from "express";
import { generators, Issuer } from "openid-client";
import { Issuer } from "openid-client";
import cookieSession from "cookie-session";
import morgan from "morgan";
import * as crypto from "crypto";

const port = parseInt(process.env.PORT, 10) || 3000;
const origin = `${process.env.HOST}`;
Expand Down Expand Up @@ -41,38 +42,81 @@ app.get("/", async (req, res, next) => {
userinfo: JSON.stringify(req.session.userinfo, null, 2),
idtoken: JSON.stringify(req.session.idtoken, null, 2),
oauth2token: JSON.stringify(req.session.oauth2token, null, 2),
showAgentConnectButton: process.env.SHOW_AGENTCONNECT_BUTTON === "true",
});
} catch (e) {
next(e);
}
});

app.post("/login", async (req, res, next) => {
try {
const client = await getMcpClient();
const acr_values = process.env.ACR_VALUES
? process.env.ACR_VALUES.split(",")
: null;

const redirectUrl = client.authorizationUrl({
scope: process.env.MCP_SCOPES,
// claims: { id_token: { amr: { essential: true } } },
login_hint: process.env.LOGIN_HINT || null,
acr_values,
});
const getAuthorizationControllerFactory = (extraParams) => {
return async (req, res, next) => {
try {
const client = await getMcpClient();
const acr_values = process.env.ACR_VALUES
? process.env.ACR_VALUES.split(",")
: null;
const login_hint = process.env.LOGIN_HINT || null;
const scope = process.env.MCP_SCOPES;
const nonce = crypto.randomBytes(16).toString("hex");
const state = crypto.randomBytes(16).toString("hex");

req.session.state = state;
req.session.nonce = nonce;

const redirectUrl = client.authorizationUrl({
scope,
login_hint,
acr_values,
nonce,
state,
...extraParams,
});

res.redirect(redirectUrl);
} catch (e) {
next(e);
}
};
};

res.redirect(redirectUrl);
} catch (e) {
next(e);
}
});
app.post("/login", getAuthorizationControllerFactory());

app.post(
"/select-organization",
getAuthorizationControllerFactory({
prompt: "select_organization",
}),
);

app.post(
"/update-userinfo",
getAuthorizationControllerFactory({
prompt: "update_userinfo",
}),
);

app.post(
"/force-login",
getAuthorizationControllerFactory({
claims: { id_token: { auth_time: { essential: true } } },
prompt: "login",
// alternatively, you can use the 'max_age: 0'
// if so, claims parameter is not necessary as auth_time will be returned
}),
);

app.get(process.env.CALLBACK_URL, async (req, res, next) => {
try {
const client = await getMcpClient();
const params = client.callbackParams(req);
const tokenSet = await client.callback(redirectUri, params);
const tokenSet = await client.callback(redirectUri, params, {
nonce: req.session.nonce,
state: req.session.state,
});

req.session.nonce = null;
req.session.state = null;
req.session.userinfo = await client.userinfo(tokenSet.access_token);
req.session.idtoken = tokenSet.claims();
req.session.oauth2token = tokenSet;
Expand All @@ -83,37 +127,6 @@ app.get(process.env.CALLBACK_URL, async (req, res, next) => {
}
});

app.post("/select-organization", async (req, res, next) => {
try {
const client = await getMcpClient();

const redirectUrl = client.authorizationUrl({
scope: process.env.MCP_SCOPES,
login_hint: process.env.LOGIN_HINT || null,
prompt: "select_organization",
});

res.redirect(redirectUrl);
} catch (e) {
next(e);
}
});

app.post("/update-userinfo", async (req, res, next) => {
try {
const client = await getMcpClient();
const redirectUrl = client.authorizationUrl({
scope: process.env.MCP_SCOPES,
login_hint: process.env.LOGIN_HINT || null,
prompt: "update_userinfo",
});

res.redirect(redirectUrl);
} catch (e) {
next(e);
}
});

app.post("/logout", async (req, res, next) => {
try {
req.session = null;
Expand All @@ -128,25 +141,6 @@ app.post("/logout", async (req, res, next) => {
}
});

app.post("/force-login", async (req, res, next) => {
try {
const client = await getMcpClient();

const redirectUrl = client.authorizationUrl({
scope: process.env.MCP_SCOPES,
claims: { id_token: { auth_time: { essential: true } } },
login_hint: process.env.LOGIN_HINT || null,
prompt: "login",
// alternatively, you can use the 'max_age: 0'
// if so, claims parameter is not necessary as auth_time will be returned
});

res.redirect(redirectUrl);
} catch (e) {
next(e);
}
});

app.listen(port, () => {
console.log(`App listening on port ${port}`);
console.log(process.env);
Expand Down
Loading

0 comments on commit 81602e1

Please sign in to comment.