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

[api] add Microsoft strategy to auth module (single sign-on) #3453

Merged
merged 5 commits into from
Sep 9, 2024

Conversation

freemvmt
Copy link
Contributor

@freemvmt freemvmt commented Jul 25, 2024

🎫 Ticket: https://trello.com/c/owvV9UZh

Summary

This PR adds the ability to sign into PlanX using a Microsoft account (personal or organisational), which most users (working in local government) are more likely to have to hand as compared to a Google account.

We make use of the existing passportjs implementation, adding a new microsoft-oidc strategy which leverages openid-client, which we consider reliable thanks to certification by OpenID themselves.

Since this package has some asynchronous behaviour for which the best code pattern involved using top-level awaits, this project induced some bonuses in api.planx.uk, namely adopting ESM throughout (#3464), which in turn required a migration to Vitest (#3555). We also took the opportunity to bump passportjs to latest (#3502).

See also #3221, which I used as a development/demo pizza, and has a much more granular commit structure.

TODO

  • Resolve various TODOs and polish
  • Resolve issue with Jest (see below)
  • Add front channel logout to OIDC setup
  • Further security hardening (e.g. validating ID tokens)
  • Add unit and integration tests
  • Document changes in README and as a new ADR (including Azure/Entra setup)

We will address anything not included here in further PRs.

Jest/ESM issue ✅

The stubborn issue here is that Jest (or ts-jest, the transformer we're using to transpile .ts files into .js files that Jest can read) will not treat files as ES modules (despite following docs to the letter), and hence throws errors like the following:

 FAIL  ./server.test.js
  ● Test suite failed to run

    server.ts:126:18 - error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher.

    126 const passport = await getPassport();
                         ~~~~~

I examine some of the background for this in #3528, onto which this PR is chained.

Below is a non-exhaustive list of possible approaches to this problem, which I've marked complete if they've been fully explored and not provided a solution:

  • Use an appropriate ts-jest preset instead of manual config
  • Try latest Jest v30 alpha - for example, alpha5 includes a fix which purports to 'allow Node16/NodeNext/Bundler moduleResolution in project's tsconfig', which seems relevant
  • Change config file to jest.config.js
  • Take seriously the idea (suggested by above error) that Jest is failing to read the tsconfig at all
  • Switch off above diagnostic message, as @jessicamcinchak did to resolve a different problem in fix: refactor uses of __dirname now that API is using esnext #3507 - doing so reveals ReferenceError: await is not defined, which only adds to our case that modules are not being treated as ESM
  • Implement verbatimModuleSyntax (didn't resolve issue, but was edifying - see [api] implement verbatim module syntax in tsconfig #3528)
  • Replace ts-jest with @swc/jest or babel-jest (failing examples in api-add-microsoft-sso-swc and api-add-microsoft-sso-babel respectively) - could include using multiple transformers, e.g. babel for .js, ts-jest for .ts
  • Try using .mjs/.mts file endings (not a desirable outcome)
  • Just let Jest consider whole codebase as commonJS
  • Replace top-level await with IIFE (would feel like a retreat, but may be necessary...)

Worth noting that even Jest advises that ESM support is experimental, and indeed their issue tracking this is very much still open. This could suggest we should lean to some of the latter, more climb down-y options.

🏁 In the end we abandoned Jest in favour of Vitest, which is much more ESM-friendly! See #3555 :)

api.planx.uk/modules/auth/routes.ts Dismissed Show dismissed Hide dismissed
api.planx.uk/modules/auth/routes.ts Dismissed Show dismissed Hide dismissed
api.planx.uk/modules/auth/routes.ts Dismissed Show dismissed Hide dismissed
api.planx.uk/modules/auth/routes.ts Dismissed Show dismissed Hide dismissed
api.planx.uk/server.ts Fixed Show fixed Hide fixed
Copy link

github-actions bot commented Jul 25, 2024

Removed vultr server and associated DNS entries

@freemvmt freemvmt mentioned this pull request Jul 27, 2024
3 tasks
Copy link
Contributor

@DafyddLlyr DafyddLlyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bearing with me whilst I got through this. Will give it another pass through once top level await takes away a lot of the changes here 👍

Code looking great - easy to follow and clear!

api.planx.uk/modules/auth/passport.ts Outdated Show resolved Hide resolved
api.planx.uk/modules/auth/strategy/microsoft-oidc.ts Outdated Show resolved Hide resolved
api.planx.uk/modules/auth/strategy/microsoft-oidc.ts Outdated Show resolved Hide resolved
editor.planx.uk/src/pages/Login.tsx Outdated Show resolved Hide resolved
@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch 3 times, most recently from 8ffa211 to ac7a60e Compare August 15, 2024 19:10
@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch from ac7a60e to d9f76ad Compare August 16, 2024 20:58
@freemvmt freemvmt changed the base branch from main to implement-verbatim-module-syntax August 16, 2024 20:59
@freemvmt
Copy link
Contributor Author

Rebased this PR onto #3528 - full explanation given in that PR description.

@freemvmt freemvmt requested a review from a team August 16, 2024 21:40
@freemvmt freemvmt changed the base branch from implement-verbatim-module-syntax to main September 5, 2024 15:04
@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch from 380c91c to 64b37d2 Compare September 5, 2024 15:28
@freemvmt
Copy link
Contributor Author

freemvmt commented Sep 5, 2024

Rebased PR onto #main now that it includes #3555, which makes #3528 redundant for the purpose of fixing the Jest issue (but potentially valuable in and of itself, see comment).

@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch from d67e62f to 92013f2 Compare September 5, 2024 19:58
clean up code: add comments, remove logs, improve naming etc

add exemplar Microsoft auth environment variables

add Microsoft URLs to CORS allow list
@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch from 630caed to 9f08497 Compare September 6, 2024 16:38
@freemvmt freemvmt changed the title [WIP][api] add microsoft strategy to auth module [api] add microsoft strategy to auth module Sep 6, 2024
@freemvmt freemvmt force-pushed the api-add-microsoft-sso-strategy-2 branch from 9f08497 to a918020 Compare September 6, 2024 17:04
Copy link
Contributor Author

@freemvmt freemvmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes

const customPassport = new passport.Passport();

// instantiate Microsoft OIDC client, and use it to build the related strategy
const microsoftIssuer = await Issuer.discover(MICROSOFT_OPENID_CONFIG_URL);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetches config from Microsoft's .well-known address

import * as Middleware from "./middleware.js";
import * as Controller from "./controller.js";

const router = Router();
export default (passport: Authenticator): Router => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to pass the passport instance through to the middleware for these routes, since they rely on it already being instantiated, which in turn is a blocking process, so instead export a factory function as default.

return {
client_id,
client_secret: process.env.MICROSOFT_CLIENT_SECRET!,
redirect_uris: [`${process.env.API_URL_EXT}/auth/microsoft/callback`],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to match one of the redirect URI configured in the PlanX Azure application

i.e. https://api.editor.planx.dev/auth/microsoft/callback for staging
and https://api.editor.planx.uk/auth/microsoft/callback for prod

cb(null, obj);
});
// equip passport with auth strategies early on, so we can pass it to route handlers
const passport = await getPassport();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fabled top-level await!

Copy link
Contributor Author

@freemvmt freemvmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a query requiring some typescript tekkers

api.planx.uk/server.ts Show resolved Hide resolved
@freemvmt freemvmt changed the title [api] add microsoft strategy to auth module [api] add Microsoft strategy to auth module (single sign-on) Sep 6, 2024
@DafyddLlyr DafyddLlyr mentioned this pull request Sep 9, 2024
@DafyddLlyr
Copy link
Contributor

Merging to main - will pick up Pulumi vars in a follow up PR shortly, and outstanding items from the checklist in description can follow along also.

@DafyddLlyr DafyddLlyr merged commit 2e4edc6 into main Sep 9, 2024
12 checks passed
@DafyddLlyr DafyddLlyr deleted the api-add-microsoft-sso-strategy-2 branch September 9, 2024 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants