├── .changeset ├── README.md ├── commit.cjs ├── config.json ├── popular-geese-reply.md ├── stupid-boats-play.md └── ten-pans-invent.md ├── .github ├── CODE_OF_CONDUCT └── workflows │ ├── docs.yml │ ├── format.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── CNAME ├── LICENSE ├── README.md ├── bun.lockb ├── bunfig.toml ├── examples ├── .gitignore ├── README.md ├── client │ ├── astro │ │ ├── .gitignore │ │ ├── .vscode │ │ │ ├── extensions.json │ │ │ └── launch.json │ │ ├── README.md │ │ ├── astro.config.mjs │ │ ├── package.json │ │ ├── public │ │ │ └── favicon.svg │ │ ├── src │ │ │ ├── assets │ │ │ │ ├── astro.svg │ │ │ │ └── background.svg │ │ │ ├── auth.ts │ │ │ ├── components │ │ │ │ └── Welcome.astro │ │ │ ├── env.d.ts │ │ │ ├── layouts │ │ │ │ └── Layout.astro │ │ │ ├── middleware.ts │ │ │ └── pages │ │ │ │ ├── callback.ts │ │ │ │ └── index.astro │ │ └── tsconfig.json │ ├── cloudflare-api │ │ ├── api.ts │ │ └── package.json │ ├── jwt-api │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── lambda-api │ │ ├── api.ts │ │ └── package.json │ ├── nextjs │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── app │ │ │ ├── actions.ts │ │ │ ├── api │ │ │ │ └── callback │ │ │ │ │ └── route.ts │ │ │ ├── auth.ts │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.module.css │ │ │ └── page.tsx │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── public │ │ │ ├── file.svg │ │ │ ├── globe.svg │ │ │ ├── next.svg │ │ │ ├── vercel.svg │ │ │ └── window.svg │ │ └── tsconfig.json │ ├── react │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ │ └── vite.svg │ │ ├── src │ │ │ ├── App.tsx │ │ │ ├── AuthContext.tsx │ │ │ ├── assets │ │ │ │ └── react.svg │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── sveltekit │ │ ├── .npmrc │ │ ├── package.json │ │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── hooks.server.ts │ │ ├── lib │ │ │ └── auth.server.ts │ │ └── routes │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── callback │ │ │ └── +server.ts │ │ ├── static │ │ └── favicon.png │ │ ├── svelte.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts ├── issuer │ ├── bun │ │ ├── .gitignore │ │ ├── issuer.ts │ │ └── package.json │ ├── cloudflare │ │ ├── issuer.ts │ │ ├── package.json │ │ ├── sst-env.d.ts │ │ └── sst.config.ts │ ├── custom-frontend │ │ ├── auth │ │ │ ├── issuer.ts │ │ │ └── package.json │ │ ├── frontend │ │ │ ├── frontend.tsx │ │ │ └── package.json │ │ └── package.json │ ├── lambda │ │ ├── issuer.ts │ │ ├── package.json │ │ ├── sst-env.d.ts │ │ └── sst.config.ts │ └── node │ │ ├── .gitignore │ │ ├── authorizer.ts │ │ └── package.json ├── quickstart │ ├── sst │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ │ ├── actions.ts │ │ │ ├── api │ │ │ │ └── callback │ │ │ │ │ └── route.ts │ │ │ ├── auth.ts │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.module.css │ │ │ └── page.tsx │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── subjects.ts │ │ ├── next.config.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── file.svg │ │ │ ├── globe.svg │ │ │ ├── next.svg │ │ │ ├── vercel.svg │ │ │ └── window.svg │ │ ├── sst-env.d.ts │ │ ├── sst.config.ts │ │ └── tsconfig.json │ └── standalone │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ ├── actions.ts │ │ ├── api │ │ │ └── callback │ │ │ │ └── route.ts │ │ ├── auth.ts │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ │ ├── auth │ │ ├── index.ts │ │ └── subjects.ts │ │ ├── bun.lockb │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ │ └── tsconfig.json ├── subjects.ts └── tsconfig.json ├── package.json ├── packages └── openauth │ ├── CHANGELOG.md │ ├── bunfig.toml │ ├── package.json │ ├── script │ └── build.ts │ ├── src │ ├── client.ts │ ├── css.d.ts │ ├── error.ts │ ├── index.ts │ ├── issuer.ts │ ├── jwt.ts │ ├── keys.ts │ ├── pkce.ts │ ├── provider │ │ ├── apple.ts │ │ ├── arctic.ts │ │ ├── code.ts │ │ ├── cognito.ts │ │ ├── discord.ts │ │ ├── facebook.ts │ │ ├── github.ts │ │ ├── google.ts │ │ ├── index.ts │ │ ├── jumpcloud.ts │ │ ├── keycloak.ts │ │ ├── linkedin.ts │ │ ├── microsoft.ts │ │ ├── oauth2.ts │ │ ├── oidc.ts │ │ ├── password.ts │ │ ├── provider.ts │ │ ├── slack.ts │ │ ├── spotify.ts │ │ ├── twitch.ts │ │ ├── x.ts │ │ └── yahoo.ts │ ├── random.ts │ ├── storage │ │ ├── aws.ts │ │ ├── cloudflare.ts │ │ ├── dynamo.ts │ │ ├── memory.ts │ │ └── storage.ts │ ├── subject.ts │ ├── ui │ │ ├── base.tsx │ │ ├── code.tsx │ │ ├── form.tsx │ │ ├── icon.tsx │ │ ├── password.tsx │ │ ├── select.tsx │ │ ├── theme.ts │ │ └── ui.css │ └── util.ts │ ├── test │ ├── client.test.ts │ ├── issuer.test.ts │ ├── scrap.test.ts │ ├── storage.test.ts │ └── util.test.ts │ └── tsconfig.json ├── scripts └── format └── www ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── astro.config.mjs ├── bun.lockb ├── config.ts ├── generate.ts ├── package.json ├── public ├── favicon-dark.svg ├── favicon.ico ├── favicon.svg └── social-share.png ├── src ├── assets │ ├── logo-dark.svg │ └── logo-light.svg ├── components │ ├── Hero.astro │ └── Lander.astro ├── content │ ├── config.ts │ └── docs │ │ ├── docs │ │ ├── client.mdx │ │ ├── index.mdx │ │ ├── issuer.mdx │ │ ├── provider │ │ │ ├── apple.mdx │ │ │ ├── code.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── facebook.mdx │ │ │ ├── github.mdx │ │ │ ├── google.mdx │ │ │ ├── jumpcloud.mdx │ │ │ ├── keycloak.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── oauth2.mdx │ │ │ ├── oidc.mdx │ │ │ ├── password.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── twitch.mdx │ │ │ ├── x.mdx │ │ │ └── yahoo.mdx │ │ ├── start │ │ │ ├── nextjs-dark.png │ │ │ ├── nextjs-light.png │ │ │ ├── sst.mdx │ │ │ └── standalone.mdx │ │ ├── storage │ │ │ ├── cloudflare.mdx │ │ │ ├── dynamo.mdx │ │ │ └── memory.mdx │ │ ├── subject.mdx │ │ ├── themes-dark.png │ │ ├── themes-light.png │ │ └── ui │ │ │ ├── code.mdx │ │ │ ├── password.mdx │ │ │ ├── select.mdx │ │ │ └── theme.mdx │ │ └── index.mdx ├── custom.css ├── env.d.ts └── styles │ └── lander.css └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/commit.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@changesets/types').CommitFunctions["getAddMessage"]} */ 2 | module.exports.getAddMessage = async (changeset) => { 3 | return changeset.summary; 4 | }; 5 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": "./commit.cjs", 5 | "fixed": [["@openauthjs/openauth"]], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["@openauthjs/example-*"] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/popular-geese-reply.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@openauthjs/openauth": patch 3 | --- 4 | 5 | update google icon to comply with branding guidelines 6 | -------------------------------------------------------------------------------- /.changeset/stupid-boats-play.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@openauthjs/openauth": patch 3 | --- 4 | 5 | allow auth style autodetection 6 | -------------------------------------------------------------------------------- /.changeset/ten-pans-invent.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@openauthjs/openauth": patch 3 | --- 4 | 5 | add linkedin adapter 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | I don't typically set up a code of conduct for our projects but given this one is security related it will draw a very specific set of problems I want to avoid. There's only two rules 4 | 5 | 1. Reporting security issues 6 | 7 | If you find a security issue please report them to me directly on [X](https://twitter.com/thdxr) or [Bluesky](https://bsky.app/). Do not open a public issue or post publicly in case the issue can be exploited. Feel free to give us a window of time to respond before disclosing it publicly - that seems fair. 8 | 9 | 2. Reporting "security" issues 10 | 11 | A lot of things that seem to fall in that first category are not really security problems, just tradeoffs that were made in the design of OpenAuth. Security products attract a lot of binary opinions like "never use X". We reject this type of thinking entirely - security is a spectrum of usability and infinitely optimizing for "security" does not yield a good product. 12 | 13 | All discussions around the tradeoffs that were made must consider this - if you disagree with a decision you MUST articulate why the decision was probably made before you argue against it. Eg. "X seem to be used because of benefit [a] and their downside [b] is mitigated by [c] BUT I do not think this is enough because of [d]" 14 | 15 | We do not tolerate wasting the maintainers time and forcing them to articulate this nuance. If something is not clear of course you can ask for clarification. 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | # Trigger the workflow every time you push to the `main` branch 5 | # Using a different branch name? Replace `main` with your branch’s name 6 | push: 7 | branches: [master] 8 | # Allows you to run this workflow manually from the Actions tab on GitHub. 9 | workflow_dispatch: 10 | 11 | # Allow this job to clone the repo and create a page deployment 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout your repository using git 22 | uses: actions/checkout@v4 23 | - name: Install, build, and upload your site 24 | uses: withastro/action@v3 25 | with: 26 | path: www 27 | 28 | deploy: 29 | needs: build 30 | runs-on: ubuntu-latest 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | steps: 35 | - name: Deploy to GitHub Pages 36 | id: deployment 37 | uses: actions/deploy-pages@v4 38 | with: 39 | path: www 40 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: format 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | format: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.head_ref }} 21 | fetch-depth: 0 22 | - uses: oven-sh/setup-bun@v2 23 | - run: | 24 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 25 | git config --local user.name "github-actions[bot]" 26 | ./scripts/format 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | concurrency: ${{ github.workflow }}-${{ github.ref }} 13 | 14 | jobs: 15 | release: 16 | name: release 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: oven-sh/setup-bun@v2 21 | - run: bun install 22 | - id: changesets 23 | uses: changesets/action@v1 24 | with: 25 | publish: bun run release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: oven-sh/setup-bun@v2 16 | with: 17 | bun-version: latest 18 | - run: bun install 19 | - run: cd packages/openauth && bun run build 20 | - run: cd packages/openauth && bun test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .sst 3 | .env 4 | dist 5 | persist.json 6 | .DS_Store 7 | notes 8 | .nvim.lua 9 | .svelte-kit -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | } 4 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | openauth.js.org 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SST 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 | 23 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | exact = true 3 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | # sst 178 | .sst 179 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | There are two sets of examples here, issuers and clients. Issuers are examples of setting up an OpenAuth server. The clients are examples of using OpenAuth in a client application and work with any of the issuer servers. 4 | 5 | The fastest way to play around is to use the bun issuer. You can bring it up with: 6 | 7 | ```shell 8 | $ bun run --hot ./issuer/bun/issuer.ts 9 | ``` 10 | 11 | You might have to install some workspace packages first, run this in the root: 12 | 13 | ```shell 14 | $ bun install 15 | $ cd packages/openauth 16 | $ bun run build 17 | ``` 18 | 19 | This will bring it up on port 3000. Then try one of the clients - for example the astro one. 20 | 21 | ``` 22 | $ cd client/astro 23 | $ bun dev 24 | ``` 25 | 26 | Now visit `http://localhost:4321` (the astro app) and experience the auth flow. 27 | 28 | Or head over to `http://localhost:3000/password/authorize` to try the password flow directly. 29 | -------------------------------------------------------------------------------- /examples/client/astro/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /examples/client/astro/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /examples/client/astro/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/client/astro/README.md: -------------------------------------------------------------------------------- 1 | # OpenAuth Astro Client 2 | 3 | The files to note are 4 | 5 | - `src/auth.ts` - creates the client that is used to interact with the auth server 6 | - `src/middleware.ts` - middleware that runs to verify access tokens, refresh them if out of date, and redirect the user to the auth server if they are not logged in 7 | - `src/pages/callback.ts` - the callback endpoint that receives the auth code and exchanges it for an access/refresh token 8 | -------------------------------------------------------------------------------- /examples/client/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from "astro/config"; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | output: "server", 7 | server: { 8 | host: "0.0.0.0", 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/client/astro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-client-astro", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "build": "astro build", 8 | "preview": "astro preview", 9 | "astro": "astro" 10 | }, 11 | "dependencies": { 12 | "@openauthjs/openauth": "workspace:*", 13 | "astro": "5.0.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/client/astro/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /examples/client/astro/src/assets/astro.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/client/astro/src/assets/background.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/client/astro/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@openauthjs/openauth/client" 2 | import type { APIContext } from "astro" 3 | export { subjects } from "../../../subjects" 4 | 5 | export const client = createClient({ 6 | clientID: "astro", 7 | issuer: "http://localhost:3000", 8 | }) 9 | 10 | export function setTokens(ctx: APIContext, access: string, refresh: string) { 11 | ctx.cookies.set("refresh_token", refresh, { 12 | httpOnly: true, 13 | sameSite: "lax", 14 | path: "/", 15 | maxAge: 34560000, 16 | }) 17 | ctx.cookies.set("access_token", access, { 18 | httpOnly: true, 19 | sameSite: "lax", 20 | path: "/", 21 | maxAge: 34560000, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /examples/client/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | import type { SubjectPayload } from "@openauthjs/openauth/subject" 2 | import { subjects } from "./auth" 3 | 4 | declare global { 5 | declare namespace App { 6 | interface Locals { 7 | subject?: SubjectPayload 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/client/astro/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Astro Basics 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /examples/client/astro/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { defineMiddleware } from "astro:middleware" 2 | import { subjects } from "../../../subjects" 3 | import { client, setTokens } from "./auth" 4 | 5 | export const onRequest = defineMiddleware(async (ctx, next) => { 6 | if (ctx.routePattern === "/callback") { 7 | return next() 8 | } 9 | 10 | try { 11 | const accessToken = ctx.cookies.get("access_token") 12 | if (accessToken) { 13 | const refreshToken = ctx.cookies.get("refresh_token") 14 | const verified = await client.verify(subjects, accessToken.value, { 15 | refresh: refreshToken?.value, 16 | }) 17 | if (!verified.err) { 18 | if (verified.tokens) 19 | setTokens(ctx, verified.tokens.access, verified.tokens.refresh) 20 | ctx.locals.subject = verified.subject 21 | return next() 22 | } 23 | } 24 | } catch (e) {} 25 | 26 | const { url } = await client.authorize( 27 | new URL(ctx.request.url).origin + "/callback", 28 | "code", 29 | ) 30 | return Response.redirect(url, 302) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/client/astro/src/pages/callback.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from "astro" 2 | import { client, setTokens } from "../auth" 3 | 4 | export const GET: APIRoute = async (ctx) => { 5 | const code = ctx.url.searchParams.get("code") 6 | try { 7 | const tokens = await client.exchange(code!, ctx.url.origin + "/callback") 8 | if (!tokens.err) { 9 | setTokens(ctx, tokens.tokens.access, tokens.tokens.refresh) 10 | } else { 11 | throw tokens.err 12 | } 13 | return ctx.redirect("/", 302) 14 | } catch (e) { 15 | return Response.json(e, { 16 | status: 400, 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/client/astro/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Welcome from '../components/Welcome.astro'; 3 | import Layout from '../layouts/Layout.astro'; 4 | 5 | // Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build 6 | // Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. 7 | --- 8 | 9 | 10 | Hello {Astro.locals.subject?.properties.id} 11 | 12 | -------------------------------------------------------------------------------- /examples/client/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | -------------------------------------------------------------------------------- /examples/client/cloudflare-api/api.ts: -------------------------------------------------------------------------------- 1 | import type { Service } from "@cloudflare/workers-types" 2 | import { createClient } from "@openauthjs/openauth/client" 3 | import { subjects } from "../../subjects" 4 | 5 | interface Env { 6 | OPENAUTH_ISSUER: string 7 | Auth: Service 8 | CloudflareAuth: Service 9 | } 10 | 11 | export default { 12 | async fetch(request: Request, env: Env) { 13 | const client = createClient({ 14 | clientID: "cloudflare-api", 15 | // enables worker to worker communication if issuer is also a worker 16 | fetch: (input, init) => env.CloudflareAuth.fetch(input, init), 17 | issuer: env.OPENAUTH_ISSUER, 18 | }) 19 | const url = new URL(request.url) 20 | const redirectURI = url.origin + "/callback" 21 | 22 | switch (url.pathname) { 23 | case "/callback": 24 | try { 25 | const code = url.searchParams.get("code")! 26 | const exchanged = await client.exchange(code, redirectURI) 27 | if (exchanged.err) throw new Error("Invalid code") 28 | const response = new Response(null, { status: 302, headers: {} }) 29 | response.headers.set("Location", url.origin) 30 | setSession( 31 | response, 32 | exchanged.tokens.access, 33 | exchanged.tokens.refresh, 34 | ) 35 | return response 36 | } catch (e: any) { 37 | return new Response(e.toString()) 38 | } 39 | case "/authorize": 40 | return Response.redirect( 41 | await client.authorize(redirectURI, "code").then((v) => v.url), 42 | 302, 43 | ) 44 | case "/": 45 | const cookies = new URLSearchParams( 46 | request.headers.get("cookie")?.replaceAll("; ", "&"), 47 | ) 48 | const verified = await client.verify( 49 | subjects, 50 | cookies.get("access_token")!, 51 | { 52 | refresh: cookies.get("refresh_token") || undefined, 53 | }, 54 | ) 55 | if (verified.err) 56 | return Response.redirect(url.origin + "/authorize", 302) 57 | const resp = Response.json(verified.subject) 58 | if (verified.tokens) 59 | setSession(resp, verified.tokens.access, verified.tokens.refresh) 60 | return resp 61 | default: 62 | return new Response("Not found", { status: 404 }) 63 | } 64 | }, 65 | } 66 | 67 | function setSession(response: Response, access: string, refresh: string) { 68 | if (access) { 69 | response.headers.append( 70 | "Set-Cookie", 71 | `access_token=${access}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, 72 | ) 73 | } 74 | if (refresh) { 75 | response.headers.append( 76 | "Set-Cookie", 77 | `refresh_token=${refresh}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2147483647`, 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/client/cloudflare-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-api", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/client/jwt-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # jwt-api 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [8b5f490] 8 | - @openauthjs/openauth@0.2.4 9 | -------------------------------------------------------------------------------- /examples/client/jwt-api/README.md: -------------------------------------------------------------------------------- 1 | # JWT API 2 | 3 | This simple API verifies the `Authorization` header using the OpenAuth client and returns the subject. 4 | 5 | Run it using. 6 | 7 | ```bash 8 | bun run --hot index.ts 9 | ``` 10 | 11 | Then visit `http://localhost:3001/` in your browser. 12 | 13 | This works with the [React Client](../react) example that makes a call to this API after the auth flow. 14 | -------------------------------------------------------------------------------- /examples/client/jwt-api/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@openauthjs/openauth/client" 2 | import { subjects } from "../../subjects" 3 | 4 | const headers = { 5 | "Access-Control-Allow-Origin": "*", 6 | "Access-Control-Allow-Headers": "*", 7 | "Access-Control-Allow-Methods": "*", 8 | } 9 | 10 | const client = createClient({ 11 | clientID: "jwt-api", 12 | issuer: "http://localhost:3000", 13 | }) 14 | 15 | const server = Bun.serve({ 16 | port: 3001, 17 | async fetch(req) { 18 | const url = new URL(req.url) 19 | 20 | if (req.method === "OPTIONS") { 21 | return new Response(null, { headers }) 22 | } 23 | 24 | if (url.pathname === "/" && req.method === "GET") { 25 | const authHeader = req.headers.get("Authorization") 26 | 27 | if (!authHeader) { 28 | return new Response("401", { headers, status: 401 }) 29 | } 30 | 31 | const token = authHeader.split(" ")[1] 32 | const verified = await client.verify(subjects, token) 33 | 34 | if (verified.err) { 35 | return new Response("401", { headers, status: 401 }) 36 | } 37 | 38 | return new Response(verified.subject.properties.id, { headers }) 39 | } 40 | 41 | return new Response("404", { status: 404 }) 42 | }, 43 | }) 44 | 45 | console.log(`Listening on ${server.url}`) 46 | -------------------------------------------------------------------------------- /examples/client/jwt-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-jwt-api", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@openauthjs/openauth": "workspace:*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/client/lambda-api/api.ts: -------------------------------------------------------------------------------- 1 | import { Context, Hono } from "hono" 2 | import { getCookie, setCookie } from "hono/cookie" 3 | import { createClient } from "@openauthjs/openauth/client" 4 | import { handle } from "hono/aws-lambda" 5 | import { subjects } from "../../subjects" 6 | 7 | const client = createClient({ 8 | clientID: "lambda-api", 9 | }) 10 | 11 | const app = new Hono() 12 | .get("/authorize", async (c) => { 13 | const origin = new URL(c.req.url).origin 14 | const { url } = await client.authorize(origin + "/callback", "code") 15 | return c.redirect(url, 302) 16 | }) 17 | .get("/callback", async (c) => { 18 | const origin = new URL(c.req.url).origin 19 | try { 20 | const code = c.req.query("code") 21 | if (!code) throw new Error("Missing code") 22 | const exchanged = await client.exchange(code, origin + "/callback") 23 | if (exchanged.err) 24 | return new Response(exchanged.err.toString(), { 25 | status: 400, 26 | }) 27 | setSession(c, exchanged.tokens.access, exchanged.tokens.refresh) 28 | return c.redirect("/", 302) 29 | } catch (e: any) { 30 | return new Response(e.toString()) 31 | } 32 | }) 33 | .get("/", async (c) => { 34 | const access = getCookie(c, "access_token") 35 | const refresh = getCookie(c, "refresh_token") 36 | try { 37 | const verified = await client.verify(subjects, access!, { 38 | refresh, 39 | }) 40 | if (verified.err) throw new Error("Invalid access token") 41 | if (verified.tokens) 42 | setSession(c, verified.tokens.access, verified.tokens.refresh) 43 | return c.json(verified.subject) 44 | } catch (e) { 45 | console.error(e) 46 | return c.redirect("/authorize", 302) 47 | } 48 | }) 49 | 50 | export const handler = handle(app) 51 | 52 | function setSession(c: Context, accessToken?: string, refreshToken?: string) { 53 | if (accessToken) { 54 | setCookie(c, "access_token", accessToken, { 55 | httpOnly: true, 56 | sameSite: "Strict", 57 | path: "/", 58 | maxAge: 34560000, 59 | }) 60 | } 61 | if (refreshToken) { 62 | setCookie(c, "refresh_token", refreshToken, { 63 | httpOnly: true, 64 | sameSite: "Strict", 65 | path: "/", 66 | maxAge: 34560000, 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/client/lambda-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-api", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/client/nextjs/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /examples/client/nextjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nextjs 2 | 3 | ## 0.1.6 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [8b5f490] 8 | - @openauthjs/openauth@0.2.4 9 | 10 | ## 0.1.5 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [80238de] 15 | - @openauthjs/openauth@0.2.3 16 | 17 | ## 0.1.4 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [6da8647] 22 | - @openauthjs/openauth@0.2.2 23 | 24 | ## 0.1.3 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [83125f1] 29 | - @openauthjs/openauth@0.2.1 30 | 31 | ## 0.1.2 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [8c3f050] 36 | - Updated dependencies [0f93def] 37 | - @openauthjs/openauth@0.2.0 38 | 39 | ## 0.1.1 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies [584728f] 44 | - Updated dependencies [41acdc2] 45 | - Updated dependencies [2aa531b] 46 | - @openauthjs/openauth@0.1.2 47 | -------------------------------------------------------------------------------- /examples/client/nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | Make sure your OpenAuth server is running at `http://localhost:3000`. 6 | 7 | Then start the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | # or 14 | pnpm dev 15 | # or 16 | bun dev 17 | ``` 18 | 19 | Open [http://localhost:3001](http://localhost:3001) with your browser and click **Login with OpenAuth** to start the auth flow. 20 | 21 | ## Files 22 | 23 | - [`app/auth.ts`](app/auth.ts): OpenAuth client and helper to set tokens in cookies. 24 | - [`app/actions.ts`](app/actions.ts): Actions to get current logged in user, and to login and logout. 25 | - [`app/api/callback/route.ts`](app/api/callback/route.ts): Callback route for OpenAuth. 26 | - [`app/page.tsx`](app/page.tsx): Shows login and logout buttons and the current user. 27 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | import { headers as getHeaders, cookies as getCookies } from "next/headers" 5 | import { client, subjects, setTokens } from "./auth" 6 | 7 | export async function auth() { 8 | const cookies = await getCookies() 9 | const accessToken = cookies.get("access_token") 10 | const refreshToken = cookies.get("refresh_token") 11 | 12 | if (!accessToken) { 13 | return false 14 | } 15 | 16 | const verified = await client.verify(subjects, accessToken.value, { 17 | refresh: refreshToken?.value, 18 | }) 19 | 20 | if (verified.err) { 21 | return false 22 | } 23 | if (verified.tokens) { 24 | await setTokens(verified.tokens.access, verified.tokens.refresh) 25 | } 26 | 27 | return verified.subject 28 | } 29 | 30 | export async function login() { 31 | const cookies = await getCookies() 32 | const accessToken = cookies.get("access_token") 33 | const refreshToken = cookies.get("refresh_token") 34 | 35 | if (accessToken) { 36 | const verified = await client.verify(subjects, accessToken.value, { 37 | refresh: refreshToken?.value, 38 | }) 39 | if (!verified.err && verified.tokens) { 40 | await setTokens(verified.tokens.access, verified.tokens.refresh) 41 | redirect("/") 42 | } 43 | } 44 | 45 | const headers = await getHeaders() 46 | const host = headers.get("host") 47 | const protocol = host?.includes("localhost") ? "http" : "https" 48 | const { url } = await client.authorize( 49 | `${protocol}://${host}/api/callback`, 50 | "code", 51 | ) 52 | redirect(url) 53 | } 54 | 55 | export async function logout() { 56 | const cookies = await getCookies() 57 | cookies.delete("access_token") 58 | cookies.delete("refresh_token") 59 | 60 | redirect("/") 61 | } 62 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/api/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { client, setTokens } from "../../auth" 2 | import { type NextRequest, NextResponse } from "next/server" 3 | 4 | export async function GET(req: NextRequest) { 5 | const url = new URL(req.url) 6 | const code = url.searchParams.get("code") 7 | const exchanged = await client.exchange(code!, `${url.origin}/api/callback`) 8 | if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 }) 9 | await setTokens(exchanged.tokens.access, exchanged.tokens.refresh) 10 | return NextResponse.redirect(`${url.origin}/`) 11 | } 12 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/auth.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@openauthjs/openauth/client" 2 | import { cookies as getCookies } from "next/headers" 3 | export { subjects } from "../../../subjects" 4 | 5 | export const client = createClient({ 6 | clientID: "nextjs", 7 | issuer: "http://localhost:3000", 8 | }) 9 | 10 | export async function setTokens(access: string, refresh: string) { 11 | const cookies = await getCookies() 12 | 13 | cookies.set({ 14 | name: "access_token", 15 | value: access, 16 | httpOnly: true, 17 | sameSite: "lax", 18 | path: "/", 19 | maxAge: 34560000, 20 | }) 21 | cookies.set({ 22 | name: "refresh_token", 23 | value: refresh, 24 | httpOnly: true, 25 | sameSite: "lax", 26 | path: "/", 27 | maxAge: 34560000, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/examples/client/nextjs/app/favicon.ico -------------------------------------------------------------------------------- /examples/client/nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | import "./globals.css" 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }) 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }) 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | } 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode 24 | }>) { 25 | return ( 26 | 27 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-family: var(--font-geist-sans); 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas button { 65 | appearance: none; 66 | background: transparent; 67 | border-radius: 128px; 68 | height: 48px; 69 | padding: 0 20px; 70 | border: none; 71 | border: 1px solid transparent; 72 | transition: 73 | background 0.2s, 74 | color 0.2s, 75 | border-color 0.2s; 76 | cursor: pointer; 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | font-size: 16px; 81 | line-height: 20px; 82 | font-weight: 500; 83 | } 84 | 85 | button.primary { 86 | background: var(--foreground); 87 | color: var(--background); 88 | gap: 8px; 89 | } 90 | 91 | button.secondary { 92 | border-color: var(--gray-alpha-200); 93 | min-width: 180px; 94 | } 95 | 96 | .footer { 97 | grid-row-start: 3; 98 | display: flex; 99 | gap: 24px; 100 | } 101 | 102 | .footer a { 103 | display: flex; 104 | align-items: center; 105 | gap: 8px; 106 | } 107 | 108 | .footer img { 109 | flex-shrink: 0; 110 | } 111 | 112 | /* Enable hover only on non-touch devices */ 113 | @media (hover: hover) and (pointer: fine) { 114 | a.primary:hover { 115 | background: var(--button-primary-hover); 116 | border-color: transparent; 117 | } 118 | 119 | a.secondary:hover { 120 | background: var(--button-secondary-hover); 121 | border-color: transparent; 122 | } 123 | 124 | .footer a:hover { 125 | text-decoration: underline; 126 | text-underline-offset: 4px; 127 | } 128 | } 129 | 130 | @media (max-width: 600px) { 131 | .page { 132 | padding: 32px; 133 | padding-bottom: 80px; 134 | } 135 | 136 | .main { 137 | align-items: center; 138 | } 139 | 140 | .main ol { 141 | text-align: center; 142 | } 143 | 144 | .ctas { 145 | flex-direction: column; 146 | } 147 | 148 | .ctas a { 149 | font-size: 14px; 150 | height: 40px; 151 | padding: 0 16px; 152 | } 153 | 154 | a.secondary { 155 | min-width: auto; 156 | } 157 | 158 | .footer { 159 | flex-wrap: wrap; 160 | align-items: center; 161 | justify-content: center; 162 | } 163 | } 164 | 165 | @media (prefers-color-scheme: dark) { 166 | .logo { 167 | filter: invert(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /examples/client/nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { auth, login, logout } from "./actions" 3 | import styles from "./page.module.css" 4 | 5 | export default async function Home() { 6 | const subject = await auth() 7 | 8 | return ( 9 |
10 |
11 | Next.js logo 19 |
    20 | {subject ? ( 21 | <> 22 |
  1. 23 | Logged in as {subject.properties.id}. 24 |
  2. 25 |
  3. 26 | And then check out app/page.tsx. 27 |
  4. 28 | 29 | ) : ( 30 | <> 31 |
  5. Login with your email and password.
  6. 32 |
  7. 33 | And then check out app/page.tsx. 34 |
  8. 35 | 36 | )} 37 |
38 | 39 |
40 | {subject ? ( 41 |
42 | 43 |
44 | ) : ( 45 |
46 | 47 |
48 | )} 49 |
50 |
51 | 95 |
96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /examples/client/nextjs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /examples/client/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-nextjs", 3 | "version": "0.1.6", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@openauthjs/openauth": "workspace:*", 13 | "next": "15.1.0", 14 | "react": "19.0.0", 15 | "react-dom": "19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "22.10.1", 19 | "@types/react": "19.0.1", 20 | "@types/react-dom": "19.0.2", 21 | "typescript": "5.6.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/client/nextjs/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/nextjs/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/nextjs/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/client/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/client/react/README.md: -------------------------------------------------------------------------------- 1 | # React SPA Auth 2 | 3 | This uses the token + pkce flow to authenticate a user. Start it using. 4 | 5 | ```bash 6 | bun run dev 7 | ``` 8 | 9 | Then visit `http://localhost:5173` in your browser. 10 | 11 | It needs the OpenAuth server running at `http://localhost:3000`. Start it from the `examples/` dir using. 12 | 13 | ```bash 14 | bun run --hot issuer/bun/issuer.ts 15 | ``` 16 | 17 | You might have to install some workspace packages first, run this in the root: 18 | 19 | ```bash 20 | $ bun install 21 | $ cd packages/openauth 22 | $ bun run build 23 | ``` 24 | 25 | And optionally a JWT API running to get the user subject on `http://localhost:3001`. Start it using. 26 | 27 | ```bash 28 | bun run --hot client/jwt-api/index.ts 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/client/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/client/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@openauthjs/openauth": "workspace:*", 14 | "react": "19.0.0", 15 | "react-dom": "19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.15.0", 19 | "@types/react": "19.0.1", 20 | "@types/react-dom": "19.0.2", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "eslint": "^9.15.0", 23 | "eslint-plugin-react-hooks": "^5.0.0", 24 | "eslint-plugin-react-refresh": "^0.4.14", 25 | "globals": "^15.12.0", 26 | "typescript": "5.6.3", 27 | "typescript-eslint": "^8.15.0", 28 | "vite": "^6.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/client/react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { useAuth } from "./AuthContext" 3 | 4 | function App() { 5 | const auth = useAuth() 6 | const [status, setStatus] = useState("") 7 | 8 | async function callApi() { 9 | const res = await fetch("http://localhost:3001/", { 10 | headers: { 11 | Authorization: `Bearer ${await auth.getToken()}`, 12 | }, 13 | }) 14 | 15 | setStatus(res.ok ? "success" : "error") 16 | } 17 | 18 | return !auth.loaded ? ( 19 |
Loading...
20 | ) : ( 21 |
22 | {auth.loggedIn ? ( 23 |
24 |

25 | Logged in 26 | {auth.userId && as {auth.userId}} 27 |

28 | {status !== "" &&

API call: {status}

} 29 | 30 | 31 |
32 | ) : ( 33 | 34 | )} 35 |
36 | ) 37 | } 38 | 39 | export default App 40 | -------------------------------------------------------------------------------- /examples/client/react/src/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useRef, 3 | useState, 4 | ReactNode, 5 | useEffect, 6 | useContext, 7 | createContext, 8 | } from "react" 9 | import { createClient } from "@openauthjs/openauth/client" 10 | 11 | const client = createClient({ 12 | clientID: "react", 13 | issuer: "http://localhost:3000", 14 | }) 15 | 16 | interface AuthContextType { 17 | userId?: string 18 | loaded: boolean 19 | loggedIn: boolean 20 | logout: () => void 21 | login: () => Promise 22 | getToken: () => Promise 23 | } 24 | 25 | const AuthContext = createContext({} as AuthContextType) 26 | 27 | export function AuthProvider({ children }: { children: ReactNode }) { 28 | const initializing = useRef(true) 29 | const [loaded, setLoaded] = useState(false) 30 | const [loggedIn, setLoggedIn] = useState(false) 31 | const token = useRef(undefined) 32 | const [userId, setUserId] = useState() 33 | 34 | useEffect(() => { 35 | const hash = new URLSearchParams(location.search.slice(1)) 36 | const code = hash.get("code") 37 | const state = hash.get("state") 38 | 39 | if (!initializing.current) { 40 | return 41 | } 42 | 43 | initializing.current = false 44 | 45 | if (code && state) { 46 | callback(code, state) 47 | return 48 | } 49 | 50 | auth() 51 | }, []) 52 | 53 | async function auth() { 54 | const token = await refreshTokens() 55 | 56 | if (token) { 57 | await user() 58 | } 59 | 60 | setLoaded(true) 61 | } 62 | 63 | async function refreshTokens() { 64 | const refresh = localStorage.getItem("refresh") 65 | if (!refresh) return 66 | const next = await client.refresh(refresh, { 67 | access: token.current, 68 | }) 69 | if (next.err) return 70 | if (!next.tokens) return token.current 71 | 72 | localStorage.setItem("refresh", next.tokens.refresh) 73 | token.current = next.tokens.access 74 | 75 | return next.tokens.access 76 | } 77 | 78 | async function getToken() { 79 | const token = await refreshTokens() 80 | 81 | if (!token) { 82 | await login() 83 | return 84 | } 85 | 86 | return token 87 | } 88 | 89 | async function login() { 90 | const { challenge, url } = await client.authorize(location.origin, "code", { 91 | pkce: true, 92 | }) 93 | sessionStorage.setItem("challenge", JSON.stringify(challenge)) 94 | location.href = url 95 | } 96 | 97 | async function callback(code: string, state: string) { 98 | const challenge = JSON.parse(sessionStorage.getItem("challenge")!) 99 | if (code) { 100 | if (state === challenge.state && challenge.verifier) { 101 | const exchanged = await client.exchange( 102 | code!, 103 | location.origin, 104 | challenge.verifier, 105 | ) 106 | if (!exchanged.err) { 107 | token.current = exchanged.tokens?.access 108 | localStorage.setItem("refresh", exchanged.tokens.refresh) 109 | } 110 | } 111 | window.location.replace("/") 112 | } 113 | } 114 | 115 | async function user() { 116 | const res = await fetch("http://localhost:3001/", { 117 | headers: { 118 | Authorization: `Bearer ${token.current}`, 119 | }, 120 | }) 121 | 122 | if (res.ok) { 123 | setUserId(await res.text()) 124 | setLoggedIn(true) 125 | } 126 | } 127 | 128 | function logout() { 129 | localStorage.removeItem("refresh") 130 | token.current = undefined 131 | 132 | window.location.replace("/") 133 | } 134 | 135 | return ( 136 | 146 | {children} 147 | 148 | ) 149 | } 150 | 151 | export function useAuth() { 152 | return useContext(AuthContext) 153 | } 154 | -------------------------------------------------------------------------------- /examples/client/react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/client/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { AuthProvider } from "./AuthContext" 4 | import App from "./App" 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /examples/client/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/client/react/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/client/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/client/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/client/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import react from "@vitejs/plugin-react" 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/client/sveltekit/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /examples/client/sveltekit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-client-sveltekit", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "prepare": "svelte-kit sync || echo ''", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 12 | }, 13 | "devDependencies": { 14 | "@openauthjs/openauth": "^0.4.3", 15 | "@sveltejs/adapter-auto": "^4.0.0", 16 | "@sveltejs/kit": "^2.16.0", 17 | "@sveltejs/vite-plugin-svelte": "^5.0.0", 18 | "svelte": "^5.0.0", 19 | "svelte-check": "^4.0.0", 20 | "typescript": "5.6.3", 21 | "valibot": "1.0.0-beta.15", 22 | "vite": "^6.0.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | interface Locals { 7 | session: { id: string } 8 | } 9 | // interface PageData {} 10 | // interface PageState {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | export {} 16 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect, type Handle } from "@sveltejs/kit" 2 | import { createAuthClient, setTokens } from "$lib/auth.server" 3 | import { subjects } from "../../../subjects" 4 | 5 | export const handle: Handle = async ({ event, resolve }) => { 6 | if (event.url.pathname === "/callback") { 7 | return resolve(event) 8 | } 9 | 10 | const client = createAuthClient(event) 11 | try { 12 | const accessToken = event.cookies.get("access_token") 13 | if (accessToken) { 14 | const refreshToken = event.cookies.get("refresh_token") 15 | const verified = await client.verify(subjects, accessToken, { 16 | refresh: refreshToken, 17 | }) 18 | if (!verified.err) { 19 | if (verified.tokens) 20 | setTokens(event, verified.tokens.access, verified.tokens.refresh) 21 | event.locals.session = verified.subject.properties 22 | return resolve(event) 23 | } 24 | } 25 | } catch (e) {} 26 | 27 | const { url } = await client.authorize(event.url.origin + "/callback", "code") 28 | return redirect(302, url) 29 | } 30 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/lib/auth.server.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@openauthjs/openauth/client" 2 | import type { RequestEvent } from "@sveltejs/kit" 3 | 4 | export function createAuthClient(event: RequestEvent) { 5 | return createClient({ 6 | clientID: "openauth-sveltekit-example", 7 | issuer: "http://localhost:3000", 8 | fetch: event.fetch, 9 | }) 10 | } 11 | 12 | export function setTokens( 13 | event: RequestEvent, 14 | access: string, 15 | refresh: string, 16 | ) { 17 | event.cookies.set("refresh_token", refresh, { 18 | httpOnly: true, 19 | sameSite: "lax", 20 | path: "/", 21 | maxAge: 34560000, 22 | }) 23 | event.cookies.set("access_token", access, { 24 | httpOnly: true, 25 | sameSite: "lax", 26 | path: "/", 27 | maxAge: 34560000, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | export async function load(event) { 2 | return { 3 | subject: event.locals.session, 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

Hello {data.subject.id}

6 | -------------------------------------------------------------------------------- /examples/client/sveltekit/src/routes/callback/+server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@sveltejs/kit" 2 | import { createAuthClient, setTokens } from "$lib/auth.server.js" 3 | 4 | export async function GET(event) { 5 | const code = event.url.searchParams.get("code") 6 | const authClient = createAuthClient(event) 7 | const tokens = await authClient.exchange( 8 | code!, 9 | event.url.origin + "/callback", 10 | ) 11 | if (!tokens.err) { 12 | setTokens(event, tokens.tokens.access, tokens.tokens.refresh) 13 | } else { 14 | throw tokens.err 15 | } 16 | return redirect(302, `/`) 17 | } 18 | -------------------------------------------------------------------------------- /examples/client/sveltekit/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/examples/client/sveltekit/static/favicon.png -------------------------------------------------------------------------------- /examples/client/sveltekit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto" 2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter(), 15 | }, 16 | } 17 | 18 | export default config 19 | -------------------------------------------------------------------------------- /examples/client/sveltekit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /examples/client/sveltekit/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite" 2 | import { defineConfig } from "vite" 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/issuer/bun/.gitignore: -------------------------------------------------------------------------------- 1 | persist.json 2 | -------------------------------------------------------------------------------- /examples/issuer/bun/issuer.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 3 | import { PasswordProvider } from "@openauthjs/openauth/provider/password" 4 | import { PasswordUI } from "@openauthjs/openauth/ui/password" 5 | import { subjects } from "../../subjects.js" 6 | 7 | async function getUser(email: string) { 8 | // Get user from database 9 | // Return user ID 10 | return "123" 11 | } 12 | 13 | export default issuer({ 14 | subjects, 15 | storage: MemoryStorage({ 16 | persist: "./persist.json", 17 | }), 18 | providers: { 19 | password: PasswordProvider( 20 | PasswordUI({ 21 | sendCode: async (email, code) => { 22 | console.log(email, code) 23 | }, 24 | validatePassword: (password) => { 25 | if (password.length < 8) { 26 | return "Password must be at least 8 characters" 27 | } 28 | }, 29 | }), 30 | ), 31 | }, 32 | async allow() { 33 | return true 34 | }, 35 | success: async (ctx, value) => { 36 | if (value.provider === "password") { 37 | return ctx.subject("user", { 38 | id: await getUser(value.email), 39 | }) 40 | } 41 | throw new Error("Invalid provider") 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /examples/issuer/bun/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-issuer-bun", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@openauthjs/openauth": "workspace:*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/issuer/cloudflare/issuer.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare" 3 | import { 4 | type ExecutionContext, 5 | type KVNamespace, 6 | } from "@cloudflare/workers-types" 7 | import { subjects } from "../../subjects.js" 8 | import { PasswordProvider } from "@openauthjs/openauth/provider/password" 9 | import { PasswordUI } from "@openauthjs/openauth/ui/password" 10 | 11 | interface Env { 12 | CloudflareAuthKV: KVNamespace 13 | } 14 | 15 | async function getUser(email: string) { 16 | // Get user from database 17 | // Return user ID 18 | return "123" 19 | } 20 | 21 | export default { 22 | async fetch(request: Request, env: Env, ctx: ExecutionContext) { 23 | return issuer({ 24 | storage: CloudflareStorage({ 25 | namespace: env.CloudflareAuthKV, 26 | }), 27 | subjects, 28 | providers: { 29 | password: PasswordProvider( 30 | PasswordUI({ 31 | sendCode: async (email, code) => { 32 | console.log(email, code) 33 | }, 34 | }), 35 | ), 36 | }, 37 | success: async (ctx, value) => { 38 | if (value.provider === "password") { 39 | return ctx.subject("user", { 40 | id: await getUser(value.email), 41 | }) 42 | } 43 | throw new Error("Invalid provider") 44 | }, 45 | }).fetch(request, env, ctx) 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /examples/issuer/cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-issuer-cloudflare", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@openauthjs/openauth": "workspace:*", 6 | "sst": "3.5.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/issuer/cloudflare/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst" 6 | export {} 7 | declare module "sst" { 8 | export interface Resource { 9 | CloudflareAuth: { 10 | type: "sst.cloudflare.Worker" 11 | url: string 12 | } 13 | CloudflareAuthKV: { 14 | type: "sst.cloudflare.Kv" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/issuer/cloudflare/sst.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default $config({ 3 | app(input) { 4 | return { 5 | name: "openauth-example-cloudflare", 6 | removal: input?.stage === "production" ? "retain" : "remove", 7 | home: "cloudflare", 8 | } 9 | }, 10 | async run() { 11 | // cloudflare 12 | const kv = new sst.cloudflare.Kv("CloudflareAuthKV") 13 | const auth = new sst.cloudflare.Worker("CloudflareAuth", { 14 | handler: "./issuer.ts", 15 | link: [kv], 16 | url: true, 17 | }) 18 | 19 | return { 20 | url: auth.url, 21 | } 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /examples/issuer/custom-frontend/auth/issuer.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 3 | import { CodeProvider } from "@openauthjs/openauth/provider/code" 4 | import { subjects } from "../../../subjects.js" 5 | 6 | async function getUser(email: string) { 7 | // Get user from database 8 | // Return user ID 9 | return "123" 10 | } 11 | 12 | export default issuer({ 13 | subjects, 14 | storage: MemoryStorage({ 15 | persist: "./persist.json", 16 | }), 17 | providers: { 18 | code: CodeProvider({ 19 | sendCode: async (claims, code) => { 20 | console.log(claims.email, code) 21 | }, 22 | async request(req, state, _form, error) { 23 | const url = new URL(`http://localhost:3001`) 24 | url.pathname = `/auth/${state.type}` 25 | if (error) url.searchParams.set("error", error.type) 26 | return new Response(null, { 27 | status: 302, 28 | headers: { 29 | Location: url.toString(), 30 | }, 31 | }) 32 | }, 33 | }), 34 | }, 35 | success: async (ctx, value) => { 36 | if (value.provider === "code") { 37 | return ctx.subject("user", { 38 | id: await getUser(value.claims.email), 39 | }) 40 | } 41 | throw new Error("Invalid provider") 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /examples/issuer/custom-frontend/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-custom-frontend-issuer", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@openauthjs/openauth": "workspace:*" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/issuer/custom-frontend/frontend/frontend.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | /** @jsxImportSource hono/jsx */ 3 | 4 | import { Hono } from "hono" 5 | import { PropsWithChildren } from "hono/jsx" 6 | 7 | function Layout(props: PropsWithChildren) { 8 | return ( 9 | 10 | 11 | 12 | 13 | Issuer 14 | 15 | {props.children} 16 | 17 | ) 18 | } 19 | 20 | const app = new Hono() 21 | .get("/auth/start", async (c) => { 22 | return c.html( 23 | 24 |

Issuer

25 |
26 | 27 | 28 | 29 | 30 |
31 |
, 32 | ) 33 | }) 34 | .get("/auth/code", async (c) => { 35 | return c.html( 36 | 37 |

Issuer

38 |
39 | 40 | 41 | 48 | 49 |
50 |
, 51 | ) 52 | }) 53 | 54 | export default { 55 | port: 3001, 56 | fetch: app.fetch, 57 | } 58 | -------------------------------------------------------------------------------- /examples/issuer/custom-frontend/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-custom-frontend", 3 | "scripts": { 4 | "dev": "bun run --hot frontend.tsx" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/issuer/custom-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-frontend", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/issuer/lambda/issuer.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { handle } from "hono/aws-lambda" 3 | import { subjects } from "../../subjects.js" 4 | import { PasswordUI } from "@openauthjs/openauth/ui/password" 5 | import { PasswordProvider } from "@openauthjs/openauth/provider/password" 6 | 7 | async function getUser(email: string) { 8 | // Get user from database 9 | // Return user ID 10 | return "123" 11 | } 12 | 13 | const app = issuer({ 14 | subjects, 15 | providers: { 16 | password: PasswordProvider( 17 | PasswordUI({ 18 | sendCode: async (email, code) => { 19 | console.log(email, code) 20 | }, 21 | }), 22 | ), 23 | }, 24 | success: async (ctx, value) => { 25 | if (value.provider === "password") { 26 | return ctx.subject("user", { 27 | id: await getUser(value.email), 28 | }) 29 | } 30 | throw new Error("Invalid provider") 31 | }, 32 | }) 33 | 34 | // @ts-ignore 35 | export const handler = handle(app) 36 | -------------------------------------------------------------------------------- /examples/issuer/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/example-issuer-aws", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@openauthjs/openauth": "workspace:*", 6 | "sst": "3.5.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/issuer/lambda/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst" 6 | export {} 7 | declare module "sst" { 8 | export interface Resource {} 9 | } 10 | -------------------------------------------------------------------------------- /examples/issuer/lambda/sst.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default $config({ 3 | app(input) { 4 | return { 5 | name: "openauth-example-lambda", 6 | removal: input?.stage === "production" ? "retain" : "remove", 7 | home: "aws", 8 | } 9 | }, 10 | async run() { 11 | const auth = new sst.aws.Auth("Auth", { 12 | issuer: "./issuer.handler", 13 | }) 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /examples/issuer/node/.gitignore: -------------------------------------------------------------------------------- 1 | persist.json 2 | -------------------------------------------------------------------------------- /examples/issuer/node/authorizer.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 3 | import { PasswordUI } from "@openauthjs/openauth/ui/password" 4 | import { serve } from "@hono/node-server" 5 | import { subjects } from "../../subjects" 6 | import { PasswordProvider } from "@openauthjs/openauth/provider/password" 7 | 8 | async function getUser(email: string) { 9 | // Get user from database 10 | // Return user ID 11 | return "123" 12 | } 13 | 14 | const app = issuer({ 15 | subjects, 16 | storage: MemoryStorage({ 17 | persist: "./persist.json", 18 | }), 19 | providers: { 20 | password: PasswordProvider( 21 | PasswordUI({ 22 | sendCode: async (email, code) => { 23 | console.log(email, code) 24 | }, 25 | }), 26 | ), 27 | }, 28 | success: async (ctx, value) => { 29 | if (value.provider === "password") { 30 | return ctx.subject("user", { 31 | id: await getUser(value.email), 32 | }) 33 | } 34 | throw new Error("Invalid provider") 35 | }, 36 | }) 37 | 38 | serve(app) 39 | -------------------------------------------------------------------------------- /examples/issuer/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/quickstart/sst/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # sst 44 | .sst 45 | 46 | # open-next 47 | .open-next 48 | -------------------------------------------------------------------------------- /examples/quickstart/sst/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | import { headers as getHeaders, cookies as getCookies } from "next/headers" 5 | import { subjects } from "../auth/subjects" 6 | import { client, setTokens } from "./auth" 7 | 8 | export async function auth() { 9 | const cookies = await getCookies() 10 | const accessToken = cookies.get("access_token") 11 | const refreshToken = cookies.get("refresh_token") 12 | 13 | if (!accessToken) { 14 | return false 15 | } 16 | 17 | const verified = await client.verify(subjects, accessToken.value, { 18 | refresh: refreshToken?.value, 19 | }) 20 | 21 | if (verified.err) { 22 | return false 23 | } 24 | if (verified.tokens) { 25 | await setTokens(verified.tokens.access, verified.tokens.refresh) 26 | } 27 | 28 | return verified.subject 29 | } 30 | 31 | export async function login() { 32 | const cookies = await getCookies() 33 | const accessToken = cookies.get("access_token") 34 | const refreshToken = cookies.get("refresh_token") 35 | 36 | if (accessToken) { 37 | const verified = await client.verify(subjects, accessToken.value, { 38 | refresh: refreshToken?.value, 39 | }) 40 | if (!verified.err && verified.tokens) { 41 | await setTokens(verified.tokens.access, verified.tokens.refresh) 42 | redirect("/") 43 | } 44 | } 45 | 46 | const headers = await getHeaders() 47 | const host = headers.get("host") 48 | const protocol = host?.includes("localhost") ? "http" : "https" 49 | const { url } = await client.authorize( 50 | `${protocol}://${host}/api/callback`, 51 | "code", 52 | ) 53 | redirect(url) 54 | } 55 | 56 | export async function logout() { 57 | const cookies = await getCookies() 58 | cookies.delete("access_token") 59 | cookies.delete("refresh_token") 60 | 61 | redirect("/") 62 | } 63 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/api/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { client, setTokens } from "../../auth" 2 | import { type NextRequest, NextResponse } from "next/server" 3 | 4 | export async function GET(req: NextRequest) { 5 | const url = new URL(req.url) 6 | const code = url.searchParams.get("code") 7 | 8 | const exchanged = await client.exchange(code!, `${url.origin}/api/callback`) 9 | 10 | if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 }) 11 | 12 | await setTokens(exchanged.tokens.access, exchanged.tokens.refresh) 13 | 14 | return NextResponse.redirect(`${url.origin}/`) 15 | } 16 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/auth.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "sst" 2 | import { createClient } from "@openauthjs/openauth/client" 3 | import { cookies as getCookies } from "next/headers" 4 | 5 | export const client = createClient({ 6 | clientID: "nextjs", 7 | issuer: Resource.MyAuth.url, 8 | }) 9 | 10 | export async function setTokens(access: string, refresh: string) { 11 | const cookies = await getCookies() 12 | 13 | cookies.set({ 14 | name: "access_token", 15 | value: access, 16 | httpOnly: true, 17 | sameSite: "lax", 18 | path: "/", 19 | maxAge: 34560000, 20 | }) 21 | cookies.set({ 22 | name: "refresh_token", 23 | value: refresh, 24 | httpOnly: true, 25 | sameSite: "lax", 26 | path: "/", 27 | maxAge: 34560000, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/examples/quickstart/sst/app/favicon.ico -------------------------------------------------------------------------------- /examples/quickstart/sst/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | import "./globals.css" 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }) 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }) 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | } 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode 24 | }>) { 25 | return ( 26 | 27 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/quickstart/sst/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import styles from "./page.module.css" 3 | import { auth, login, logout } from "./actions" 4 | 5 | export default async function Home() { 6 | const subject = await auth() 7 | 8 | return ( 9 |
10 |
11 | Next.js logo 19 |
    20 | {subject ? ( 21 | <> 22 |
  1. 23 | Logged in as {subject.properties.id}. 24 |
  2. 25 |
  3. 26 | And then check out app/page.tsx. 27 |
  4. 28 | 29 | ) : ( 30 | <> 31 |
  5. Login with your email and password.
  6. 32 |
  7. 33 | And then check out app/page.tsx. 34 |
  8. 35 | 36 | )} 37 |
38 | 39 |
40 | {subject ? ( 41 |
42 | 43 |
44 | ) : ( 45 |
46 | 47 |
48 | )} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /examples/quickstart/sst/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { handle } from "hono/aws-lambda" 2 | import { issuer } from "@openauthjs/openauth" 3 | import { CodeUI } from "@openauthjs/openauth/ui/code" 4 | import { CodeProvider } from "@openauthjs/openauth/provider/code" 5 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 6 | import { subjects } from "./subjects" 7 | 8 | async function getUser(email: string) { 9 | // Get user from database and return user ID 10 | return "123" 11 | } 12 | 13 | const app = issuer({ 14 | subjects, 15 | storage: MemoryStorage(), 16 | // Remove after setting custom domain 17 | allow: async () => true, 18 | providers: { 19 | code: CodeProvider( 20 | CodeUI({ 21 | sendCode: async (email, code) => { 22 | console.log(email, code) 23 | }, 24 | }), 25 | ), 26 | }, 27 | success: async (ctx, value) => { 28 | if (value.provider === "code") { 29 | return ctx.subject("user", { 30 | id: await getUser(value.claims.email), 31 | }) 32 | } 33 | throw new Error("Invalid provider") 34 | }, 35 | }) 36 | 37 | export const handler = handle(app) 38 | -------------------------------------------------------------------------------- /examples/quickstart/sst/auth/subjects.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from "valibot" 2 | import { createSubjects } from "@openauthjs/openauth/subject" 3 | 4 | export const subjects = createSubjects({ 5 | user: object({ 6 | id: string(), 7 | }), 8 | }) 9 | -------------------------------------------------------------------------------- /examples/quickstart/sst/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /examples/quickstart/sst/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oa-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@openauthjs/openauth": "^0.3.2", 13 | "hono": "^4.6.16", 14 | "next": "15.1.4", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "sst": "latest", 18 | "valibot": "^1.0.0-beta.11" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20", 22 | "@types/react": "^19", 23 | "@types/react-dom": "^19", 24 | "typescript": "^5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/quickstart/sst/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/sst/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/sst/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/sst/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/sst/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/sst/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst" 6 | export {} 7 | declare module "sst" { 8 | export interface Resource { 9 | MyAuth: { 10 | type: "sst.aws.Auth" 11 | url: string 12 | } 13 | MyWeb: { 14 | type: "sst.aws.Nextjs" 15 | url: string 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/quickstart/sst/sst.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export default $config({ 4 | app(input) { 5 | return { 6 | name: "oa-nextjs", 7 | removal: input?.stage === "production" ? "retain" : "remove", 8 | protect: ["production"].includes(input?.stage), 9 | home: "aws", 10 | } 11 | }, 12 | async run() { 13 | const auth = new sst.aws.Auth("MyAuth", { 14 | issuer: "auth/index.handler", 15 | }) 16 | 17 | new sst.aws.Nextjs("MyWeb", { 18 | link: [auth], 19 | }) 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /examples/quickstart/sst/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules", "sst.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | import { headers as getHeaders, cookies as getCookies } from "next/headers" 5 | import { subjects } from "../auth/subjects" 6 | import { client, setTokens } from "./auth" 7 | 8 | export async function auth() { 9 | const cookies = await getCookies() 10 | const accessToken = cookies.get("access_token") 11 | const refreshToken = cookies.get("refresh_token") 12 | 13 | if (!accessToken) { 14 | return false 15 | } 16 | 17 | const verified = await client.verify(subjects, accessToken.value, { 18 | refresh: refreshToken?.value, 19 | }) 20 | 21 | if (verified.err) { 22 | return false 23 | } 24 | if (verified.tokens) { 25 | await setTokens(verified.tokens.access, verified.tokens.refresh) 26 | } 27 | 28 | return verified.subject 29 | } 30 | 31 | export async function login() { 32 | const cookies = await getCookies() 33 | const accessToken = cookies.get("access_token") 34 | const refreshToken = cookies.get("refresh_token") 35 | 36 | if (accessToken) { 37 | const verified = await client.verify(subjects, accessToken.value, { 38 | refresh: refreshToken?.value, 39 | }) 40 | if (!verified.err && verified.tokens) { 41 | await setTokens(verified.tokens.access, verified.tokens.refresh) 42 | redirect("/") 43 | } 44 | } 45 | 46 | const headers = await getHeaders() 47 | const host = headers.get("host") 48 | const protocol = host?.includes("localhost") ? "http" : "https" 49 | const { url } = await client.authorize( 50 | `${protocol}://${host}/api/callback`, 51 | "code", 52 | ) 53 | redirect(url) 54 | } 55 | 56 | export async function logout() { 57 | const cookies = await getCookies() 58 | cookies.delete("access_token") 59 | cookies.delete("refresh_token") 60 | 61 | redirect("/") 62 | } 63 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/api/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { client, setTokens } from "../../auth" 2 | import { type NextRequest, NextResponse } from "next/server" 3 | 4 | export async function GET(req: NextRequest) { 5 | const url = new URL(req.url) 6 | const code = url.searchParams.get("code") 7 | 8 | const exchanged = await client.exchange(code!, `${url.origin}/api/callback`) 9 | 10 | if (exchanged.err) return NextResponse.json(exchanged.err, { status: 400 }) 11 | 12 | await setTokens(exchanged.tokens.access, exchanged.tokens.refresh) 13 | 14 | return NextResponse.redirect(`${url.origin}/`) 15 | } 16 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/auth.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@openauthjs/openauth/client" 2 | import { cookies as getCookies } from "next/headers" 3 | 4 | export const client = createClient({ 5 | clientID: "nextjs", 6 | issuer: "http://localhost:3001", 7 | }) 8 | 9 | export async function setTokens(access: string, refresh: string) { 10 | const cookies = await getCookies() 11 | 12 | cookies.set({ 13 | name: "access_token", 14 | value: access, 15 | httpOnly: true, 16 | sameSite: "lax", 17 | path: "/", 18 | maxAge: 34560000, 19 | }) 20 | cookies.set({ 21 | name: "refresh_token", 22 | value: refresh, 23 | httpOnly: true, 24 | sameSite: "lax", 25 | path: "/", 26 | maxAge: 34560000, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/examples/quickstart/standalone/app/favicon.ico -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | import "./globals.css" 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }) 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }) 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | } 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode 24 | }>) { 25 | return ( 26 | 27 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import styles from "./page.module.css" 3 | import { auth, login, logout } from "./actions" 4 | 5 | export default async function Home() { 6 | const subject = await auth() 7 | 8 | return ( 9 |
10 |
11 | Next.js logo 19 |
    20 | {subject ? ( 21 | <> 22 |
  1. 23 | Logged in as {subject.properties.id}. 24 |
  2. 25 |
  3. 26 | And then check out app/page.tsx. 27 |
  4. 28 | 29 | ) : ( 30 | <> 31 |
  5. Login with your email and password.
  6. 32 |
  7. 33 | And then check out app/page.tsx. 34 |
  8. 35 | 36 | )} 37 |
38 | 39 |
40 | {subject ? ( 41 |
42 | 43 |
44 | ) : ( 45 |
46 | 47 |
48 | )} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth" 2 | import { CodeUI } from "@openauthjs/openauth/ui/code" 3 | import { CodeProvider } from "@openauthjs/openauth/provider/code" 4 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 5 | import { subjects } from "./subjects" 6 | 7 | async function getUser(email: string) { 8 | // Get user from database and return user ID 9 | return "123" 10 | } 11 | 12 | export default issuer({ 13 | subjects, 14 | storage: MemoryStorage(), 15 | providers: { 16 | code: CodeProvider( 17 | CodeUI({ 18 | sendCode: async (email, code) => { 19 | console.log(email, code) 20 | }, 21 | }), 22 | ), 23 | }, 24 | success: async (ctx, value) => { 25 | if (value.provider === "code") { 26 | return ctx.subject("user", { 27 | id: await getUser(value.claims.email), 28 | }) 29 | } 30 | throw new Error("Invalid provider") 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/auth/subjects.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from "valibot" 2 | import { createSubjects } from "@openauthjs/openauth/subject" 3 | 4 | export const subjects = createSubjects({ 5 | user: object({ 6 | id: string(), 7 | }), 8 | }) 9 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/examples/quickstart/standalone/bun.lockb -------------------------------------------------------------------------------- /examples/quickstart/standalone/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oa-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "dev:auth": "PORT=3001 bun run --hot auth/index.ts", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@openauthjs/openauth": "^0.3.2", 14 | "next": "15.1.4", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "valibot": "^1.0.0-beta.11" 18 | }, 19 | "devDependencies": { 20 | "typescript": "^5", 21 | "@types/node": "^20", 22 | "@types/react": "^19", 23 | "@types/react-dom": "^19" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/quickstart/standalone/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/subjects.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from "valibot" 2 | import { createSubjects } from "@openauthjs/openauth/subject" 3 | 4 | export const subjects = createSubjects({ 5 | user: object({ 6 | id: string(), 7 | }), 8 | }) 9 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "hono/jsx" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openauthjs", 3 | "module": "index.ts", 4 | "type": "module", 5 | "workspaces": [ 6 | "packages/openauth", 7 | "examples/issuer/*", 8 | "examples/client/*" 9 | ], 10 | "scripts": { 11 | "release": "bun run --filter=\"@openauthjs/openauth\" build && changeset publish" 12 | }, 13 | "devDependencies": { 14 | "@tsconfig/node22": "22.0.0", 15 | "@types/bun": "latest" 16 | }, 17 | "dependencies": { 18 | "@changesets/cli": "2.27.10", 19 | "prettier": "3.4.2", 20 | "typescript": "5.6.3" 21 | }, 22 | "private": true 23 | } 24 | -------------------------------------------------------------------------------- /packages/openauth/bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | root = "./test" 3 | -------------------------------------------------------------------------------- /packages/openauth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openauthjs/openauth", 3 | "version": "0.4.3", 4 | "type": "module", 5 | "scripts": { 6 | "build": "bun run script/build.ts", 7 | "test": "bun test" 8 | }, 9 | "sideEffects": false, 10 | "devDependencies": { 11 | "@cloudflare/workers-types": "4.20241205.0", 12 | "@tsconfig/node22": "22.0.0", 13 | "@types/node": "22.10.1", 14 | "arctic": "2.2.2", 15 | "hono": "4.6.9", 16 | "typescript": "5.6.3", 17 | "valibot": "1.0.0-beta.15" 18 | }, 19 | "exports": { 20 | ".": { 21 | "import": "./dist/esm/index.js", 22 | "types": "./dist/types/index.d.ts" 23 | }, 24 | "./*": { 25 | "import": "./dist/esm/*.js", 26 | "types": "./dist/types/*.d.ts" 27 | }, 28 | "./ui": { 29 | "import": "./dist/esm/ui/index.js", 30 | "types": "./dist/types/ui/index.d.ts" 31 | } 32 | }, 33 | "peerDependencies": { 34 | "arctic": "^2.2.2", 35 | "hono": "^4.0.0" 36 | }, 37 | "dependencies": { 38 | "@standard-schema/spec": "1.0.0-beta.3", 39 | "aws4fetch": "1.0.20", 40 | "jose": "5.9.6" 41 | }, 42 | "files": [ 43 | "src", 44 | "dist" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /packages/openauth/script/build.ts: -------------------------------------------------------------------------------- 1 | import { Glob, $ } from "bun" 2 | import pkg from "../package.json" 3 | 4 | await $`rm -rf dist` 5 | const files = new Glob("./src/**/*.{ts,tsx}").scan() 6 | for await (const file of files) { 7 | await Bun.build({ 8 | format: "esm", 9 | outdir: "dist/esm", 10 | external: ["*"], 11 | root: "src", 12 | entrypoints: [file], 13 | }) 14 | } 15 | await Bun.build({ 16 | format: "esm", 17 | outdir: "dist/esm", 18 | external: [ 19 | ...Object.keys(pkg.dependencies), 20 | ...Object.keys(pkg.peerDependencies), 21 | ], 22 | root: "src", 23 | entrypoints: ["./src/ui/base.tsx"], 24 | }) 25 | await $`tsc --outDir dist/types --declaration --emitDeclarationOnly --declarationMap` 26 | -------------------------------------------------------------------------------- /packages/openauth/src/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" { 2 | const css: string 3 | export default css 4 | } 5 | -------------------------------------------------------------------------------- /packages/openauth/src/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A list of errors that can be thrown by OpenAuth. 3 | * 4 | * You can use these errors to check the type of error and handle it. For example. 5 | * 6 | * ```ts 7 | * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error" 8 | * 9 | * if (err instanceof InvalidAuthorizationCodeError) { 10 | * // handle invalid code error 11 | * } 12 | * ``` 13 | * 14 | * @packageDocumentation 15 | */ 16 | 17 | /** 18 | * The OAuth server returned an error. 19 | */ 20 | export class OauthError extends Error { 21 | constructor( 22 | public error: 23 | | "invalid_request" 24 | | "invalid_grant" 25 | | "unauthorized_client" 26 | | "access_denied" 27 | | "unsupported_grant_type" 28 | | "server_error" 29 | | "temporarily_unavailable", 30 | public description: string, 31 | ) { 32 | super(error + " - " + description) 33 | } 34 | } 35 | 36 | /** 37 | * The `provider` needs to be passed in. 38 | */ 39 | export class MissingProviderError extends OauthError { 40 | constructor() { 41 | super( 42 | "invalid_request", 43 | "Must specify `provider` query parameter if `select` callback on issuer is not specified", 44 | ) 45 | } 46 | } 47 | 48 | /** 49 | * The given parameter is missing. 50 | */ 51 | export class MissingParameterError extends OauthError { 52 | constructor(public parameter: string) { 53 | super("invalid_request", "Missing parameter: " + parameter) 54 | } 55 | } 56 | 57 | /** 58 | * The given client is not authorized to use the redirect URI that was passed in. 59 | */ 60 | export class UnauthorizedClientError extends OauthError { 61 | constructor( 62 | public clientID: string, 63 | redirectURI: string, 64 | ) { 65 | super( 66 | "unauthorized_client", 67 | `Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}`, 68 | ) 69 | } 70 | } 71 | 72 | /** 73 | * The browser was in an unknown state. 74 | * 75 | * This can happen when certain cookies have expired. Or the browser was switched in the middle 76 | * of the authentication flow. 77 | */ 78 | export class UnknownStateError extends Error { 79 | constructor() { 80 | super( 81 | "The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.", 82 | ) 83 | } 84 | } 85 | 86 | /** 87 | * The given subject is invalid. 88 | */ 89 | export class InvalidSubjectError extends Error { 90 | constructor() { 91 | super("Invalid subject") 92 | } 93 | } 94 | 95 | /** 96 | * The given refresh token is invalid. 97 | */ 98 | export class InvalidRefreshTokenError extends Error { 99 | constructor() { 100 | super("Invalid refresh token") 101 | } 102 | } 103 | 104 | /** 105 | * The given access token is invalid. 106 | */ 107 | export class InvalidAccessTokenError extends Error { 108 | constructor() { 109 | super("Invalid access token") 110 | } 111 | } 112 | 113 | /** 114 | * The given authorization code is invalid. 115 | */ 116 | export class InvalidAuthorizationCodeError extends Error { 117 | constructor() { 118 | super("Invalid authorization code") 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/openauth/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | /** 3 | * @deprecated 4 | * Use `import { createClient } from "@openauthjs/openauth/client"` instead - it will tree shake better 5 | */ 6 | createClient, 7 | } from "./client.js" 8 | 9 | export { 10 | /** 11 | * @deprecated 12 | * Use `import { createSubjects } from "@openauthjs/openauth/subject"` instead - it will tree shake better 13 | */ 14 | createSubjects, 15 | } from "./subject.js" 16 | 17 | import { issuer } from "./issuer.js" 18 | 19 | export { 20 | /** 21 | * @deprecated 22 | * Use `import { issuer } from "@openauthjs/openauth"` instead, it was renamed 23 | */ 24 | issuer as authorizer, 25 | issuer, 26 | } 27 | -------------------------------------------------------------------------------- /packages/openauth/src/jwt.ts: -------------------------------------------------------------------------------- 1 | import { JWTPayload, jwtVerify, KeyLike, SignJWT } from "jose" 2 | 3 | export namespace jwt { 4 | export function create( 5 | payload: JWTPayload, 6 | algorithm: string, 7 | privateKey: KeyLike, 8 | ) { 9 | return new SignJWT(payload) 10 | .setProtectedHeader({ alg: algorithm, typ: "JWT", kid: "sst" }) 11 | .sign(privateKey) 12 | } 13 | 14 | export function verify(token: string, publicKey: KeyLike) { 15 | return jwtVerify(token, publicKey) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/openauth/src/pkce.ts: -------------------------------------------------------------------------------- 1 | import { base64url } from "jose" 2 | 3 | function generateVerifier(length: number): string { 4 | const buffer = new Uint8Array(length) 5 | crypto.getRandomValues(buffer) 6 | return base64url.encode(buffer) 7 | } 8 | 9 | async function generateChallenge(verifier: string, method: "S256" | "plain") { 10 | if (method === "plain") return verifier 11 | const encoder = new TextEncoder() 12 | const data = encoder.encode(verifier) 13 | const hash = await crypto.subtle.digest("SHA-256", data) 14 | return base64url.encode(new Uint8Array(hash)) 15 | } 16 | 17 | export async function generatePKCE(length: number = 64) { 18 | if (length < 43 || length > 128) { 19 | throw new Error( 20 | "Code verifier length must be between 43 and 128 characters", 21 | ) 22 | } 23 | const verifier = generateVerifier(length) 24 | const challenge = await generateChallenge(verifier, "S256") 25 | return { 26 | verifier, 27 | challenge, 28 | method: "S256", 29 | } 30 | } 31 | 32 | export async function validatePKCE( 33 | verifier: string, 34 | challenge: string, 35 | method: "S256" | "plain" = "S256", 36 | ) { 37 | const generatedChallenge = await generateChallenge(verifier, method) 38 | // timing safe equals? 39 | return generatedChallenge === challenge 40 | } 41 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/apple.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Apple. Supports both OAuth2 and OIDC. 3 | * 4 | * #### Using OAuth 5 | * 6 | * ```ts {5-8} 7 | * import { AppleProvider } from "@openauthjs/openauth/provider/apple" 8 | * 9 | * export default issuer({ 10 | * providers: { 11 | * apple: AppleProvider({ 12 | * clientID: "1234567890", 13 | * clientSecret: "0987654321" 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * #### Using OAuth with form_post response mode 20 | * 21 | * When requesting name or email scopes from Apple, you must use form_post response mode: 22 | * 23 | * ```ts {5-9} 24 | * import { AppleProvider } from "@openauthjs/openauth/provider/apple" 25 | * 26 | * export default issuer({ 27 | * providers: { 28 | * apple: AppleProvider({ 29 | * clientID: "1234567890", 30 | * clientSecret: "0987654321", 31 | * responseMode: "form_post" 32 | * }) 33 | * } 34 | * }) 35 | * ``` 36 | * 37 | * #### Using OIDC 38 | * 39 | * ```ts {5-7} 40 | * import { AppleOidcProvider } from "@openauthjs/openauth/provider/apple" 41 | * 42 | * export default issuer({ 43 | * providers: { 44 | * apple: AppleOidcProvider({ 45 | * clientID: "1234567890" 46 | * }) 47 | * } 48 | * }) 49 | * ``` 50 | * 51 | * @packageDocumentation 52 | */ 53 | 54 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 55 | import { OidcProvider, OidcWrappedConfig } from "./oidc.js" 56 | 57 | export interface AppleConfig extends Oauth2WrappedConfig { 58 | /** 59 | * The response mode to use for the authorization request. 60 | * Apple requires 'form_post' response mode when requesting name or email scopes. 61 | * @default "query" 62 | */ 63 | responseMode?: "query" | "form_post" 64 | } 65 | export interface AppleOidcConfig extends OidcWrappedConfig {} 66 | 67 | /** 68 | * Create an Apple OAuth2 provider. 69 | * 70 | * @param config - The config for the provider. 71 | * @example 72 | * ```ts 73 | * // Using default query response mode (GET callback) 74 | * AppleProvider({ 75 | * clientID: "1234567890", 76 | * clientSecret: "0987654321" 77 | * }) 78 | * 79 | * // Using form_post response mode (POST callback) 80 | * // Required when requesting name or email scope 81 | * AppleProvider({ 82 | * clientID: "1234567890", 83 | * clientSecret: "0987654321", 84 | * responseMode: "form_post", 85 | * scopes: ["name", "email"] 86 | * }) 87 | * ``` 88 | */ 89 | export function AppleProvider(config: AppleConfig) { 90 | const { responseMode, ...restConfig } = config 91 | const additionalQuery = 92 | responseMode === "form_post" 93 | ? { response_mode: "form_post", ...config.query } 94 | : config.query || {} 95 | 96 | return Oauth2Provider({ 97 | ...restConfig, 98 | type: "apple" as const, 99 | endpoint: { 100 | authorization: "https://appleid.apple.com/auth/authorize", 101 | token: "https://appleid.apple.com/auth/token", 102 | jwks: "https://appleid.apple.com/auth/keys", 103 | }, 104 | query: additionalQuery, 105 | }) 106 | } 107 | 108 | /** 109 | * Create an Apple OIDC provider. 110 | * 111 | * This is useful if you just want to verify the user's email address. 112 | * 113 | * @param config - The config for the provider. 114 | * @example 115 | * ```ts 116 | * AppleOidcProvider({ 117 | * clientID: "1234567890" 118 | * }) 119 | * ``` 120 | */ 121 | export function AppleOidcProvider(config: AppleOidcConfig) { 122 | return OidcProvider({ 123 | ...config, 124 | type: "apple" as const, 125 | issuer: "https://appleid.apple.com", 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/arctic.ts: -------------------------------------------------------------------------------- 1 | import type { OAuth2Tokens } from "arctic" 2 | import { Context } from "hono" 3 | import { Provider } from "./provider.js" 4 | import { OauthError } from "../error.js" 5 | import { getRelativeUrl } from "../util.js" 6 | 7 | export interface ArcticProviderOptions { 8 | scopes: string[] 9 | clientID: string 10 | clientSecret: string 11 | query?: Record 12 | } 13 | 14 | interface ProviderState { 15 | state: string 16 | } 17 | 18 | export function ArcticProvider( 19 | provider: new ( 20 | clientID: string, 21 | clientSecret: string, 22 | callback: string, 23 | ) => { 24 | createAuthorizationURL(state: string, scopes: string[]): URL 25 | validateAuthorizationCode(code: string): Promise 26 | refreshAccessToken(refreshToken: string): Promise 27 | }, 28 | config: ArcticProviderOptions, 29 | ): Provider<{ 30 | tokenset: OAuth2Tokens 31 | }> { 32 | function getClient(c: Context) { 33 | const callback = new URL(c.req.url) 34 | const pathname = callback.pathname.replace(/authorize.*$/, "callback") 35 | const url = getRelativeUrl(c, pathname) 36 | return new provider(config.clientID, config.clientSecret, url) 37 | } 38 | return { 39 | type: "arctic", 40 | init(routes, ctx) { 41 | routes.get("/authorize", async (c) => { 42 | const client = getClient(c) 43 | const state = crypto.randomUUID() 44 | await ctx.set(c, "provider", 60 * 10, { 45 | state, 46 | }) 47 | return c.redirect(client.createAuthorizationURL(state, config.scopes)) 48 | }) 49 | 50 | routes.get("/callback", async (c) => { 51 | const client = getClient(c) 52 | const provider = (await ctx.get(c, "provider")) as ProviderState 53 | if (!provider) return c.redirect("../authorize") 54 | const code = c.req.query("code") 55 | const state = c.req.query("state") 56 | if (!code) throw new Error("Missing code") 57 | if (state !== provider.state) 58 | throw new OauthError("invalid_request", "Invalid state") 59 | const tokens = await client.validateAuthorizationCode(code) 60 | return ctx.success(c, { 61 | tokenset: tokens, 62 | }) 63 | }) 64 | }, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/cognito.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with a Cognito OAuth endpoint. 3 | * 4 | * ```ts {5-10} 5 | * import { CognitoProvider } from "@openauthjs/openauth/provider/cognito" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * cognito: CognitoProvider({ 10 | * domain: "your-domain.auth.us-east-1.amazoncognito.com", 11 | * region: "us-east-1", 12 | * clientID: "1234567890", 13 | * clientSecret: "0987654321" 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * @packageDocumentation 20 | */ 21 | 22 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 23 | 24 | export interface CognitoConfig extends Oauth2WrappedConfig { 25 | /** 26 | * The domain of the Cognito User Pool. 27 | * 28 | * @example 29 | * ```ts 30 | * { 31 | * domain: "your-domain.auth.us-east-1.amazoncognito.com" 32 | * } 33 | * ``` 34 | */ 35 | domain: string 36 | /** 37 | * The region the Cognito User Pool is in. 38 | * 39 | * @example 40 | * ```ts 41 | * { 42 | * region: "us-east-1" 43 | * } 44 | * ``` 45 | */ 46 | region: string 47 | } 48 | 49 | /** 50 | * Create a Cognito OAuth2 provider. 51 | * 52 | * @param config - The config for the provider. 53 | * @example 54 | * ```ts 55 | * CognitoProvider({ 56 | * domain: "your-domain.auth.us-east-1.amazoncognito.com", 57 | * region: "us-east-1", 58 | * clientID: "1234567890", 59 | * clientSecret: "0987654321" 60 | * }) 61 | * ``` 62 | */ 63 | export function CognitoProvider(config: CognitoConfig) { 64 | const domain = `${config.domain}.auth.${config.region}.amazoncognito.com` 65 | 66 | return Oauth2Provider({ 67 | type: "cognito", 68 | ...config, 69 | endpoint: { 70 | authorization: `https://${domain}/oauth2/authorize`, 71 | token: `https://${domain}/oauth2/token`, 72 | }, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/discord.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Discord. 3 | * 4 | * ```ts {5-8} 5 | * import { DiscordProvider } from "@openauthjs/openauth/provider/discord" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * discord: DiscordProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface DiscordConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a Discord OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * DiscordProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function DiscordProvider(config: DiscordConfig) { 37 | return Oauth2Provider({ 38 | type: "discord", 39 | ...config, 40 | endpoint: { 41 | authorization: "https://discord.com/oauth2/authorize", 42 | token: "https://discord.com/api/oauth2/token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/facebook.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Facebook. Supports both OAuth2 and OIDC. 3 | * 4 | * #### Using OAuth 5 | * 6 | * ```ts {5-8} 7 | * import { FacebookProvider } from "@openauthjs/openauth/provider/facebook" 8 | * 9 | * export default issuer({ 10 | * providers: { 11 | * facebook: FacebookProvider({ 12 | * clientID: "1234567890", 13 | * clientSecret: "0987654321" 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * #### Using OIDC 20 | * 21 | * ```ts {5-7} 22 | * import { FacebookOidcProvider } from "@openauthjs/openauth/provider/facebook" 23 | * 24 | * export default issuer({ 25 | * providers: { 26 | * facebook: FacebookOidcProvider({ 27 | * clientID: "1234567890" 28 | * }) 29 | * } 30 | * }) 31 | * ``` 32 | * 33 | * @packageDocumentation 34 | */ 35 | 36 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 37 | import { OidcProvider, OidcWrappedConfig } from "./oidc.js" 38 | 39 | export interface FacebookConfig extends Oauth2WrappedConfig {} 40 | export interface FacebookOidcConfig extends OidcWrappedConfig {} 41 | 42 | /** 43 | * Create a Facebook OAuth2 provider. 44 | * 45 | * @param config - The config for the provider. 46 | * @example 47 | * ```ts 48 | * FacebookProvider({ 49 | * clientID: "1234567890", 50 | * clientSecret: "0987654321" 51 | * }) 52 | * ``` 53 | */ 54 | export function FacebookProvider(config: FacebookConfig) { 55 | return Oauth2Provider({ 56 | ...config, 57 | type: "facebook", 58 | endpoint: { 59 | authorization: "https://www.facebook.com/v12.0/dialog/oauth", 60 | token: "https://graph.facebook.com/v12.0/oauth/access_token", 61 | }, 62 | }) 63 | } 64 | 65 | /** 66 | * Create a Facebook OIDC provider. 67 | * 68 | * This is useful if you just want to verify the user's email address. 69 | * 70 | * @param config - The config for the provider. 71 | * @example 72 | * ```ts 73 | * FacebookOidcProvider({ 74 | * clientID: "1234567890" 75 | * }) 76 | * ``` 77 | */ 78 | export function FacebookOidcProvider(config: FacebookOidcConfig) { 79 | return OidcProvider({ 80 | ...config, 81 | type: "facebook", 82 | issuer: "https://graph.facebook.com", 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/github.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Github. 3 | * 4 | * ```ts {5-8} 5 | * import { GithubProvider } from "@openauthjs/openauth/provider/github" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * github: GithubProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface GithubConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a Github OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * GithubProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function GithubProvider(config: GithubConfig) { 37 | return Oauth2Provider({ 38 | ...config, 39 | type: "github", 40 | endpoint: { 41 | authorization: "https://github.com/login/oauth/authorize", 42 | token: "https://github.com/login/oauth/access_token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/google.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Google. Supports both OAuth2 and OIDC. 3 | * 4 | * #### Using OAuth 5 | * 6 | * ```ts {5-8} 7 | * import { GoogleProvider } from "@openauthjs/openauth/provider/google" 8 | * 9 | * export default issuer({ 10 | * providers: { 11 | * google: GoogleProvider({ 12 | * clientID: "1234567890", 13 | * clientSecret: "0987654321" 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * #### Using OIDC 20 | * 21 | * ```ts {5-7} 22 | * import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google" 23 | * 24 | * export default issuer({ 25 | * providers: { 26 | * google: GoogleOidcProvider({ 27 | * clientID: "1234567890" 28 | * }) 29 | * } 30 | * }) 31 | * ``` 32 | * 33 | * @packageDocumentation 34 | */ 35 | 36 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 37 | import { OidcProvider, OidcWrappedConfig } from "./oidc.js" 38 | 39 | export interface GoogleConfig extends Oauth2WrappedConfig {} 40 | export interface GoogleOidcConfig extends OidcWrappedConfig {} 41 | 42 | /** 43 | * Create a Google OAuth2 provider. 44 | * 45 | * @param config - The config for the provider. 46 | * @example 47 | * ```ts 48 | * GoogleProvider({ 49 | * clientID: "1234567890", 50 | * clientSecret: "0987654321" 51 | * }) 52 | * ``` 53 | */ 54 | export function GoogleProvider(config: GoogleConfig) { 55 | return Oauth2Provider({ 56 | ...config, 57 | type: "google", 58 | endpoint: { 59 | authorization: "https://accounts.google.com/o/oauth2/v2/auth", 60 | token: "https://oauth2.googleapis.com/token", 61 | jwks: "https://www.googleapis.com/oauth2/v3/certs", 62 | }, 63 | }) 64 | } 65 | 66 | /** 67 | * Create a Google OIDC provider. 68 | * 69 | * This is useful if you just want to verify the user's email address. 70 | * 71 | * @param config - The config for the provider. 72 | * @example 73 | * ```ts 74 | * GoogleOidcProvider({ 75 | * clientID: "1234567890" 76 | * }) 77 | * ``` 78 | */ 79 | export function GoogleOidcProvider(config: GoogleOidcConfig) { 80 | return OidcProvider({ 81 | ...config, 82 | type: "google", 83 | issuer: "https://accounts.google.com", 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./code.js" 2 | export type { Provider as Provider } from "./provider.js" 3 | export * from "./spotify.js" 4 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/jumpcloud.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with JumpCloud. 3 | * 4 | * ```ts {5-8} 5 | * import { JumpCloudProvider } from "@openauthjs/openauth/provider/jumpcloud" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * jumpcloud: JumpCloudProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface JumpCloudConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a JumpCloud OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * JumpCloudProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function JumpCloudProvider(config: JumpCloudConfig) { 37 | return Oauth2Provider({ 38 | type: "jumpcloud", 39 | ...config, 40 | endpoint: { 41 | authorization: "https://oauth.id.jumpcloud.com/oauth2/auth", 42 | token: "https://oauth.id.jumpcloud.com/oauth2/token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/keycloak.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with a Keycloak server. 3 | * 4 | * ```ts {5-10} 5 | * import { KeycloakProvider } from "@openauthjs/openauth/provider/keycloak" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * keycloak: KeycloakProvider({ 10 | * baseUrl: "https://your-keycloak-domain", 11 | * realm: "your-realm", 12 | * clientID: "1234567890", 13 | * clientSecret: "0987654321" 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * @packageDocumentation 20 | */ 21 | 22 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 23 | 24 | export interface KeycloakConfig extends Oauth2WrappedConfig { 25 | /** 26 | * The base URL of the Keycloak server. 27 | * 28 | * @example 29 | * ```ts 30 | * { 31 | * baseUrl: "https://your-keycloak-domain" 32 | * } 33 | * ``` 34 | */ 35 | baseUrl: string 36 | /** 37 | * The realm in the Keycloak server to authenticate against. 38 | * 39 | * A realm in Keycloak is like a tenant or namespace that manages a set of 40 | * users, credentials, roles, and groups. 41 | * 42 | * @example 43 | * ```ts 44 | * { 45 | * realm: "your-realm" 46 | * } 47 | * ``` 48 | */ 49 | realm: string 50 | } 51 | 52 | /** 53 | * Create a Keycloak OAuth2 provider. 54 | * 55 | * @param config - The config for the provider. 56 | * @example 57 | * ```ts 58 | * KeycloakProvider({ 59 | * baseUrl: "https://your-keycloak-domain", 60 | * realm: "your-realm", 61 | * clientID: "1234567890", 62 | * clientSecret: "0987654321" 63 | * }) 64 | * ``` 65 | */ 66 | export function KeycloakProvider(config: KeycloakConfig) { 67 | const baseConfig = { 68 | ...config, 69 | endpoint: { 70 | authorization: `${config.baseUrl}/realms/${config.realm}/protocol/openid-connect/auth`, 71 | token: `${config.baseUrl}/realms/${config.realm}/protocol/openid-connect/token`, 72 | }, 73 | } 74 | return Oauth2Provider(baseConfig) 75 | } 76 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/linkedin.ts: -------------------------------------------------------------------------------- 1 | import { Oauth2Provider, type Oauth2WrappedConfig } from "./oauth2.js" 2 | 3 | export function LinkedInAdapter(config: Oauth2WrappedConfig) { 4 | return Oauth2Provider({ 5 | ...config, 6 | type: "linkedin", 7 | endpoint: { 8 | authorization: "https://www.linkedin.com/oauth/v2/authorization", 9 | token: "https://www.linkedin.com/oauth/v2/accessToken", 10 | }, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/microsoft.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Microsoft. Supports both OAuth2 and OIDC. 3 | * 4 | * #### Using OAuth 5 | * 6 | * ```ts {5-9} 7 | * import { MicrosoftProvider } from "@openauthjs/openauth/provider/microsoft" 8 | * 9 | * export default issuer({ 10 | * providers: { 11 | * microsoft: MicrosoftProvider({ 12 | * tenant: "1234567890", 13 | * clientID: "1234567890", 14 | * clientSecret: "0987654321" 15 | * }) 16 | * } 17 | * }) 18 | * ``` 19 | * 20 | * #### Using OIDC 21 | * 22 | * ```ts {5-7} 23 | * import { MicrosoftOidcProvider } from "@openauthjs/openauth/provider/microsoft" 24 | * 25 | * export default issuer({ 26 | * providers: { 27 | * microsoft: MicrosoftOidcProvider({ 28 | * clientID: "1234567890" 29 | * }) 30 | * } 31 | * }) 32 | * ``` 33 | * 34 | * @packageDocumentation 35 | */ 36 | 37 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 38 | import { OidcProvider, OidcWrappedConfig } from "./oidc.js" 39 | 40 | export interface MicrosoftConfig extends Oauth2WrappedConfig { 41 | /** 42 | * The tenant ID of the Microsoft account. 43 | * 44 | * This is usually the same as the client ID. 45 | * 46 | * @example 47 | * ```ts 48 | * { 49 | * tenant: "1234567890" 50 | * } 51 | * ``` 52 | */ 53 | tenant: string 54 | } 55 | export interface MicrosoftOidcConfig extends OidcWrappedConfig {} 56 | 57 | /** 58 | * Create a Microsoft OAuth2 provider. 59 | * 60 | * @param config - The config for the provider. 61 | * @example 62 | * ```ts 63 | * MicrosoftProvider({ 64 | * tenant: "1234567890", 65 | * clientID: "1234567890", 66 | * clientSecret: "0987654321" 67 | * }) 68 | * ``` 69 | */ 70 | export function MicrosoftProvider(config: MicrosoftConfig) { 71 | return Oauth2Provider({ 72 | ...config, 73 | type: "microsoft", 74 | endpoint: { 75 | authorization: `https://login.microsoftonline.com/${config?.tenant}/oauth2/v2.0/authorize`, 76 | token: `https://login.microsoftonline.com/${config?.tenant}/oauth2/v2.0/token`, 77 | }, 78 | }) 79 | } 80 | 81 | /** 82 | * Create a Microsoft OIDC provider. 83 | * 84 | * This is useful if you just want to verify the user's email address. 85 | * 86 | * @param config - The config for the provider. 87 | * @example 88 | * ```ts 89 | * MicrosoftOidcProvider({ 90 | * clientID: "1234567890" 91 | * }) 92 | * ``` 93 | */ 94 | export function MicrosoftOidcProvider(config: MicrosoftOidcConfig) { 95 | return OidcProvider({ 96 | ...config, 97 | type: "microsoft", 98 | issuer: "https://graph.microsoft.com/oidc/userinfo", 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/provider.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Hono } from "hono" 2 | import { StorageAdapter } from "../storage/storage.js" 3 | 4 | export type ProviderRoute = Hono 5 | 6 | export interface Provider { 7 | type: string 8 | init: (route: ProviderRoute, options: ProviderOptions) => void 9 | client?: (input: { 10 | clientID: string 11 | clientSecret: string 12 | params: Record 13 | }) => Promise 14 | } 15 | 16 | export interface ProviderOptions { 17 | name: string 18 | success: ( 19 | ctx: Context, 20 | properties: Properties, 21 | opts?: { 22 | invalidate?: (subject: string) => Promise 23 | }, 24 | ) => Promise 25 | forward: (ctx: Context, response: Response) => Response 26 | set: (ctx: Context, key: string, maxAge: number, value: T) => Promise 27 | get: (ctx: Context, key: string) => Promise 28 | unset: (ctx: Context, key: string) => Promise 29 | invalidate: (subject: string) => Promise 30 | storage: StorageAdapter 31 | } 32 | export class ProviderError extends Error {} 33 | export class ProviderUnknownError extends ProviderError {} 34 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/slack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Slack. 3 | * 4 | * ```ts {5-10} 5 | * import { SlackProvider } from "@openauthjs/openauth/provider/slack" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * slack: SlackProvider({ 10 | * team: "T1234567890", 11 | * clientID: "1234567890", 12 | * clientSecret: "0987654321", 13 | * scopes: ["openid", "email", "profile"] 14 | * }) 15 | * } 16 | * }) 17 | * ``` 18 | * 19 | * @packageDocumentation 20 | */ 21 | 22 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 23 | 24 | export interface SlackConfig extends Oauth2WrappedConfig { 25 | /** 26 | * The workspace the user is intending to authenticate. 27 | * 28 | * If that workspace has been previously authenticated, the user will be signed in directly, 29 | * bypassing the consent screen. 30 | */ 31 | team: string 32 | /** 33 | * The scopes to request from the user. 34 | * 35 | * | Scope | Description | 36 | * |-|-| 37 | * | `email` | Grants permission to access the user's email address. | 38 | * | `profile` | Grants permission to access the user's profile information. | 39 | * | `openid` | Grants permission to use OpenID Connect to verify the user's identity. | 40 | */ 41 | scopes: ("email" | "profile" | "openid")[] 42 | } 43 | 44 | /** 45 | * Creates a [Slack OAuth2 provider](https://api.slack.com/authentication/sign-in-with-slack). 46 | * 47 | * @param {SlackConfig} config - The config for the provider. 48 | * @example 49 | * ```ts 50 | * SlackProvider({ 51 | * team: "T1234567890", 52 | * clientID: "1234567890", 53 | * clientSecret: "0987654321", 54 | * scopes: ["openid", "email", "profile"] 55 | * }) 56 | * ``` 57 | */ 58 | export function SlackProvider(config: SlackConfig) { 59 | return Oauth2Provider({ 60 | ...config, 61 | type: "slack", 62 | endpoint: { 63 | authorization: "https://slack.com/openid/connect/authorize", 64 | token: "https://slack.com/api/openid.connect.token", 65 | }, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/spotify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Spotify. 3 | * 4 | * ```ts {5-8} 5 | * import { SpotifyProvider } from "@openauthjs/openauth/provider/spotify" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * spotify: SpotifyProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, type Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface SpotifyConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a Spotify OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * SpotifyProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function SpotifyProvider(config: SpotifyConfig) { 37 | return Oauth2Provider({ 38 | ...config, 39 | type: "spotify", 40 | endpoint: { 41 | authorization: "https://accounts.spotify.com/authorize", 42 | token: "https://accounts.spotify.com/api/token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/twitch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Twitch. 3 | * 4 | * ```ts {5-8} 5 | * import { TwitchProvider } from "@openauthjs/openauth/provider/twitch" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * twitch: TwitchProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface TwitchConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a Twitch OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * TwitchProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function TwitchProvider(config: TwitchConfig) { 37 | return Oauth2Provider({ 38 | type: "twitch", 39 | ...config, 40 | endpoint: { 41 | authorization: "https://id.twitch.tv/oauth2/authorize", 42 | token: "https://id.twitch.tv/oauth2/token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/x.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with X.com. 3 | * 4 | * ```ts {5-8} 5 | * import { XProvider } from "@openauthjs/openauth/provider/x" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * x: XProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface XProviderConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a X.com OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * XProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function XProvider(config: XProviderConfig) { 37 | return Oauth2Provider({ 38 | ...config, 39 | type: "x", 40 | endpoint: { 41 | authorization: "https://twitter.com/i/oauth2/authorize", 42 | token: "https://api.x.com/2/oauth2/token", 43 | }, 44 | pkce: true, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /packages/openauth/src/provider/yahoo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this provider to authenticate with Yahoo. 3 | * 4 | * ```ts {5-8} 5 | * import { YahooProvider } from "@openauthjs/openauth/provider/yahoo" 6 | * 7 | * export default issuer({ 8 | * providers: { 9 | * yahoo: YahooProvider({ 10 | * clientID: "1234567890", 11 | * clientSecret: "0987654321" 12 | * }) 13 | * } 14 | * }) 15 | * ``` 16 | * 17 | * @packageDocumentation 18 | */ 19 | 20 | import { Oauth2Provider, Oauth2WrappedConfig } from "./oauth2.js" 21 | 22 | export interface YahooConfig extends Oauth2WrappedConfig {} 23 | 24 | /** 25 | * Create a Yahoo OAuth2 provider. 26 | * 27 | * @param config - The config for the provider. 28 | * @example 29 | * ```ts 30 | * YahooProvider({ 31 | * clientID: "1234567890", 32 | * clientSecret: "0987654321" 33 | * }) 34 | * ``` 35 | */ 36 | export function YahooProvider(config: YahooConfig) { 37 | return Oauth2Provider({ 38 | ...config, 39 | type: "yahoo", 40 | endpoint: { 41 | authorization: "https://api.login.yahoo.com/oauth2/request_auth", 42 | token: "https://api.login.yahoo.com/oauth2/get_token", 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/openauth/src/random.ts: -------------------------------------------------------------------------------- 1 | import { timingSafeEqual } from "node:crypto" 2 | 3 | export function generateUnbiasedDigits(length: number): string { 4 | const result: number[] = [] 5 | while (result.length < length) { 6 | const buffer = crypto.getRandomValues(new Uint8Array(length * 2)) 7 | for (const byte of buffer) { 8 | if (byte < 250 && result.length < length) { 9 | result.push(byte % 10) 10 | } 11 | } 12 | } 13 | return result.join("") 14 | } 15 | 16 | export function timingSafeCompare(a: string, b: string): boolean { 17 | if (typeof a !== "string" || typeof b !== "string") { 18 | return false 19 | } 20 | if (a.length !== b.length) { 21 | return false 22 | } 23 | return timingSafeEqual(Buffer.from(a), Buffer.from(b)) 24 | } 25 | -------------------------------------------------------------------------------- /packages/openauth/src/storage/aws.ts: -------------------------------------------------------------------------------- 1 | import { AwsClient } from "aws4fetch" 2 | 3 | interface EC2Credentials { 4 | AccessKeyId: string 5 | SecretAccessKey: string 6 | Token: string 7 | Expiration: string 8 | Type: string 9 | } 10 | 11 | let cachedCredentials: EC2Credentials | null = null 12 | 13 | async function getCredentials(url: string): Promise { 14 | if (cachedCredentials) { 15 | const currentTime = new Date() 16 | const fiveMinutesFromNow = new Date(currentTime.getTime() + 5 * 60000) 17 | const expirationTime = new Date(cachedCredentials.Expiration) 18 | if (expirationTime > fiveMinutesFromNow) { 19 | return cachedCredentials 20 | } 21 | } 22 | 23 | const credentials = (await fetch(url).then((res) => 24 | res.json(), 25 | )) as EC2Credentials 26 | cachedCredentials = credentials 27 | return credentials 28 | } 29 | 30 | export async function client(): Promise { 31 | if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { 32 | return new AwsClient({ 33 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 34 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 35 | sessionToken: process.env.AWS_SESSION_TOKEN, 36 | region: process.env.AWS_REGION, 37 | }) 38 | } 39 | 40 | if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) { 41 | const credentials = await getCredentials( 42 | "http://169.254.170.2" + 43 | process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, 44 | ) 45 | return new AwsClient({ 46 | accessKeyId: credentials.AccessKeyId, 47 | secretAccessKey: credentials.SecretAccessKey, 48 | sessionToken: credentials.Token, 49 | region: process.env.AWS_REGION, 50 | }) 51 | } 52 | 53 | throw new Error("No AWS credentials found") 54 | } 55 | 56 | export type AwsOptions = Exclude< 57 | Parameters[1], 58 | null | undefined 59 | >["aws"] 60 | -------------------------------------------------------------------------------- /packages/openauth/src/storage/cloudflare.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure OpenAuth to use [Cloudflare KV](https://developers.cloudflare.com/kv/) as a 3 | * storage adapter. 4 | * 5 | * ```ts 6 | * import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare" 7 | * 8 | * const storage = CloudflareStorage({ 9 | * namespace: "my-namespace" 10 | * }) 11 | * 12 | * 13 | * export default issuer({ 14 | * storage, 15 | * // ... 16 | * }) 17 | * ``` 18 | * 19 | * @packageDocumentation 20 | */ 21 | import type { KVNamespace } from "@cloudflare/workers-types" 22 | import { joinKey, splitKey, StorageAdapter } from "./storage.js" 23 | 24 | /** 25 | * Configure the Cloudflare KV store that's created. 26 | */ 27 | export interface CloudflareStorageOptions { 28 | namespace: KVNamespace 29 | } 30 | /** 31 | * Creates a Cloudflare KV store. 32 | * @param options - The config for the adapter. 33 | */ 34 | export function CloudflareStorage( 35 | options: CloudflareStorageOptions, 36 | ): StorageAdapter { 37 | return { 38 | async get(key: string[]) { 39 | const value = await options.namespace.get(joinKey(key), "json") 40 | if (!value) return 41 | return value as Record 42 | }, 43 | 44 | async set(key: string[], value: any, expiry?: Date) { 45 | await options.namespace.put(joinKey(key), JSON.stringify(value), { 46 | expirationTtl: expiry 47 | ? Math.max(Math.floor((expiry.getTime() - Date.now()) / 1000), 60) 48 | : undefined, 49 | }) 50 | }, 51 | 52 | async remove(key: string[]) { 53 | await options.namespace.delete(joinKey(key)) 54 | }, 55 | 56 | async *scan(prefix: string[]) { 57 | let cursor: string | undefined 58 | while (true) { 59 | const result = await options.namespace.list({ 60 | prefix: joinKey([...prefix, ""]), 61 | cursor, 62 | }) 63 | 64 | for (const key of result.keys) { 65 | const value = await options.namespace.get(key.name, "json") 66 | if (value !== null) { 67 | yield [splitKey(key.name), value] 68 | } 69 | } 70 | if (result.list_complete) { 71 | break 72 | } 73 | cursor = result.cursor 74 | } 75 | }, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/openauth/src/storage/memory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure OpenAuth to use a simple in-memory store. 3 | * 4 | * :::caution 5 | * This is not meant to be used in production. 6 | * ::: 7 | * 8 | * This is useful for testing and development. It's not meant to be used in production. 9 | * 10 | * ```ts 11 | * import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 12 | * 13 | * const storage = MemoryStorage() 14 | * 15 | * export default issuer({ 16 | * storage, 17 | * // ... 18 | * }) 19 | * ``` 20 | * 21 | * Optionally, you can persist the store to a file. 22 | * 23 | * ```ts 24 | * MemoryStorage({ 25 | * persist: "./persist.json" 26 | * }) 27 | * ``` 28 | * 29 | * @packageDocumentation 30 | */ 31 | import { joinKey, splitKey, StorageAdapter } from "./storage.js" 32 | import { existsSync, readFileSync } from "node:fs" 33 | import { writeFile } from "node:fs/promises" 34 | 35 | /** 36 | * Configure the memory store. 37 | */ 38 | export interface MemoryStorageOptions { 39 | /** 40 | * Optionally, backup the store to a file. So it'll be persisted when the issuer restarts. 41 | * 42 | * @example 43 | * ```ts 44 | * { 45 | * persist: "./persist.json" 46 | * } 47 | * ``` 48 | */ 49 | persist?: string 50 | } 51 | export function MemoryStorage(input?: MemoryStorageOptions): StorageAdapter { 52 | const store = [] as [ 53 | string, 54 | { value: Record; expiry?: number }, 55 | ][] 56 | 57 | if (input?.persist) { 58 | if (existsSync(input.persist)) { 59 | const file = readFileSync(input?.persist) 60 | store.push(...JSON.parse(file.toString())) 61 | } 62 | } 63 | 64 | async function save() { 65 | if (!input?.persist) return 66 | const file = JSON.stringify(store) 67 | await writeFile(input.persist, file) 68 | } 69 | 70 | function search(key: string) { 71 | let left = 0 72 | let right = store.length - 1 73 | while (left <= right) { 74 | const mid = Math.floor((left + right) / 2) 75 | const comparison = key.localeCompare(store[mid][0]) 76 | 77 | if (comparison === 0) { 78 | return { found: true, index: mid } 79 | } else if (comparison < 0) { 80 | right = mid - 1 81 | } else { 82 | left = mid + 1 83 | } 84 | } 85 | return { found: false, index: left } 86 | } 87 | return { 88 | async get(key: string[]) { 89 | const match = search(joinKey(key)) 90 | if (!match.found) return undefined 91 | const entry = store[match.index][1] 92 | if (entry.expiry && Date.now() >= entry.expiry) { 93 | store.splice(match.index, 1) 94 | await save() 95 | return undefined 96 | } 97 | return entry.value 98 | }, 99 | async set(key: string[], value: any, expiry?: Date) { 100 | const joined = joinKey(key) 101 | const match = search(joined) 102 | // Handle both Date objects and TTL numbers while maintaining Date type in signature 103 | const entry = [ 104 | joined, 105 | { 106 | value, 107 | expiry: expiry ? expiry.getTime() : expiry, 108 | }, 109 | ] as (typeof store)[number] 110 | if (!match.found) { 111 | store.splice(match.index, 0, entry) 112 | } else { 113 | store[match.index] = entry 114 | } 115 | await save() 116 | }, 117 | async remove(key: string[]) { 118 | const joined = joinKey(key) 119 | const match = search(joined) 120 | if (match.found) { 121 | store.splice(match.index, 1) 122 | await save() 123 | } 124 | }, 125 | async *scan(prefix: string[]) { 126 | const now = Date.now() 127 | const prefixStr = joinKey(prefix) 128 | for (const [key, entry] of store) { 129 | if (!key.startsWith(prefixStr)) continue 130 | if (entry.expiry && now >= entry.expiry) continue 131 | yield [splitKey(key), entry.value] 132 | } 133 | }, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/openauth/src/storage/storage.ts: -------------------------------------------------------------------------------- 1 | export interface StorageAdapter { 2 | get(key: string[]): Promise | undefined> 3 | remove(key: string[]): Promise 4 | set(key: string[], value: any, expiry?: Date): Promise 5 | scan(prefix: string[]): AsyncIterable<[string[], any]> 6 | } 7 | 8 | const SEPERATOR = String.fromCharCode(0x1f) 9 | 10 | export function joinKey(key: string[]) { 11 | return key.join(SEPERATOR) 12 | } 13 | 14 | export function splitKey(key: string) { 15 | return key.split(SEPERATOR) 16 | } 17 | 18 | export namespace Storage { 19 | function encode(key: string[]) { 20 | return key.map((k) => k.replaceAll(SEPERATOR, "")) 21 | } 22 | export function get(adapter: StorageAdapter, key: string[]) { 23 | return adapter.get(encode(key)) as Promise 24 | } 25 | 26 | export function set( 27 | adapter: StorageAdapter, 28 | key: string[], 29 | value: any, 30 | ttl?: number, 31 | ) { 32 | const expiry = ttl ? new Date(Date.now() + ttl * 1000) : undefined 33 | return adapter.set(encode(key), value, expiry) 34 | } 35 | 36 | export function remove(adapter: StorageAdapter, key: string[]) { 37 | return adapter.remove(encode(key)) 38 | } 39 | 40 | export function scan( 41 | adapter: StorageAdapter, 42 | key: string[], 43 | ): AsyncIterable<[string[], T]> { 44 | return adapter.scan(encode(key)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/openauth/src/subject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subjects are what the access token generated at the end of the auth flow will map to. Under 3 | * the hood, the access token is a JWT that contains this data. 4 | * 5 | * #### Define subjects 6 | * 7 | * ```ts title="subjects.ts" 8 | * import { object, string } from "valibot" 9 | * 10 | * const subjects = createSubjects({ 11 | * user: object({ 12 | * userID: string() 13 | * }) 14 | * }) 15 | * ``` 16 | * 17 | * We are using [valibot](https://github.com/fabian-hiller/valibot) here. You can use any 18 | * validation library that's following the 19 | * [standard-schema specification](https://github.com/standard-schema/standard-schema). 20 | * 21 | * :::tip 22 | * You typically want to place subjects in its own file so it can be imported by all of your apps. 23 | * ::: 24 | * 25 | * You can start with one subject. Later you can add more for different types of users. 26 | * 27 | * #### Set the subjects 28 | * 29 | * Then you can pass it to the `issuer`. 30 | * 31 | * ```ts title="issuer.ts" 32 | * import { subjects } from "./subjects" 33 | * 34 | * const app = issuer({ 35 | * providers: { ... }, 36 | * subjects, 37 | * // ... 38 | * }) 39 | * ``` 40 | * 41 | * #### Add the subject payload 42 | * 43 | * When your user completes the flow, you can add the subject payload in the `success` callback. 44 | * 45 | * ```ts title="issuer.ts" 46 | * const app = issuer({ 47 | * providers: { ... }, 48 | * subjects, 49 | * async success(ctx, value) { 50 | * let userID 51 | * if (value.provider === "password") { 52 | * console.log(value.email) 53 | * userID = ... // lookup user or create them 54 | * } 55 | * return ctx.subject("user", { 56 | * userID 57 | * }) 58 | * }, 59 | * // ... 60 | * }) 61 | * ``` 62 | * 63 | * Here we are looking up the userID from our database and adding it to the subject payload. 64 | * 65 | * :::caution 66 | * You should only store properties that won't change for the lifetime of the user. 67 | * ::: 68 | * 69 | * Since these will be stored in the access token, you should avoid storing information 70 | * that'll change often. For example, if you store the user's username, you'll need to 71 | * revoke the access token when the user changes their username. 72 | * 73 | * #### Decode the subject 74 | * 75 | * Now when your user logs in, you can use the OpenAuth client to decode the subject. For 76 | * example, in our SSR app we can do the following. 77 | * 78 | * ```ts title="app/page.tsx" 79 | * import { subjects } from "../subjects" 80 | * 81 | * const verified = await client.verify(subjects, cookies.get("access_token")!) 82 | * console.log(verified.subject.properties.userID) 83 | * ``` 84 | * 85 | * All this is typesafe based on the shape of the subjects you defined. 86 | * 87 | * @packageDocumentation 88 | */ 89 | import type { v1 } from "@standard-schema/spec" 90 | import { Prettify } from "./util.js" 91 | 92 | /** 93 | * Subject schema is a map of types that are used to define the subjects. 94 | */ 95 | export type SubjectSchema = Record 96 | 97 | /** @internal */ 98 | export type SubjectPayload = Prettify< 99 | { 100 | [type in keyof T & string]: { 101 | type: type 102 | properties: v1.InferOutput 103 | } 104 | }[keyof T & string] 105 | > 106 | 107 | /** 108 | * Create a subject schema. 109 | * 110 | * @example 111 | * ```ts 112 | * const subjects = createSubjects({ 113 | * user: object({ 114 | * userID: string() 115 | * }), 116 | * admin: object({ 117 | * workspaceID: string() 118 | * }) 119 | * }) 120 | * ``` 121 | * 122 | * This is using [valibot](https://github.com/fabian-hiller/valibot) to define the shape of the 123 | * subjects. You can use any validation library that's following the 124 | * [standard-schema specification](https://github.com/standard-schema/standard-schema). 125 | */ 126 | export function createSubjects( 127 | types: Schema, 128 | ): Schema { 129 | return { ...types } 130 | } 131 | -------------------------------------------------------------------------------- /packages/openauth/src/ui/form.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource hono/jsx */ 2 | 3 | export function FormAlert(props: { 4 | message?: string 5 | color?: "danger" | "success" 6 | }) { 7 | return ( 8 |
9 | 17 | 22 | 23 | 31 | 36 | 37 | {props.message} 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/openauth/src/util.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "hono" 2 | 3 | export type Prettify = { 4 | [K in keyof T]: T[K] 5 | } 6 | 7 | export function getRelativeUrl(ctx: Context, path: string) { 8 | const result = new URL(path, ctx.req.url) 9 | result.host = ctx.req.header("x-forwarded-host") || result.host 10 | result.protocol = ctx.req.header("x-forwarded-proto") || result.protocol 11 | result.port = ctx.req.header("x-forwarded-port") || result.port 12 | return result.toString() 13 | } 14 | 15 | const twoPartTlds = [ 16 | "co.uk", 17 | "co.jp", 18 | "co.kr", 19 | "co.nz", 20 | "co.za", 21 | "co.in", 22 | "com.au", 23 | "com.br", 24 | "com.cn", 25 | "com.mx", 26 | "com.tw", 27 | "net.au", 28 | "org.uk", 29 | "ne.jp", 30 | "ac.uk", 31 | "gov.uk", 32 | "edu.au", 33 | "gov.au", 34 | ] 35 | 36 | export function isDomainMatch(a: string, b: string): boolean { 37 | if (a === b) return true 38 | const partsA = a.split(".") 39 | const partsB = b.split(".") 40 | const hasTwoPartTld = twoPartTlds.some( 41 | (tld) => a.endsWith("." + tld) || b.endsWith("." + tld), 42 | ) 43 | const numParts = hasTwoPartTld ? -3 : -2 44 | const min = Math.min(partsA.length, partsB.length, numParts) 45 | const tailA = partsA.slice(min).join(".") 46 | const tailB = partsB.slice(min).join(".") 47 | return tailA === tailB 48 | } 49 | 50 | export function lazy(fn: () => T): () => T { 51 | let value: T | undefined 52 | return () => { 53 | if (value === undefined) { 54 | value = fn() 55 | } 56 | return value 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/openauth/test/scrap.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "bun:test" 2 | import { issuer } from "../src/issuer.js" 3 | import { MemoryStorage } from "../src/storage/memory.js" 4 | import { object, string } from "valibot" 5 | import { createSubjects } from "../src/subject.js" 6 | import { createClient } from "../src/client.js" 7 | 8 | const subjects = createSubjects({ 9 | user: object({ 10 | userID: string(), 11 | }), 12 | }) 13 | 14 | const auth = issuer({ 15 | storage: MemoryStorage(), 16 | subjects, 17 | allow: async () => true, 18 | success: async (ctx) => { 19 | return ctx.subject("user", { 20 | userID: "123", 21 | }) 22 | }, 23 | ttl: { 24 | access: 1, 25 | }, 26 | providers: { 27 | dummy: { 28 | type: "dummy", 29 | init(route, ctx) { 30 | route.get("/authorize", async (c) => { 31 | return ctx.success(c, { 32 | email: "foo@bar.com", 33 | }) 34 | }) 35 | }, 36 | }, 37 | }, 38 | }) 39 | 40 | test("code flow", async () => { 41 | const client = createClient({ 42 | issuer: "https://auth.example.com", 43 | clientID: "123", 44 | fetch: (a, b) => Promise.resolve(auth.request(a, b)), 45 | }) 46 | const [verifier, authorization] = await client.pkce( 47 | "https://client.example.com/callback", 48 | ) 49 | let response = await auth.request(authorization) 50 | expect(response.status).toBe(302) 51 | response = await auth.request(response.headers.get("location")!, { 52 | headers: { 53 | cookie: response.headers.get("set-cookie")!, 54 | }, 55 | }) 56 | expect(response.status).toBe(302) 57 | const location = new URL(response.headers.get("location")!) 58 | const code = location.searchParams.get("code") 59 | expect(code).not.toBeNull() 60 | const exchanged = await client.exchange( 61 | code!, 62 | "https://client.example.com/callback", 63 | verifier, 64 | ) 65 | if (exchanged.err) throw exchanged.err 66 | expect(exchanged.tokens.access).toBeTruthy() 67 | expect(exchanged.tokens.refresh).toBeTruthy() 68 | const verified = await client.verify(subjects, exchanged.tokens.access) 69 | if (verified.err) throw verified.err 70 | expect(verified.subject.type).toBe("user") 71 | if (verified.subject.type !== "user") throw new Error("Invalid subject") 72 | expect(verified.subject.properties.userID).toBe("123") 73 | await new Promise((resolve) => setTimeout(resolve, 2000)) 74 | const failed = await client.verify(subjects, exchanged.tokens.access) 75 | expect(failed.err).toBeInstanceOf(Error) 76 | const next = await client.verify(subjects, exchanged.tokens.access, { 77 | refresh: exchanged.tokens.refresh, 78 | }) 79 | if (next.err) throw next.err 80 | expect(next.tokens?.access).toBeDefined() 81 | expect(next.tokens?.refresh).toBeDefined() 82 | expect(next.tokens?.access).not.toEqual(exchanged.tokens.access) 83 | expect(next.tokens?.refresh).not.toEqual(exchanged.tokens.refresh) 84 | await client.verify(subjects, next.tokens!.access!) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/openauth/test/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, setSystemTime } from "bun:test" 2 | import { beforeEach, describe, expect, test } from "bun:test" 3 | import { MemoryStorage } from "../src/storage/memory.js" 4 | 5 | let storage = MemoryStorage() 6 | 7 | beforeEach(async () => { 8 | storage = MemoryStorage() 9 | setSystemTime(new Date("1/1/2024")) 10 | }) 11 | 12 | afterEach(() => { 13 | setSystemTime() 14 | }) 15 | 16 | describe("set", () => { 17 | test("basic", async () => { 18 | await storage.set(["users", "123"], { name: "Test User" }) 19 | const result = await storage.get(["users", "123"]) 20 | expect(result).toEqual({ name: "Test User" }) 21 | }) 22 | 23 | test("ttl", async () => { 24 | await storage.set( 25 | ["temp", "key"], 26 | { value: "value" }, 27 | new Date(Date.now() + 100), 28 | ) // 100ms TTL 29 | let result = await storage.get(["temp", "key"]) 30 | expect(result?.value).toBe("value") 31 | 32 | setSystemTime(Date.now() + 150) 33 | result = await storage.get(["temp", "key"]) 34 | expect(result).toBeUndefined() 35 | }) 36 | 37 | test("nested", async () => { 38 | const complexObj = { 39 | id: 1, 40 | nested: { a: 1, b: { c: 2 } }, 41 | array: [1, 2, 3], 42 | } 43 | await storage.set(["complex"], complexObj) 44 | const result = await storage.get(["complex"]) 45 | expect(result).toEqual(complexObj) 46 | }) 47 | }) 48 | 49 | describe("get", () => { 50 | test("missing", async () => { 51 | const result = await storage.get(["nonexistent"]) 52 | expect(result).toBeUndefined() 53 | }) 54 | 55 | test("key", async () => { 56 | await storage.set(["a", "b", "c"], { value: "nested" }) 57 | const result = await storage.get(["a", "b", "c"]) 58 | expect(result?.value).toBe("nested") 59 | }) 60 | }) 61 | 62 | describe("remove", () => { 63 | test("existing", async () => { 64 | await storage.set(["test"], "value") 65 | await storage.remove(["test"]) 66 | const result = await storage.get(["test"]) 67 | expect(result).toBeUndefined() 68 | }) 69 | 70 | test("missing", async () => { 71 | expect(storage.remove(["nonexistent"])).resolves.toBeUndefined() 72 | }) 73 | }) 74 | 75 | describe("scan", () => { 76 | test("all", async () => { 77 | await storage.set(["users", "1"], { id: 1 }) 78 | await storage.set(["users", "2"], { id: 2 }) 79 | await storage.set(["other"], { id: 3 }) 80 | const results = await Array.fromAsync(storage.scan(["users"])) 81 | expect(results).toHaveLength(2) 82 | expect(results).toContainEqual([["users", "1"], { id: 1 }]) 83 | expect(results).toContainEqual([["users", "2"], { id: 2 }]) 84 | }) 85 | 86 | test("ttl", async () => { 87 | await storage.set(["temp", "1"], "a", new Date(Date.now() + 100)) 88 | await storage.set(["temp", "2"], "b", new Date(Date.now() + 100)) 89 | await storage.set(["temp", "3"], "c") 90 | expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(3) 91 | setSystemTime(Date.now() + 150) 92 | expect(await Array.fromAsync(storage.scan(["temp"]))).toHaveLength(1) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /packages/openauth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true, 6 | "skipLibCheck": true, 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "hono/jsx" 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | bun x prettier --write "**/*.{js,jsx,ts,tsx,json,md,yaml,yml}" 6 | 7 | # Check for changes 8 | if ! git diff --quiet HEAD; then 9 | git add -A 10 | git commit -m "auto: format code" 11 | git push origin HEAD 12 | echo "pushed changes" 13 | else 14 | echo "no changes" 15 | fi 16 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | # doc output 6 | output/ 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /www/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /www/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) 12 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) 13 | 14 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 15 | 16 | ## 🚀 Project Structure 17 | 18 | Inside of your Astro + Starlight project, you'll see the following folders and files: 19 | 20 | ``` 21 | . 22 | ├── public/ 23 | ├── src/ 24 | │ ├── assets/ 25 | │ ├── content/ 26 | │ │ ├── docs/ 27 | │ │ └── config.ts 28 | │ └── env.d.ts 29 | ├── astro.config.mjs 30 | ├── package.json 31 | └── tsconfig.json 32 | ``` 33 | 34 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 35 | 36 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 37 | 38 | Static assets, like favicons, can be placed in the `public/` directory. 39 | 40 | ## 🧞 Commands 41 | 42 | All commands are run from the root of the project, from a terminal: 43 | 44 | | Command | Action | 45 | | :------------------------ | :----------------------------------------------- | 46 | | `npm install` | Installs dependencies | 47 | | `npm run dev` | Starts local dev server at `localhost:4321` | 48 | | `npm run build` | Build your production site to `./dist/` | 49 | | `npm run preview` | Preview your build locally, before deploying | 50 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 51 | | `npm run astro -- --help` | Get help using the Astro CLI | 52 | 53 | ## 👀 Want to learn more? 54 | 55 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 56 | -------------------------------------------------------------------------------- /www/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/bun.lockb -------------------------------------------------------------------------------- /www/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | github: "https://github.com/toolbeam/openauth", 3 | discord: "https://sst.dev/discord", 4 | } 5 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "bun generate.ts && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/markdown-remark": "^6.0.1", 14 | "@astrojs/starlight": "^0.32.0", 15 | "@fontsource/ibm-plex-mono": "^5.1.0", 16 | "astro": "^5.1.5", 17 | "rehype-autolink-headings": "^7.1.0", 18 | "sharp": "^0.33.4", 19 | "toolbeam-docs-theme": "^0.0.3" 20 | }, 21 | "devDependencies": { 22 | "typedoc": "^0.27.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /www/public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/public/social-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/public/social-share.png -------------------------------------------------------------------------------- /www/src/components/Hero.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Default from '@astrojs/starlight/components/Hero.astro'; 3 | import Lander from './Lander.astro'; 4 | 5 | const { slug } = Astro.locals.starlightRoute.entry; 6 | --- 7 | 8 | { slug === "" 9 | ? 10 | : 11 | } 12 | -------------------------------------------------------------------------------- /www/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content" 2 | import { docsSchema } from "@astrojs/starlight/schema" 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | } 7 | -------------------------------------------------------------------------------- /www/src/content/docs/docs/provider/oidc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OidcProvider 3 | editUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/provider/oidc.ts 4 | description: Reference doc for the `OidcProvider`. 5 | --- 6 | 7 | import { Segment, Section, NestedTitle, InlineSection } from 'toolbeam-docs-theme/components' 8 | import { Tabs, TabItem } from '@astrojs/starlight/components' 9 | 10 |
11 |
12 | Use this to connect authentication providers that support OIDC. 13 | 14 | ```ts {5-8} 15 | import { OidcProvider } from "@openauthjs/openauth/provider/oidc" 16 | 17 | export default issuer({ 18 | providers: { 19 | oauth2: OidcProvider({ 20 | clientId: "1234567890", 21 | issuer: "https://auth.myserver.com" 22 | }) 23 | } 24 | }) 25 | ``` 26 |
27 | --- 28 | ## Methods 29 | ### OidcProvider 30 | 31 |
32 | ```ts 33 | OidcProvider(config) 34 | ``` 35 |
36 |
37 | #### Parameters 38 | -

config [OidcConfig](/docs/provider/oidc#oidcconfig)

39 |
40 | 41 | **Returns** Provider 42 | 43 |
44 | ## OidcConfig 45 | 46 |
47 | -

[clientID](#oidcconfig.clientid) string

48 | -

[issuer](#oidcconfig.issuer) string

49 | -

[query?](#oidcconfig.query) Record<string, string>

50 | -

[scopes?](#oidcconfig.scopes) string[]

51 |
52 |
53 | clientID 54 | 55 |
56 | 57 | **Type** string 58 | 59 |
60 | The client ID. 61 | 62 | This is just a string to identify your app. 63 | ```ts 64 | { 65 | clientID: "my-client" 66 | } 67 | ``` 68 |
69 | issuer 70 | 71 |
72 | 73 | **Type** string 74 | 75 |
76 | The URL of your authorization server. 77 | ```ts 78 | { 79 | issuer: "https://auth.myserver.com" 80 | } 81 | ``` 82 |
83 | query? 84 | 85 |
86 | 87 | **Type** Record<string, string> 88 | 89 |
90 | Any additional parameters that you want to pass to the authorization endpoint. 91 | ```ts 92 | { 93 | query: { 94 | prompt: "consent" 95 | } 96 | } 97 | ``` 98 |
99 | scopes? 100 | 101 |
102 | 103 | **Type** string[] 104 | 105 |
106 | A list of OIDC scopes that you want to request. 107 | ```ts 108 | { 109 | scopes: ["openid", "profile", "email"] 110 | } 111 | ``` 112 |
113 |
-------------------------------------------------------------------------------- /www/src/content/docs/docs/start/nextjs-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/src/content/docs/docs/start/nextjs-dark.png -------------------------------------------------------------------------------- /www/src/content/docs/docs/start/nextjs-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/src/content/docs/docs/start/nextjs-light.png -------------------------------------------------------------------------------- /www/src/content/docs/docs/storage/cloudflare.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloudflare KV 3 | editUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/storage/cloudflare.ts 4 | description: Reference doc for the Cloudflare KV storage adapter. 5 | --- 6 | 7 | import { Segment, Section, NestedTitle, InlineSection } from 'toolbeam-docs-theme/components' 8 | import { Tabs, TabItem } from '@astrojs/starlight/components' 9 | 10 |
11 |
12 | Configure OpenAuth to use [Cloudflare KV](https://developers.cloudflare.com/kv/) as a 13 | storage adapter. 14 | 15 | ```ts 16 | import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare" 17 | 18 | const storage = CloudflareStorage({ 19 | namespace: "my-namespace" 20 | }) 21 | 22 | 23 | export default issuer({ 24 | storage, 25 | // ... 26 | }) 27 | ``` 28 |
29 | --- 30 | ## Methods 31 | ### CloudflareStorage 32 | 33 |
34 | ```ts 35 | CloudflareStorage(options) 36 | ``` 37 |
38 |
39 | #### Parameters 40 | -

options [CloudflareStorageOptions](#cloudflarestorageoptions)

41 | The config for the adapter. 42 |
43 | 44 | **Returns** StorageAdapter 45 | 46 | Creates a Cloudflare KV store. 47 |
48 | ## CloudflareStorageOptions 49 | 50 |
51 | -

[namespace](#cloudflarestorageoptions.namespace) KVNamespace

52 |
53 | Configure the Cloudflare KV store that's created. 54 |
55 | namespace 56 | 57 |
58 | 59 | **Type** KVNamespace 60 | 61 |
62 |
63 |
-------------------------------------------------------------------------------- /www/src/content/docs/docs/storage/dynamo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: DynamoDB 3 | editUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/storage/dynamo.ts 4 | description: Reference doc for the DynamoDB storage adapter. 5 | --- 6 | 7 | import { Segment, Section, NestedTitle, InlineSection } from 'toolbeam-docs-theme/components' 8 | import { Tabs, TabItem } from '@astrojs/starlight/components' 9 | 10 |
11 |
12 | Configure OpenAuth to use [DynamoDB](https://aws.amazon.com/dynamodb/) as a storage adapter. 13 | 14 | ```ts 15 | import { DynamoStorage } from "@openauthjs/openauth/storage/dynamo" 16 | 17 | const storage = DynamoStorage({ 18 | table: "my-table", 19 | pk: "pk", 20 | sk: "sk" 21 | }) 22 | 23 | export default issuer({ 24 | storage, 25 | // ... 26 | }) 27 | ``` 28 |
29 | --- 30 | ## Methods 31 | ### DynamoStorage 32 | 33 |
34 | ```ts 35 | DynamoStorage(options) 36 | ``` 37 |
38 |
39 | #### Parameters 40 | -

options [DynamoStorageOptions](#dynamostorageoptions)

41 | The config for the adapter. 42 |
43 | 44 | **Returns** StorageAdapter 45 | 46 | Creates a DynamoDB store. 47 |
48 | ## DynamoStorageOptions 49 | 50 |
51 | -

[endpoint?](#dynamostorageoptions.endpoint) string

52 | -

[pk?](#dynamostorageoptions.pk) string

53 | -

[sk?](#dynamostorageoptions.sk) string

54 | -

[table](#dynamostorageoptions.table) string

55 | -

[ttl?](#dynamostorageoptions.ttl) string

56 |
57 | Configure the DynamoDB table that's created. 58 | ```ts 59 | { 60 | table: "my-table", 61 | pk: "pk", 62 | sk: "sk" 63 | } 64 | ``` 65 |
66 | endpoint? 67 | 68 |
69 | 70 | **Type** string 71 | 72 |
73 | 74 | 75 | **Default** "https://dynamodb.{region}.amazonaws.com" 76 | 77 | Endpoint URL for the DynamoDB service. Useful for local testing. 78 |
79 | pk? 80 | 81 |
82 | 83 | **Type** string 84 | 85 |
86 | 87 | 88 | **Default** "pk" 89 | 90 | The primary key column name. 91 |
92 | sk? 93 | 94 |
95 | 96 | **Type** string 97 | 98 |
99 | 100 | 101 | **Default** "sk" 102 | 103 | The sort key column name. 104 |
105 | table 106 | 107 |
108 | 109 | **Type** string 110 | 111 |
112 | The name of the DynamoDB table. 113 |
114 | ttl? 115 | 116 |
117 | 118 | **Type** string 119 | 120 |
121 | 122 | 123 | **Default** "expiry" 124 | 125 | The name of the time to live attribute. 126 |
127 |
-------------------------------------------------------------------------------- /www/src/content/docs/docs/storage/memory.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Memory 3 | editUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/storage/memory.ts 4 | description: Reference doc for the Memory storage adapter. 5 | --- 6 | 7 | import { Segment, Section, NestedTitle, InlineSection } from 'toolbeam-docs-theme/components' 8 | import { Tabs, TabItem } from '@astrojs/starlight/components' 9 | 10 |
11 |
12 | Configure OpenAuth to use a simple in-memory store. 13 | 14 | :::caution 15 | This is not meant to be used in production. 16 | ::: 17 | 18 | This is useful for testing and development. It's not meant to be used in production. 19 | 20 | ```ts 21 | import { MemoryStorage } from "@openauthjs/openauth/storage/memory" 22 | 23 | const storage = MemoryStorage() 24 | 25 | export default issuer({ 26 | storage, 27 | // ... 28 | }) 29 | ``` 30 | 31 | Optionally, you can persist the store to a file. 32 | 33 | ```ts 34 | MemoryStorage({ 35 | persist: "./persist.json" 36 | }) 37 | ``` 38 |
39 | --- 40 | ## Methods 41 | ### MemoryStorage 42 | 43 |
44 | ```ts 45 | MemoryStorage(input?) 46 | ``` 47 |
48 |
49 | #### Parameters 50 | -

input? [MemoryStorageOptions](#memorystorageoptions)

51 |
52 | 53 | **Returns** StorageAdapter 54 | 55 |
56 | ## MemoryStorageOptions 57 | 58 |
59 | -

[persist?](#memorystorageoptions.persist) string

60 |
61 | Configure the memory store. 62 |
63 | persist? 64 | 65 |
66 | 67 | **Type** string 68 | 69 |
70 | Optionally, backup the store to a file. So it'll be persisted when the issuer restarts. 71 | ```ts 72 | { 73 | persist: "./persist.json" 74 | } 75 | ``` 76 |
77 |
-------------------------------------------------------------------------------- /www/src/content/docs/docs/themes-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/src/content/docs/docs/themes-dark.png -------------------------------------------------------------------------------- /www/src/content/docs/docs/themes-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/src/content/docs/docs/themes-light.png -------------------------------------------------------------------------------- /www/src/content/docs/docs/ui/select.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Select 3 | editUrl: https://github.com/toolbeam/openauth/blob/master/packages/openauth/src/ui/select.tsx 4 | description: Reference doc for the `Select` UI. 5 | --- 6 | 7 | import { Segment, Section, NestedTitle, InlineSection } from 'toolbeam-docs-theme/components' 8 | import { Tabs, TabItem } from '@astrojs/starlight/components' 9 | 10 |
11 |
12 | The UI that's displayed when loading the root page of the OpenAuth server. You can configure 13 | which providers should be displayed in the select UI. 14 | 15 | ```ts 16 | import { Select } from "@openauthjs/openauth/ui/select" 17 | 18 | export default issuer({ 19 | select: Select({ 20 | providers: { 21 | github: { 22 | hide: true 23 | }, 24 | google: { 25 | display: "Google" 26 | } 27 | } 28 | }) 29 | // ... 30 | }) 31 | ``` 32 |
33 | --- 34 | ## Methods 35 | ### Select 36 | 37 |
38 | ```ts 39 | Select(props?) 40 | ``` 41 |
42 |
43 | #### Parameters 44 | -

props? [SelectProps](#selectprops)

45 |
46 | 47 | **Returns** (providers: Record<string, string>, _req: Request) => Promise<Response> 48 | 49 |
50 | ## SelectProps 51 | 52 |
53 | -

[providers?](#selectprops.providers) Record<string, Object>

54 | -

[display?](#providers[].display) string

55 | -

[hide?](#providers[].hide) boolean

56 |
57 |
58 | providers? 59 | 60 |
61 | 62 | **Type** Record<string, Object> 63 | 64 |
65 | An object with all the providers and their config; where the key is the provider name. 66 | ```ts 67 | { 68 | github: { 69 | hide: true 70 | }, 71 | google: { 72 | display: "Google" 73 | } 74 | } 75 | ``` 76 |
77 | display? 78 | 79 |
80 | 81 | **Type** string 82 | 83 |
84 | The display name of the provider. 85 |
86 | hide? 87 | 88 |
89 | 90 | **Type** boolean 91 | 92 |
93 | 94 | 95 | **Default** false 96 | 97 | Whether to hide the provider from the select UI. 98 |
99 |
-------------------------------------------------------------------------------- /www/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: OpenAuth 3 | description: Universal, standards-based auth provider. 4 | template: splash 5 | hero: 6 | title: Universal, standards-based auth 7 | tagline: A universal, standards-based auth provider. 8 | image: 9 | dark: ../../assets/logo-dark.svg 10 | light: ../../assets/logo-light.svg 11 | alt: OpenAuth logo 12 | --- 13 | -------------------------------------------------------------------------------- /www/src/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolbeam/openauth/98dc59625e656eace1d7be165500af8ec351be41/www/src/custom.css -------------------------------------------------------------------------------- /www/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /www/src/styles/lander.css: -------------------------------------------------------------------------------- 1 | :root[data-has-hero] { 2 | header.header { 3 | display: none; 4 | } 5 | .main-frame { 6 | padding-top: 0; 7 | 8 | .main-pane > main { 9 | padding: 0; 10 | } 11 | } 12 | main > .content-panel .sl-markdown-content { 13 | margin-top: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | --------------------------------------------------------------------------------