From 024258cc121f32bfb14f61de5f94d48e416b4a99 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 8 Nov 2023 13:27:13 +0100 Subject: [PATCH] feat: set oauth origin dynamically based on host header (#427) --- .changeset/lemon-seas-search.md | 5 ++ Makefile | 19 +++---- flake.lock | 77 ++++++++++++++++++++++++++ flake.nix | 98 +++++++++++++++++++++++++++++++++ src/routes/oauth/config.ts | 14 +++++ src/routes/oauth/index.ts | 6 ++ src/routes/oauth/utils.ts | 1 - 7 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 .changeset/lemon-seas-search.md create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.changeset/lemon-seas-search.md b/.changeset/lemon-seas-search.md new file mode 100644 index 000000000..3a560a67e --- /dev/null +++ b/.changeset/lemon-seas-search.md @@ -0,0 +1,5 @@ +--- +'hasura-auth': minor +--- + +feat: set oauth origin dynamically based on host header diff --git a/Makefile b/Makefile index 994a3f8b2..fdffe39d8 100644 --- a/Makefile +++ b/Makefile @@ -23,12 +23,12 @@ get-version: ## Return version. .PHONY: dev -dev: check-port install dev-env-up ## Start development environment. +dev: check-port dev-env-up ## Start development environment. bash -c "trap 'make dev-env-down' EXIT; pnpm dev:start" .PHONY: test -test: check-port install dev-env-up ## Run end-to-end tests. +test: check-port dev-env-up ## Run end-to-end tests. pnpm test .PHONY: check-port @@ -36,7 +36,7 @@ check-port: [ -z $$(lsof -t -i tcp:$(PORT)) ] || (echo "The port $(PORT) is already in use"; exit 1;) .PHONY: docgen -docgen: check-port install dev-env-up ## Generate the openapi.json file. +docgen: check-port dev-env-up ## Generate the openapi.json file. AUTH_CLIENT_URL=https://my-app.com AUTH_LOG_LEVEL=error AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS= pnpm dev & while [ "$$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:$(PORT)/healthz)" != "200" ]; do sleep 1; done curl http://localhost:$(PORT)/openapi.json | json_pp > docs/openapi.json @@ -45,28 +45,27 @@ docgen: check-port install dev-env-up ## Generate the openapi.json file. .PHONY: watch -watch: check-port install dev-env-up ## Start tests in watch mode. +watch: check-port dev-env-up ## Start tests in watch mode. bash -c "trap 'make dev-env-down' EXIT; pnpm test:watch" .PHONY: build -build: +build: docker build -t $(IMAGE) . -.PHONY: dev-env-down +.PHONY: dev-env-down dev-env-up: ## Start required services (Hasura, Postgres, Mailhog). - docker-compose -f docker-compose.yaml up -d + docker compose -f docker-compose.yaml up -d while [ "$$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8080/healthz)" != "200" ]; do sleep 1; done @echo "Hasura is ready"; .PHONY: dev-env-down dev-env-down: ## Stop required services (Hasura, Posgres, Mailhbg). - docker-compose -f docker-compose.yaml down + docker compose -f docker-compose.yaml down .PHONY: install -install: +install: pnpm install - diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..bed703eb4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-filter": { + "locked": { + "lastModified": 1694857738, + "narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "41fd48e00c22b4ced525af521ead8792402de0ea", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1698931758, + "narHash": "sha256-pwl9xS9JFMXXR1lUP/QOqO9hiZKukEcVUU1A0DKQwi4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b644d97bda6dae837d577e28383c10aa51e5e2d2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nix-filter": "nix-filter", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..af168dd21 --- /dev/null +++ b/flake.nix @@ -0,0 +1,98 @@ +{ + description = "Nhost Hasura Auth"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nix-filter.url = "github:numtide/nix-filter"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, nix-filter }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ + (final: prev: { + nodejs = prev.nodejs-18_x; + }) + ]; + + pkgs = import nixpkgs { + inherit overlays system; + }; + + nix-src = nix-filter.lib.filter { + root = ./.; + include = [ + (nix-filter.lib.matchExt "nix") + ]; + }; + + node_modules = pkgs.stdenv.mkDerivation { + inherit version; + + pname = "node_modules"; + + nativeBuildInputs = with pkgs; [ + nodePackages.pnpm + ]; + + src = nix-filter.lib.filter { + root = ./.; + include = [ + ./package.json + ./pnpm-lock.yaml + ]; + }; + + buildPhase = '' + pnpm install + ''; + + installPhase = '' + mkdir -p $out + cp -r node_modules $out + ''; + }; + + + name = "hasura-auth"; + version = "0.0.0-dev"; + + buildInputs = [ ]; + nativeBuildInputs = with pkgs; [ + nodePackages.pnpm + ]; + in + { + checks = { + nixpkgs-fmt = pkgs.runCommand "check-nixpkgs-fmt" + { + nativeBuildInputs = with pkgs; + [ + nixpkgs-fmt + ]; + } + '' + mkdir $out + nixpkgs-fmt --check ${nix-src} + ''; + + }; + + devShells = flake-utils.lib.flattenTree rec { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + nixpkgs-fmt + gnumake + ] ++ buildInputs ++ nativeBuildInputs; + + shellHook = '' + export PATH=${node_modules}/node_modules/.bin:$PATH + rm -rf node_modules + ln -sf ${node_modules}/node_modules/ node_modules + ''; + }; + }; + } + ); +} diff --git a/src/routes/oauth/config.ts b/src/routes/oauth/config.ts index 20e30c884..51c177ef4 100644 --- a/src/routes/oauth/config.ts +++ b/src/routes/oauth/config.ts @@ -51,6 +51,7 @@ export const PROVIDERS_CONFIG: Record< response_type: 'code id_token', response_mode: 'form_post', }, + dynamic: [], }, profile: ({ jwt, profile }) => { const payload = jwt?.id_token?.payload; @@ -93,6 +94,7 @@ export const PROVIDERS_CONFIG: Record< access_url: `${azureBaseUrl}/[subdomain]/oauth2/token`, profile_url: `${azureBaseUrl}/[subdomain]/openid/userinfo`, subdomain: process.env.AUTH_PROVIDER_AZUREAD_TENANT || 'common', + dynamic: [], }, profile: ({ jwt }) => { const payload = jwt?.id_token?.payload; @@ -109,6 +111,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_BITBUCKET_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_BITBUCKET_CLIENT_SECRET, scope: ['account'], + dynamic: [], }, profile: async ({ profile, access_token }) => { const { @@ -135,6 +138,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_DISCORD_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_DISCORD_CLIENT_SECRET, scope: ['identify', 'email'], + dynamic: [], }, profile: ({ profile }) => ({ id: profile.id, @@ -152,6 +156,7 @@ export const PROVIDERS_CONFIG: Record< client_secret: process.env.AUTH_PROVIDER_FACEBOOK_CLIENT_SECRET, scope: ['email'], profile_url: 'https://graph.facebook.com/me?fields=id,name,email,picture', + dynamic: [], }, profile: ({ profile }) => ({ id: profile.id, @@ -166,6 +171,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_GITHUB_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_GITHUB_CLIENT_SECRET, scope: ['user:email'], + dynamic: [], }, profile: async ({ profile, access_token }) => { // * The email is not returned by default, so we need to make a separate request @@ -191,6 +197,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_GITLAB_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_GITLAB_CLIENT_SECRET, scope: ['read_user'], + dynamic: [], }, profile: ({ profile }) => ({ id: profile.id && String(profile.id), @@ -209,6 +216,7 @@ export const PROVIDERS_CONFIG: Record< prompt: 'consent', access_type: 'offline', }, + dynamic: [], }, profile: ({ profile: { sub, name, picture, email, email_verified, locale }, @@ -229,6 +237,7 @@ export const PROVIDERS_CONFIG: Record< scope: ['r_emailaddress', 'r_liteprofile'], profile_url: 'https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~:playableStreams))', + dynamic: [], }, profile: async ({ profile, access_token }) => { const { @@ -277,6 +286,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_SPOTIFY_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_SPOTIFY_CLIENT_SECRET, scope: ['user-read-email', 'user-read-private'], + dynamic: [], }, profile: ({ profile }) => ({ id: profile.id, @@ -291,6 +301,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_STRAVA_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_STRAVA_CLIENT_SECRET, scope: ['profile:read_all'], + dynamic: [], }, // ! It is not possible to get the user's email address from Strava profile: ({ profile }) => { @@ -307,6 +318,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_TWITCH_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_TWITCH_CLIENT_SECRET, scope: ['user:read:email'], + dynamic: [], }, profile: ({ profile: { data } }) => { if (!Array.isArray(data)) { @@ -336,6 +348,7 @@ export const PROVIDERS_CONFIG: Record< response: ['tokens', 'profile', 'raw'], profile_url: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', + dynamic: [], }, profile: ({ profile }) => ({ id: profile.id_str || (profile.id && String(profile.id)), @@ -355,6 +368,7 @@ export const PROVIDERS_CONFIG: Record< client_id: process.env.AUTH_PROVIDER_WINDOWS_LIVE_CLIENT_ID, client_secret: process.env.AUTH_PROVIDER_WINDOWS_LIVE_CLIENT_SECRET, scope: ['wl.basic', 'wl.emails'], + dynamic: [], }, profile: ({ profile }) => ({ // ? Could be improved in fetching the user's profile picture - but the apis.live.net/v5.0 API is deprecated diff --git a/src/routes/oauth/index.ts b/src/routes/oauth/index.ts index 5c77a52d2..7a8fa6753 100644 --- a/src/routes/oauth/index.ts +++ b/src/routes/oauth/index.ts @@ -147,6 +147,12 @@ export const oauthProviders = Router() * Grant middleware: handle the oauth flow until the callback * @see {@link file://./config/index.ts} */ + .use((req, res, next) => { + res.locals.grant = {dynamic: { + origin: `${req.protocol}://${req.headers.host}`}, + }; + next(); + }) .use(grant.express(grantConfig)) /** diff --git a/src/routes/oauth/utils.ts b/src/routes/oauth/utils.ts index 36eebdebc..2852d7e02 100644 --- a/src/routes/oauth/utils.ts +++ b/src/routes/oauth/utils.ts @@ -138,7 +138,6 @@ export const createGrantConfig = (): GrantConfig => }, { defaults: { - origin: ENV.AUTH_SERVER_URL, prefix: `${ENV.AUTH_API_PREFIX}${OAUTH_ROUTE}`, transport: 'session', scope: ['email', 'profile'],