├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── [...stack] │ ├── layout.tsx │ └── page.tsx ├── globals.css ├── layout.tsx ├── loading.tsx ├── me │ └── route.tsx └── page.tsx ├── assets └── sign-in.png ├── express-example-server ├── .gitignore ├── package.json ├── pnpm-lock.yaml └── server.js ├── main.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── stack.tsx ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | /dist -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json pnpm-lock.yaml ./ 6 | COPY app ./app 7 | COPY stack.tsx ./ 8 | COPY next.config.mjs ./ 9 | COPY postcss.config.mjs ./ 10 | COPY tailwind.config.ts ./ 11 | COPY tsconfig.json ./ 12 | COPY main.ts ./ 13 | 14 | ENV NEXT_PUBLIC_SUPPRESS_HTTPS_COOKIE_ERROR=true 15 | 16 | RUN npm install -g pnpm 17 | RUN pnpm install 18 | RUN pnpm run build 19 | 20 | ARG NEXT_PUBLIC_STACK_PROJECT_ID 21 | ARG NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY 22 | ARG STACK_SECRET_SERVER_KEY 23 | ARG SERVER_PORT 24 | ARG PROXY_PORT 25 | 26 | ENV NEXT_PUBLIC_STACK_PROJECT_ID=${NEXT_PUBLIC_STACK_PROJECT_ID} 27 | ENV NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY} 28 | ENV STACK_SECRET_SERVER_KEY=${STACK_SECRET_SERVER_KEY} 29 | ENV SERVER_PORT=${SERVER_PORT} 30 | ENV SERVER_HOST="host.docker.internal" 31 | ENV PROXY_PORT=${PROXY_PORT} 32 | ENV PROXY_HOST="localhost" 33 | 34 | EXPOSE ${PROXY_PORT} 35 | 36 | ENTRYPOINT ["node", "dist/main.cjs"] 37 | CMD [] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stack Auth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auth-proxy 2 | 3 | auth-proxy is a simple one-command proxy that authenticates your HTTP requests and redirects to a pre-built sign-in page if a user is not authenticated. 4 | 5 | ## Setup 6 | 7 | First, sign in and create your API keys on [Stack Auth cloud](https://stack-auth.com/) (or you can [host it locally](https://github.com/stack-auth/stack)). Stack Auth is the open-source Auth0 alternative. 8 | 9 | Then, start your application server on port `3000` and run the proxy with the following command: 10 | 11 | ```sh 12 | docker run \ 13 | -e NEXT_PUBLIC_STACK_PROJECT_ID= \ 14 | -e NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= \ 15 | -e STACK_SECRET_SERVER_KEY= \ 16 | -e SERVER_PORT=3000 \ 17 | -e PROXY_PORT=3001 \ 18 | -p 3001:3001 \ 19 | stackauth/auth-proxy:latest 20 | ``` 21 | 22 | You can now access your application server at [localhost:3001](http://localhost:3001) and all the routes under the protected patterns will only be accessible by authenticated users. 23 | 24 | The protected patterns are URL patterns (check out the syntax [here](https://github.com/snd/url-pattern)). Don't forget to include the leading `/` and handling the trailing `/`. For example, if you want to protect everything under `/a` and only the route `/b` (not `/b/c`), you can run 25 | 26 | `... stackauth/auth-proxy:latest "/a(/*)" "/b(/)"`. 27 | 28 |
29 | If you don't have an application server, you can run our example server to play around with the proxy 30 | 31 | Start the example server on port 3000: 32 | ```sh 33 | git clone git@github.com:stack-auth/auth-proxy.git 34 | cd express-example-server 35 | npm install 36 | npm run dev 37 | ``` 38 | 39 | You can check out the original server without the proxy at [localhost:3000](http://localhost:3000). 40 | 41 | Now, open a new terminal and run the proxy server on port 3000: 42 | 43 | ```sh 44 | docker run \ 45 | -e NEXT_PUBLIC_STACK_PROJECT_ID= \ 46 | -e NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= \ 47 | -e STACK_SECRET_SERVER_KEY= \ 48 | -e SERVER_PORT=3000 \ 49 | -e PROXY_PORT=3001 \ 50 | -p 3001:3001 \ 51 | stackauth/auth-proxy:latest "/protected(/*)" 52 | ``` 53 | 54 | You can explore the proxy at [localhost:3001](http://localhost:3001). 55 |
56 | 57 | ## Features 58 | 59 | If you access a protected page through the proxy without being authenticated, you will be redirected to a sign-in page like this (customizable on the dashboard): 60 | 61 |
62 | Stack Setup 63 |
64 | 65 | After signing in, you will be able to access the protected pages. 66 | 67 | To retrieve user information from your webpage, you can read the headers as shown in this JavaScript Express + handlebars example (works similarly on other languages/frameworks): 68 | 69 | ```js 70 | const express = require("express"); 71 | const handlebars = require("handlebars"); 72 | 73 | const app = express(); 74 | 75 | const template = handlebars.compile(` 76 |
77 | {{#if authenticated}} 78 |

Name: {{displayName}}

79 |

Account Settings

80 | {{else}} 81 |

Sign In

82 | {{/if}} 83 |
84 | `); 85 | 86 | app.get('/', (req, res) => { 87 | const authenticated = !!req.headers['x-stack-authenticated']; 88 | const displayName = req.headers['x-stack-user-display-name'] || ''; 89 | const html = template({ authenticated, displayName }); 90 | res.send(html); 91 | }); 92 | 93 | app.listen(3000); 94 | ``` 95 | 96 | Available headers: 97 | 98 | - `x-stack-authenticated`: "true" if authenticated; not present otherwise. 99 | - `x-stack-user-id` 100 | - `x-stack-user-primary-email` 101 | - `x-stack-user-display-name` 102 | 103 | Available URLs (redirect your app server to these URLs as needed): 104 | 105 | - `/handler/sign-in` 106 | - `/handler/sign-up` 107 | - `/handler/sign-out`: Clears cookies and redirects back to your homepage. 108 | - `/handler/account-settings`: Users can update their email, display name, password, etc. 109 | - `/handler/me`: If you make a request to this URL with the user's session cookie, you will get the client information in JSON. This is useful for client-side apps. 110 | 111 | ## How It Works 112 | 113 | When a request is received, the logic is as follows: 114 | 115 | ``` 116 | if url is /handler/*: 117 | render the auth pages 118 | else: 119 | if user is not authenticated && url is protected: 120 | redirect to /handler/sign-in 121 | else: 122 | forward the request to your server with user info headers 123 | ``` 124 | 125 | ```mermaid 126 | graph TB 127 | Client((Request)) 128 | Proxy[Auth Proxy] 129 | YourServer[Your Server] 130 | StackAuthServer[Stack Auth Server] 131 | 132 | Client --> Proxy 133 | Proxy --> |"add user info headers"| YourServer 134 | Proxy --> StackAuthServer 135 | StackAuthServer --> Proxy 136 | 137 | classDef container fill:#1168bd,stroke:#0b4884,color:#ffffff 138 | class StackAuthServer container 139 | class YourServer container 140 | class Proxy container 141 | ``` 142 | -------------------------------------------------------------------------------- /app/[...stack]/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 |
7 | 11 | Back 12 | 13 |
14 |
{children}
15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /app/[...stack]/page.tsx: -------------------------------------------------------------------------------- 1 | import { StackHandler } from "@stackframe/stack"; 2 | import { stackServerApp } from "../../stack"; 3 | 4 | export default function Handler(props: any) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @media (prefers-color-scheme: dark) { 6 | :root { 7 | color-scheme: light; 8 | } 9 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { StackProvider, StackTheme } from "@stackframe/stack"; 3 | import { stackServerApp } from "../stack"; 4 | import { Inter } from "next/font/google"; 5 | import "./globals.css"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | // Stack uses React Suspense, which will render this page while user data is being fetched. 3 | // See: https://nextjs.org/docs/app/api-reference/file-conventions/loading 4 | return <>; 5 | } 6 | -------------------------------------------------------------------------------- /app/me/route.tsx: -------------------------------------------------------------------------------- 1 | import { stackServerApp } from "@/stack"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export const GET = async (req: NextRequest) => { 5 | const user = await stackServerApp.getUser(); 6 | 7 | if (!user) { 8 | return NextResponse.json( 9 | { user: null, authenticated: false }, 10 | { status: 401 } 11 | ); 12 | } else { 13 | return NextResponse.json( 14 | { 15 | user: { 16 | id: user.id, 17 | primary_email: user.primaryEmail, 18 | display_name: user.displayName, 19 | }, 20 | authenticated: true, 21 | }, 22 | { status: 200 } 23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return "hello world" 3 | } -------------------------------------------------------------------------------- /assets/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/auth-proxy/c975c20c0d9972198ea15d25e47447bbf0ce3b1b/assets/sign-in.png -------------------------------------------------------------------------------- /express-example-server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | /dist -------------------------------------------------------------------------------- /express-example-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stackframe/proxied-server-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "express": "^4.19.2", 7 | "handlebars": "^4.7.8" 8 | }, 9 | "scripts": { 10 | "dev": "PORT=3000 node server.js" 11 | }, 12 | "bin": { 13 | "proxied-server-example": "./server.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /express-example-server/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | express: 12 | specifier: ^4.19.2 13 | version: 4.21.0 14 | handlebars: 15 | specifier: ^4.7.8 16 | version: 4.7.8 17 | 18 | packages: 19 | 20 | accepts@1.3.8: 21 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 22 | engines: {node: '>= 0.6'} 23 | 24 | array-flatten@1.1.1: 25 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 26 | 27 | body-parser@1.20.3: 28 | resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} 29 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 30 | 31 | bytes@3.1.2: 32 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 33 | engines: {node: '>= 0.8'} 34 | 35 | call-bind@1.0.7: 36 | resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} 37 | engines: {node: '>= 0.4'} 38 | 39 | content-disposition@0.5.4: 40 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 41 | engines: {node: '>= 0.6'} 42 | 43 | content-type@1.0.5: 44 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 45 | engines: {node: '>= 0.6'} 46 | 47 | cookie-signature@1.0.6: 48 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 49 | 50 | cookie@0.6.0: 51 | resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 52 | engines: {node: '>= 0.6'} 53 | 54 | debug@2.6.9: 55 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 56 | peerDependencies: 57 | supports-color: '*' 58 | peerDependenciesMeta: 59 | supports-color: 60 | optional: true 61 | 62 | define-data-property@1.1.4: 63 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 64 | engines: {node: '>= 0.4'} 65 | 66 | depd@2.0.0: 67 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 68 | engines: {node: '>= 0.8'} 69 | 70 | destroy@1.2.0: 71 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 72 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 73 | 74 | ee-first@1.1.1: 75 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 76 | 77 | encodeurl@1.0.2: 78 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 79 | engines: {node: '>= 0.8'} 80 | 81 | encodeurl@2.0.0: 82 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 83 | engines: {node: '>= 0.8'} 84 | 85 | es-define-property@1.0.0: 86 | resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} 87 | engines: {node: '>= 0.4'} 88 | 89 | es-errors@1.3.0: 90 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 91 | engines: {node: '>= 0.4'} 92 | 93 | escape-html@1.0.3: 94 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 95 | 96 | etag@1.8.1: 97 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 98 | engines: {node: '>= 0.6'} 99 | 100 | express@4.21.0: 101 | resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==} 102 | engines: {node: '>= 0.10.0'} 103 | 104 | finalhandler@1.3.1: 105 | resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} 106 | engines: {node: '>= 0.8'} 107 | 108 | forwarded@0.2.0: 109 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 110 | engines: {node: '>= 0.6'} 111 | 112 | fresh@0.5.2: 113 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 114 | engines: {node: '>= 0.6'} 115 | 116 | function-bind@1.1.2: 117 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 118 | 119 | get-intrinsic@1.2.4: 120 | resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 121 | engines: {node: '>= 0.4'} 122 | 123 | gopd@1.0.1: 124 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 125 | 126 | handlebars@4.7.8: 127 | resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} 128 | engines: {node: '>=0.4.7'} 129 | hasBin: true 130 | 131 | has-property-descriptors@1.0.2: 132 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 133 | 134 | has-proto@1.0.3: 135 | resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} 136 | engines: {node: '>= 0.4'} 137 | 138 | has-symbols@1.0.3: 139 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 140 | engines: {node: '>= 0.4'} 141 | 142 | hasown@2.0.2: 143 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 144 | engines: {node: '>= 0.4'} 145 | 146 | http-errors@2.0.0: 147 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 148 | engines: {node: '>= 0.8'} 149 | 150 | iconv-lite@0.4.24: 151 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 152 | engines: {node: '>=0.10.0'} 153 | 154 | inherits@2.0.4: 155 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 156 | 157 | ipaddr.js@1.9.1: 158 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 159 | engines: {node: '>= 0.10'} 160 | 161 | media-typer@0.3.0: 162 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 163 | engines: {node: '>= 0.6'} 164 | 165 | merge-descriptors@1.0.3: 166 | resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} 167 | 168 | methods@1.1.2: 169 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 170 | engines: {node: '>= 0.6'} 171 | 172 | mime-db@1.52.0: 173 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 174 | engines: {node: '>= 0.6'} 175 | 176 | mime-types@2.1.35: 177 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 178 | engines: {node: '>= 0.6'} 179 | 180 | mime@1.6.0: 181 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 182 | engines: {node: '>=4'} 183 | hasBin: true 184 | 185 | minimist@1.2.8: 186 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 187 | 188 | ms@2.0.0: 189 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 190 | 191 | ms@2.1.3: 192 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 193 | 194 | negotiator@0.6.3: 195 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 196 | engines: {node: '>= 0.6'} 197 | 198 | neo-async@2.6.2: 199 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 200 | 201 | object-inspect@1.13.2: 202 | resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} 203 | engines: {node: '>= 0.4'} 204 | 205 | on-finished@2.4.1: 206 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 207 | engines: {node: '>= 0.8'} 208 | 209 | parseurl@1.3.3: 210 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 211 | engines: {node: '>= 0.8'} 212 | 213 | path-to-regexp@0.1.10: 214 | resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} 215 | 216 | proxy-addr@2.0.7: 217 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 218 | engines: {node: '>= 0.10'} 219 | 220 | qs@6.13.0: 221 | resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} 222 | engines: {node: '>=0.6'} 223 | 224 | range-parser@1.2.1: 225 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 226 | engines: {node: '>= 0.6'} 227 | 228 | raw-body@2.5.2: 229 | resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} 230 | engines: {node: '>= 0.8'} 231 | 232 | safe-buffer@5.2.1: 233 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 234 | 235 | safer-buffer@2.1.2: 236 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 237 | 238 | send@0.19.0: 239 | resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} 240 | engines: {node: '>= 0.8.0'} 241 | 242 | serve-static@1.16.2: 243 | resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} 244 | engines: {node: '>= 0.8.0'} 245 | 246 | set-function-length@1.2.2: 247 | resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} 248 | engines: {node: '>= 0.4'} 249 | 250 | setprototypeof@1.2.0: 251 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 252 | 253 | side-channel@1.0.6: 254 | resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} 255 | engines: {node: '>= 0.4'} 256 | 257 | source-map@0.6.1: 258 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 259 | engines: {node: '>=0.10.0'} 260 | 261 | statuses@2.0.1: 262 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 263 | engines: {node: '>= 0.8'} 264 | 265 | toidentifier@1.0.1: 266 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 267 | engines: {node: '>=0.6'} 268 | 269 | type-is@1.6.18: 270 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 271 | engines: {node: '>= 0.6'} 272 | 273 | uglify-js@3.19.3: 274 | resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} 275 | engines: {node: '>=0.8.0'} 276 | hasBin: true 277 | 278 | unpipe@1.0.0: 279 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 280 | engines: {node: '>= 0.8'} 281 | 282 | utils-merge@1.0.1: 283 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 284 | engines: {node: '>= 0.4.0'} 285 | 286 | vary@1.1.2: 287 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 288 | engines: {node: '>= 0.8'} 289 | 290 | wordwrap@1.0.0: 291 | resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} 292 | 293 | snapshots: 294 | 295 | accepts@1.3.8: 296 | dependencies: 297 | mime-types: 2.1.35 298 | negotiator: 0.6.3 299 | 300 | array-flatten@1.1.1: {} 301 | 302 | body-parser@1.20.3: 303 | dependencies: 304 | bytes: 3.1.2 305 | content-type: 1.0.5 306 | debug: 2.6.9 307 | depd: 2.0.0 308 | destroy: 1.2.0 309 | http-errors: 2.0.0 310 | iconv-lite: 0.4.24 311 | on-finished: 2.4.1 312 | qs: 6.13.0 313 | raw-body: 2.5.2 314 | type-is: 1.6.18 315 | unpipe: 1.0.0 316 | transitivePeerDependencies: 317 | - supports-color 318 | 319 | bytes@3.1.2: {} 320 | 321 | call-bind@1.0.7: 322 | dependencies: 323 | es-define-property: 1.0.0 324 | es-errors: 1.3.0 325 | function-bind: 1.1.2 326 | get-intrinsic: 1.2.4 327 | set-function-length: 1.2.2 328 | 329 | content-disposition@0.5.4: 330 | dependencies: 331 | safe-buffer: 5.2.1 332 | 333 | content-type@1.0.5: {} 334 | 335 | cookie-signature@1.0.6: {} 336 | 337 | cookie@0.6.0: {} 338 | 339 | debug@2.6.9: 340 | dependencies: 341 | ms: 2.0.0 342 | 343 | define-data-property@1.1.4: 344 | dependencies: 345 | es-define-property: 1.0.0 346 | es-errors: 1.3.0 347 | gopd: 1.0.1 348 | 349 | depd@2.0.0: {} 350 | 351 | destroy@1.2.0: {} 352 | 353 | ee-first@1.1.1: {} 354 | 355 | encodeurl@1.0.2: {} 356 | 357 | encodeurl@2.0.0: {} 358 | 359 | es-define-property@1.0.0: 360 | dependencies: 361 | get-intrinsic: 1.2.4 362 | 363 | es-errors@1.3.0: {} 364 | 365 | escape-html@1.0.3: {} 366 | 367 | etag@1.8.1: {} 368 | 369 | express@4.21.0: 370 | dependencies: 371 | accepts: 1.3.8 372 | array-flatten: 1.1.1 373 | body-parser: 1.20.3 374 | content-disposition: 0.5.4 375 | content-type: 1.0.5 376 | cookie: 0.6.0 377 | cookie-signature: 1.0.6 378 | debug: 2.6.9 379 | depd: 2.0.0 380 | encodeurl: 2.0.0 381 | escape-html: 1.0.3 382 | etag: 1.8.1 383 | finalhandler: 1.3.1 384 | fresh: 0.5.2 385 | http-errors: 2.0.0 386 | merge-descriptors: 1.0.3 387 | methods: 1.1.2 388 | on-finished: 2.4.1 389 | parseurl: 1.3.3 390 | path-to-regexp: 0.1.10 391 | proxy-addr: 2.0.7 392 | qs: 6.13.0 393 | range-parser: 1.2.1 394 | safe-buffer: 5.2.1 395 | send: 0.19.0 396 | serve-static: 1.16.2 397 | setprototypeof: 1.2.0 398 | statuses: 2.0.1 399 | type-is: 1.6.18 400 | utils-merge: 1.0.1 401 | vary: 1.1.2 402 | transitivePeerDependencies: 403 | - supports-color 404 | 405 | finalhandler@1.3.1: 406 | dependencies: 407 | debug: 2.6.9 408 | encodeurl: 2.0.0 409 | escape-html: 1.0.3 410 | on-finished: 2.4.1 411 | parseurl: 1.3.3 412 | statuses: 2.0.1 413 | unpipe: 1.0.0 414 | transitivePeerDependencies: 415 | - supports-color 416 | 417 | forwarded@0.2.0: {} 418 | 419 | fresh@0.5.2: {} 420 | 421 | function-bind@1.1.2: {} 422 | 423 | get-intrinsic@1.2.4: 424 | dependencies: 425 | es-errors: 1.3.0 426 | function-bind: 1.1.2 427 | has-proto: 1.0.3 428 | has-symbols: 1.0.3 429 | hasown: 2.0.2 430 | 431 | gopd@1.0.1: 432 | dependencies: 433 | get-intrinsic: 1.2.4 434 | 435 | handlebars@4.7.8: 436 | dependencies: 437 | minimist: 1.2.8 438 | neo-async: 2.6.2 439 | source-map: 0.6.1 440 | wordwrap: 1.0.0 441 | optionalDependencies: 442 | uglify-js: 3.19.3 443 | 444 | has-property-descriptors@1.0.2: 445 | dependencies: 446 | es-define-property: 1.0.0 447 | 448 | has-proto@1.0.3: {} 449 | 450 | has-symbols@1.0.3: {} 451 | 452 | hasown@2.0.2: 453 | dependencies: 454 | function-bind: 1.1.2 455 | 456 | http-errors@2.0.0: 457 | dependencies: 458 | depd: 2.0.0 459 | inherits: 2.0.4 460 | setprototypeof: 1.2.0 461 | statuses: 2.0.1 462 | toidentifier: 1.0.1 463 | 464 | iconv-lite@0.4.24: 465 | dependencies: 466 | safer-buffer: 2.1.2 467 | 468 | inherits@2.0.4: {} 469 | 470 | ipaddr.js@1.9.1: {} 471 | 472 | media-typer@0.3.0: {} 473 | 474 | merge-descriptors@1.0.3: {} 475 | 476 | methods@1.1.2: {} 477 | 478 | mime-db@1.52.0: {} 479 | 480 | mime-types@2.1.35: 481 | dependencies: 482 | mime-db: 1.52.0 483 | 484 | mime@1.6.0: {} 485 | 486 | minimist@1.2.8: {} 487 | 488 | ms@2.0.0: {} 489 | 490 | ms@2.1.3: {} 491 | 492 | negotiator@0.6.3: {} 493 | 494 | neo-async@2.6.2: {} 495 | 496 | object-inspect@1.13.2: {} 497 | 498 | on-finished@2.4.1: 499 | dependencies: 500 | ee-first: 1.1.1 501 | 502 | parseurl@1.3.3: {} 503 | 504 | path-to-regexp@0.1.10: {} 505 | 506 | proxy-addr@2.0.7: 507 | dependencies: 508 | forwarded: 0.2.0 509 | ipaddr.js: 1.9.1 510 | 511 | qs@6.13.0: 512 | dependencies: 513 | side-channel: 1.0.6 514 | 515 | range-parser@1.2.1: {} 516 | 517 | raw-body@2.5.2: 518 | dependencies: 519 | bytes: 3.1.2 520 | http-errors: 2.0.0 521 | iconv-lite: 0.4.24 522 | unpipe: 1.0.0 523 | 524 | safe-buffer@5.2.1: {} 525 | 526 | safer-buffer@2.1.2: {} 527 | 528 | send@0.19.0: 529 | dependencies: 530 | debug: 2.6.9 531 | depd: 2.0.0 532 | destroy: 1.2.0 533 | encodeurl: 1.0.2 534 | escape-html: 1.0.3 535 | etag: 1.8.1 536 | fresh: 0.5.2 537 | http-errors: 2.0.0 538 | mime: 1.6.0 539 | ms: 2.1.3 540 | on-finished: 2.4.1 541 | range-parser: 1.2.1 542 | statuses: 2.0.1 543 | transitivePeerDependencies: 544 | - supports-color 545 | 546 | serve-static@1.16.2: 547 | dependencies: 548 | encodeurl: 2.0.0 549 | escape-html: 1.0.3 550 | parseurl: 1.3.3 551 | send: 0.19.0 552 | transitivePeerDependencies: 553 | - supports-color 554 | 555 | set-function-length@1.2.2: 556 | dependencies: 557 | define-data-property: 1.1.4 558 | es-errors: 1.3.0 559 | function-bind: 1.1.2 560 | get-intrinsic: 1.2.4 561 | gopd: 1.0.1 562 | has-property-descriptors: 1.0.2 563 | 564 | setprototypeof@1.2.0: {} 565 | 566 | side-channel@1.0.6: 567 | dependencies: 568 | call-bind: 1.0.7 569 | es-errors: 1.3.0 570 | get-intrinsic: 1.2.4 571 | object-inspect: 1.13.2 572 | 573 | source-map@0.6.1: {} 574 | 575 | statuses@2.0.1: {} 576 | 577 | toidentifier@1.0.1: {} 578 | 579 | type-is@1.6.18: 580 | dependencies: 581 | media-typer: 0.3.0 582 | mime-types: 2.1.35 583 | 584 | uglify-js@3.19.3: 585 | optional: true 586 | 587 | unpipe@1.0.0: {} 588 | 589 | utils-merge@1.0.1: {} 590 | 591 | vary@1.1.2: {} 592 | 593 | wordwrap@1.0.0: {} 594 | -------------------------------------------------------------------------------- /express-example-server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const handlebars = require("handlebars"); 3 | 4 | const app = express(); 5 | const PORT = process.env.PORT || 3000; 6 | 7 | const authenticatedTemplateSource = ` 8 |
9 |

Authenticated: Yes

10 |

Display Name: {{displayName}}

11 |

Account Settings

12 |

Go to protected page

13 |

Sign Out

14 |
15 | `; 16 | 17 | const unauthenticatedTemplateSource = ` 18 |
19 |

Authenticated: No

20 |

Go to protected page

21 |

Sign In

22 |
23 | `; 24 | 25 | const authenticatedTemplate = handlebars.compile(authenticatedTemplateSource); 26 | const unauthenticatedTemplate = handlebars.compile( 27 | unauthenticatedTemplateSource 28 | ); 29 | 30 | app.get("/", (req, res) => { 31 | const authenticated = !!req.headers["x-stack-authenticated"]; 32 | const displayName = req.headers["x-stack-user-display-name"] || ""; 33 | 34 | let html; 35 | if (authenticated) { 36 | html = authenticatedTemplate({ displayName }); 37 | } else { 38 | html = unauthenticatedTemplate(); 39 | } 40 | 41 | res.send(html); 42 | }); 43 | 44 | app.get("/protected", (req, res) => { 45 | const protectedTemplate = handlebars.compile( 46 | "

This is a protected page, only authenticated users can access it

" 47 | ); 48 | res.send(protectedTemplate()); 49 | }); 50 | 51 | app.listen(PORT, () => { 52 | console.log(`Server is running on http://localhost:${PORT}`); 53 | }); 54 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from 'commander'; 4 | import { createServer, request as httpRequest } from "http"; 5 | import httpProxy from "http-proxy"; 6 | import UrlPattern from 'url-pattern'; 7 | import next from "next"; 8 | import { parse } from "url"; 9 | import path from "path"; 10 | 11 | // Check for required environment variables 12 | const requiredEnvVars = [ 13 | 'NEXT_PUBLIC_STACK_PROJECT_ID', 14 | 'NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY', 15 | 'STACK_SECRET_SERVER_KEY', 16 | 'SERVER_PORT', 17 | 'PROXY_PORT', 18 | ]; 19 | for (const envVar of requiredEnvVars) { 20 | if (!process.env[envVar]) { 21 | console.error(`Environment variable ${envVar} is required. Go to https://app.stack-auth.com and create your API keys to continue.`); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | const proxyPort = parseInt(process.env.PROXY_PORT!); 27 | const proxyHost = process.env.PROXY_HOST || 'localhost'; 28 | const serverPort = parseInt(process.env.SERVER_PORT!); 29 | const serverHost = process.env.SERVER_HOST || 'localhost'; 30 | 31 | const program = new Command(); 32 | program 33 | .description('Auth Proxy\nA simple proxy that authenticates http requests and provide sign-in interface to your app\nAll the routes except /handler/* are forwarded to the server with the user info headers') 34 | .argument('[protectedPattern...]', 'The protected URL patterns (glob syntax)'); 35 | 36 | program.parse(process.argv); 37 | 38 | if (program.args.length === 0) { 39 | console.error('Error: at least one protected pattern is required'); 40 | process.exit(1); 41 | } 42 | 43 | const dev = process.env.NODE_ENV === "development"; 44 | 45 | const dir = path.resolve(__dirname, ".."); 46 | process.chdir(dir); 47 | const app = next({ dev }); 48 | const handle = app.getRequestHandler(); 49 | 50 | function throwErr(message: string): never { 51 | throw new Error(message); 52 | } 53 | 54 | app.prepare().then(() => { 55 | createServer((req, res) => { 56 | let parsedUrl = parse(req.url!, true); 57 | 58 | if (new UrlPattern('/handler(/*)').match(parsedUrl.pathname || throwErr("parsedUrl.pathname is undefined"))) { 59 | // This is a hack for the account-setting + next.js basePath incompatibility, should be fixed later in the stack package 60 | if (req.url?.includes("/handler/handler")) { 61 | parsedUrl = parse(req.url.replace("/handler/handler", "/handler"), true); 62 | } 63 | 64 | handle(req, res, parsedUrl); 65 | } else { 66 | const options = { 67 | hostname: proxyHost, 68 | port: proxyPort, 69 | path: "/handler/me", 70 | method: "GET", 71 | headers: req.headers, 72 | }; 73 | 74 | const meReq = httpRequest(options, (meRes) => { 75 | let data = ""; 76 | meRes.on("data", (chunk) => { 77 | data += chunk; 78 | }); 79 | 80 | meRes.on("end", () => { 81 | let userInfo; 82 | try { 83 | userInfo = JSON.parse(data); 84 | } catch (e) { 85 | console.error(e); 86 | res.statusCode = 500; 87 | res.end("Internal server error"); 88 | return; 89 | } 90 | 91 | const proxy = httpProxy.createProxyServer({}); 92 | 93 | proxy.on("proxyReq", (proxyReq, req, res) => { 94 | const allHeaders = [ 95 | "x-stack-authenticated", 96 | "x-stack-user-id", 97 | "x-stack-user-primary-email", 98 | "x-stack-user-display-name", 99 | ]; 100 | 101 | for (const header of allHeaders) { 102 | proxyReq.removeHeader(header); 103 | } 104 | 105 | if (userInfo.authenticated) { 106 | proxyReq.setHeader("x-stack-authenticated", "true"); 107 | proxyReq.setHeader("x-stack-user-id", userInfo.user.id); 108 | proxyReq.setHeader("x-stack-user-primary-email", userInfo.user.primary_email); 109 | proxyReq.setHeader("x-stack-user-display-name", userInfo.user.display_name); 110 | } else if (program.args.some((pattern: any) => new UrlPattern(pattern).match(req.url || throwErr("req.url is undefined")))) { 111 | res.statusCode = 302; 112 | res.setHeader("Location", "/handler/sign-in"); 113 | res.end(); 114 | return; 115 | } 116 | }); 117 | 118 | proxy.web(req, res, { target: `http://${serverHost}:${serverPort}` }); 119 | }); 120 | }); 121 | 122 | meReq.on("error", (err) => { 123 | res.statusCode = 500; 124 | res.end("Internal server error"); 125 | }); 126 | 127 | meReq.end(); 128 | } 129 | }).listen(parseInt(process.env.PROXY_PORT!)).on('error', (err) => { 130 | console.error(err); 131 | }); 132 | 133 | console.log(`Auth Proxy forwarding http://${serverHost}:${serverPort} to http://${proxyHost}:${proxyPort}\nProtecting ${program.args.join(' ')}`); 134 | }).catch((err) => { 135 | console.error(err); 136 | }); -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | basePath: "/handler", 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stackframe/auth-proxy", 3 | "version": "0.0.8", 4 | "type": "module", 5 | "scripts": { 6 | "build:next": "next build", 7 | "start:next": "next start", 8 | "build:tsc": "tsup ./main.ts --target node16 --format cjs --out-dir dist", 9 | "start:tsc": "node dist/main.cjs", 10 | "build": "npm run build:next && npm run build:tsc", 11 | "start": "npm run start:tsc", 12 | "lint": "next lint" 13 | }, 14 | "files": [ 15 | "dist", 16 | "app", 17 | "stack.tsx", 18 | "package.json", 19 | "next.config.mjs", 20 | "postcss.config.mjs", 21 | "tailwind.config.ts", 22 | "tsconfig.json" 23 | ], 24 | "dependencies": { 25 | "@stackframe/stack": "^2.6.7", 26 | "commander": "^12.1.0", 27 | "http-proxy": "^1.18.1", 28 | "next": "14.2.7", 29 | "react": "^18", 30 | "react-dom": "^18", 31 | "url-pattern": "^1.0.3" 32 | }, 33 | "devDependencies": { 34 | "@types/command-line-args": "^5.2.3", 35 | "@types/http-proxy": "^1.17.15", 36 | "@types/node": "^20", 37 | "@types/react": "^18", 38 | "@types/react-dom": "^18", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.2.7", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.4.1", 43 | "tsup": "^8.3.0", 44 | "tsx": "^4.7.2", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /stack.tsx: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { StackServerApp } from "@stackframe/stack"; 4 | 5 | export const stackServerApp = new StackServerApp({ 6 | tokenStore: "nextjs-cookie", 7 | // hack to make it build without env vars 8 | projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID || "project_id_placeholder", 9 | publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || "publishable_client_key_placeholder", 10 | secretServerKey: process.env.STACK_SECRET_SERVER_KEY || "secret_server_key_placeholder", 11 | urls: { 12 | handler: "/handler", 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | }, 11 | plugins: [], 12 | }; 13 | export default config; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------