├── .github ├── _typos.toml ├── dependabot.yml ├── codecov.yml ├── workflows │ ├── publish.yml │ └── ci.yml ├── CONTRIBUTING.md └── SECURITY.md ├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── lib ├── types.ts ├── get_required_env_test.ts ├── get_required_env.ts ├── _kv_test.ts ├── _test_utils.ts ├── create_logto_oauth_config.ts ├── create_clerk_oauth_config.ts ├── create_notion_oauth_config.ts ├── create_slack_oauth_config.ts ├── create_github_oauth_config.ts ├── create_dropbox_oauth_config.ts ├── create_gitlab_oauth_config.ts ├── create_spotify_oauth_config.ts ├── create_discord_oauth_config.ts ├── create_twitter_oauth_config.ts ├── create_patreon_oauth_config.ts ├── create_google_oauth_config.ts ├── create_facebook_oauth_config.ts ├── create_okta_oauth_config.ts ├── create_auth0_oauth_config.ts ├── create_azure_ad_oauth_config.ts ├── create_aws_cognito_oauth_config.ts ├── _http_test.ts ├── create_azure_adb2c_oauth_config.ts ├── _http.ts ├── _kv.ts ├── create_oauth_config_test.ts ├── create_helpers.ts └── create_helpers_test.ts ├── tools └── check_license.ts ├── LICENSE ├── mod.ts ├── deno.json ├── demo.ts ├── logo-dark.svg ├── logo-light.svg └── README.md /.github/_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["vendor/**"] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Use Unix line endings in all text files. 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | *.DS_Store 3 | coverage/ 4 | cov.lcov 5 | dev.ts 6 | deno.lock 7 | html_cov/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.lint": true, 3 | "deno.enable": true, 4 | "deno.unstable": true, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "denoland.vscode-deno" 7 | } 8 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | export type { Cookie } from "@std/http"; 3 | export type { OAuth2ClientConfig, Tokens } from "@cmd-johnson/oauth2-client"; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | # Check for updates to GitHub Actions every week 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | 10 | ignore: 11 | - "vendor" 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # The OIDC ID token is used for authentication with JSR. 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: npx jsr publish 17 | -------------------------------------------------------------------------------- /lib/get_required_env_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { assertEquals, assertThrows } from "@std/assert"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | Deno.test("getRequiredEnv()", () => { 6 | Deno.env.set("VAR_1", crypto.randomUUID()); 7 | assertEquals(getRequiredEnv("VAR_1"), Deno.env.get("VAR_1")); 8 | assertThrows( 9 | () => getRequiredEnv("MADE_UP_VAR"), 10 | Error, 11 | '"MADE_UP_VAR" environment variable must be set', 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/get_required_env.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | /** 3 | * Returns the environment variable with the given key after ensuring that it's 4 | * been set in the current process. This can be used when defining a custom 5 | * OAuth configuration. 6 | * 7 | * @example Usage 8 | * ```ts ignore 9 | * import { getRequiredEnv } from "jsr:@deno/kv-oauth"; 10 | * 11 | * getRequiredEnv("HOME"); // Returns "/home/alice" 12 | * ``` 13 | */ 14 | export function getRequiredEnv(key: string): string { 15 | const value = Deno.env.get(key); 16 | if (value === undefined) { 17 | throw new Error(`"${key}" environment variable must be set`); 18 | } 19 | return value; 20 | } 21 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Submitting a Pull Request 4 | 5 | Before submitting a pull request, please run `deno task ok` and ensure all 6 | checks pass. This checks formatting, linting, types and runs tests. 7 | 8 | ## Adding a Pre-configured OAuth Client 9 | 10 | In the pull request, please do the following: 11 | 12 | 1. Share a screenshot of the 13 | [demo web page running on your local machine](../README.md#run-the-demo-locally). 14 | This confirms that the newly created OAuth client is working correctly. 15 | 1. Ensure the code example snippet is reproducible. 16 | 1. Add the provider to the README's list of 17 | [pre-configured OAuth clients](../README.md#pre-defined-oauth-configurations), 18 | in alphabetical order. 19 | -------------------------------------------------------------------------------- /tools/check_license.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { walk } from "@std/fs"; 3 | 4 | const CHECK = Deno.args.includes("--check"); 5 | const CURRENT_YEAR = new Date().getFullYear(); 6 | const COPYRIGHT = 7 | `// Copyright 2023-${CURRENT_YEAR} the Deno authors. All rights reserved. MIT license.\n`; 8 | 9 | async function checkLicense(path: string) { 10 | const content = await Deno.readTextFile(path); 11 | if (content.startsWith(COPYRIGHT)) return; 12 | if (CHECK) { 13 | throw new Error(`Missing copyright header: ${path}`); 14 | } else { 15 | await Deno.writeTextFile(path, COPYRIGHT + content); 16 | } 17 | } 18 | 19 | for await ( 20 | const { path } of walk(new URL("../", import.meta.url), { 21 | exts: [".ts"], 22 | includeDirs: false, 23 | skip: [/vendor/], 24 | }) 25 | ) { 26 | checkLicense(path); 27 | } 28 | -------------------------------------------------------------------------------- /lib/_kv_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { assertEquals, assertRejects } from "@std/assert"; 3 | import { getAndDeleteOAuthSession, setOAuthSession } from "./_kv.ts"; 4 | import { randomOAuthSession } from "./_test_utils.ts"; 5 | 6 | Deno.test("(getAndDelete/set)OAuthSession()", async () => { 7 | const id = crypto.randomUUID(); 8 | 9 | // OAuth session doesn't yet exist 10 | await assertRejects( 11 | async () => await getAndDeleteOAuthSession(id), 12 | Deno.errors.NotFound, 13 | "OAuth session not found", 14 | ); 15 | 16 | const oauthSession = randomOAuthSession(); 17 | await setOAuthSession(id, oauthSession, { expireIn: 1_000 }); 18 | assertEquals(await getAndDeleteOAuthSession(id), oauthSession); 19 | await assertRejects( 20 | async () => await getAndDeleteOAuthSession(id), 21 | Deno.errors.NotFound, 22 | "OAuth session not found", 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | - Please email security@deno.com with sufficient information to reproduce the 6 | problem quickly, such as reproduction steps and `deno --version` output. 7 | - Please do not take advantage of the vulnerability or problem you have 8 | discovered. 9 | - Please do not publish or reveal the problem until it has been resolved. 10 | - Please do not use attacks on physical security or applications of third 11 | parties. 12 | 13 | ## Our Commitment 14 | 15 | - We strive to resolve all problems as quickly as possible and are more than 16 | happy to play an active role in the publication of write-ups after the problem 17 | is resolved. 18 | - If you follow this policy, we will not take legal action against you regarding 19 | your report. 20 | - We will handle your report with strict confidentiality and not pass on your 21 | personal details to third parties without your permission. 22 | 23 | ## Further Reading 24 | 25 | See the Security Policy for the Deno CLI 26 | [here](https://github.com/denoland/deno/blob/main/.github/SECURITY.md). 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2023 the Deno authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | export * from "./lib/create_auth0_oauth_config.ts"; 3 | export * from "./lib/create_azure_ad_oauth_config.ts"; 4 | export * from "./lib/create_azure_adb2c_oauth_config.ts"; 5 | export * from "./lib/create_aws_cognito_oauth_config.ts"; 6 | export * from "./lib/create_clerk_oauth_config.ts"; 7 | export * from "./lib/create_discord_oauth_config.ts"; 8 | export * from "./lib/create_dropbox_oauth_config.ts"; 9 | export * from "./lib/create_facebook_oauth_config.ts"; 10 | export * from "./lib/create_github_oauth_config.ts"; 11 | export * from "./lib/create_gitlab_oauth_config.ts"; 12 | export * from "./lib/create_google_oauth_config.ts"; 13 | export * from "./lib/create_logto_oauth_config.ts"; 14 | export * from "./lib/create_notion_oauth_config.ts"; 15 | export * from "./lib/create_okta_oauth_config.ts"; 16 | export * from "./lib/create_patreon_oauth_config.ts"; 17 | export * from "./lib/create_slack_oauth_config.ts"; 18 | export * from "./lib/create_spotify_oauth_config.ts"; 19 | export * from "./lib/create_twitter_oauth_config.ts"; 20 | export * from "./lib/create_helpers.ts"; 21 | export * from "./lib/get_required_env.ts"; 22 | export * from "./lib/types.ts"; 23 | -------------------------------------------------------------------------------- /lib/_test_utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { assert, assertEquals } from "@std/assert"; 3 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 4 | import { STATUS_CODE } from "@std/http"; 5 | 6 | import type { OAuthSession } from "./_kv.ts"; 7 | 8 | export function randomOAuthConfig(): OAuth2ClientConfig { 9 | return { 10 | clientId: crypto.randomUUID(), 11 | clientSecret: crypto.randomUUID(), 12 | authorizationEndpointUri: "https://example.com/authorize", 13 | tokenUri: "https://example.com/token", 14 | }; 15 | } 16 | 17 | export function randomOAuthSession(): OAuthSession { 18 | return { 19 | state: crypto.randomUUID(), 20 | codeVerifier: crypto.randomUUID(), 21 | successUrl: `http://${crypto.randomUUID()}.com`, 22 | }; 23 | } 24 | 25 | export function randomTokensBody() { 26 | return { 27 | access_token: crypto.randomUUID(), 28 | token_type: crypto.randomUUID(), 29 | }; 30 | } 31 | 32 | export function assertRedirect(response: Response, location?: string) { 33 | assertEquals(response.status, STATUS_CODE.Found); 34 | if (location !== undefined) { 35 | assertEquals(response.headers.get("location"), location); 36 | } else { 37 | assert(response.headers.has("location")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | ci: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | deno: 16 | - v1.x 17 | - canary 18 | os: 19 | - ubuntu-latest 20 | - windows-latest 21 | - macOS-latest 22 | 23 | steps: 24 | - name: Setup repo 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Deno 28 | uses: denoland/setup-deno@v2 29 | with: 30 | deno-version: ${{ matrix.deno }} 31 | 32 | - name: Check typos 33 | if: matrix.os == 'ubuntu-latest' 34 | uses: crate-ci/typos@master 35 | with: 36 | config: .github/_typos.toml 37 | 38 | - name: Check formatting, linting, types and run tests 39 | run: deno task ok 40 | 41 | - name: Publish (dry-run) 42 | run: deno publish --dry-run --allow-dirty 43 | 44 | - name: Create lcov file 45 | run: deno task cov:gen 46 | 47 | - name: Upload coverage 48 | uses: codecov/codecov-action@v5 49 | with: 50 | name: ${{ matrix.os }} 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | files: cov.lcov 53 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/kv-oauth", 3 | "version": "0.11.0", 4 | "exports": "./mod.ts", 5 | "lock": false, 6 | "imports": { 7 | "@cmd-johnson/oauth2-client": "jsr:@cmd-johnson/oauth2-client@^2.0.0", 8 | "@std/assert": "jsr:@std/assert@^1.0.6", 9 | "@std/datetime": "jsr:@std/datetime@^0.225.2", 10 | "@std/fs": "jsr:@std/fs@^1.0.4", 11 | "@std/http": "jsr:@std/http@^1.0.8", 12 | "@std/testing": "jsr:@std/testing@^1.0.3" 13 | }, 14 | "tasks": { 15 | "demo": "deno run --allow-net --env --allow-env --unstable-kv --watch demo.ts", 16 | "check:license": "deno run -A tools/check_license.ts", 17 | "check:docs": "deno doc --lint mod.ts", 18 | "check": "deno task check:license --check", 19 | "test": "DENO_KV_PATH=:memory: deno test --unstable-kv --allow-env --allow-read --allow-run --parallel --trace-leaks --coverage --doc", 20 | "coverage": "deno coverage coverage", 21 | "ok": "deno fmt --check && deno lint && deno task check && deno task test", 22 | "cov:gen": "deno task coverage --lcov --output=cov.lcov", 23 | "update": "deno run -A https://deno.land/x/udd/main.ts --test=\"deno task test\" deps.ts dev_deps.ts", 24 | "update:fresh": "deno run -A -r https://fresh.deno.dev/update ." 25 | }, 26 | "exclude": [ 27 | "coverage/" 28 | ], 29 | "compilerOptions": { 30 | "noUncheckedIndexedAccess": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/create_logto_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Logto. 7 | * 8 | * Requires `--allow-env[=LOGTO_CLIENT_ID,LOGTO_CLIENT_SECRET,LOGTO_DOMAIN]` permissions 9 | * and environment variables: 10 | * 1. `LOGTO_CLIENT_ID` 11 | * 2. `LOGTO_CLIENT_SECRET` 12 | * 3. `LOGTO_DOMAIN` 13 | * 14 | * @example Usage 15 | * ```ts 16 | * import { createLogtoOAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createLogtoOAuthConfig(); 19 | * ``` 20 | * 21 | * @see {@link https://docs.logto.io/docs/references/applications/} 22 | */ 23 | export function createLogtoOAuthConfig(config?: { 24 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 25 | redirectUri?: string; 26 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 27 | scope?: string | string[]; 28 | }): OAuth2ClientConfig { 29 | const baseURL = getRequiredEnv("LOGTO_DOMAIN"); 30 | return { 31 | clientId: getRequiredEnv("LOGTO_CLIENT_ID"), 32 | clientSecret: getRequiredEnv("LOGTO_CLIENT_SECRET"), 33 | authorizationEndpointUri: `${baseURL}/auth`, 34 | tokenUri: `${baseURL}/token`, 35 | redirectUri: config?.redirectUri, 36 | defaults: { scope: config?.scope }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /lib/create_clerk_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Clerk. 7 | * 8 | * Requires `--allow-env[=CLERK_CLIENT_ID,CLERK_CLIENT_SECRET,CLERK_DOMAIN]` permissions 9 | * and environment variables: 10 | * 1. `CLERK_CLIENT_ID` 11 | * 2. `CLERK_CLIENT_SECRET` 12 | * 3. `CLERK_DOMAIN` 13 | * 14 | * @example Usage 15 | * ```ts 16 | * import { createClerkOAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createClerkOAuthConfig(); 19 | * ``` 20 | * 21 | * @see {@link https://clerk.com/docs/advanced-usage/clerk-idp} 22 | */ 23 | export function createClerkOAuthConfig(config?: { 24 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 25 | redirectUri?: string; 26 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 27 | scope?: string | string[]; 28 | }): OAuth2ClientConfig { 29 | const baseURL = getRequiredEnv("CLERK_DOMAIN"); 30 | return { 31 | clientId: getRequiredEnv("CLERK_CLIENT_ID"), 32 | clientSecret: getRequiredEnv("CLERK_CLIENT_SECRET"), 33 | authorizationEndpointUri: `${baseURL}/authorize`, 34 | tokenUri: `${baseURL}/token`, 35 | redirectUri: config?.redirectUri, 36 | defaults: { scope: config?.scope }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /lib/create_notion_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Notion. 7 | * 8 | * Requires `--allow-env[=NOTION_CLIENT_ID,NOTION_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `NOTION_CLIENT_ID` 11 | * 2. `NOTION_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createNotionOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createNotionOAuthConfig(); 18 | * ``` 19 | * 20 | * @see {@link https://developers.notion.com/docs/authorization} 21 | */ 22 | export function createNotionOAuthConfig( 23 | config?: { 24 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 25 | redirectUri?: string; 26 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 27 | scope?: string | string[]; 28 | }, 29 | ): OAuth2ClientConfig { 30 | return { 31 | clientId: getRequiredEnv("NOTION_CLIENT_ID"), 32 | clientSecret: getRequiredEnv("NOTION_CLIENT_SECRET"), 33 | authorizationEndpointUri: 34 | "https://api.notion.com/v1/oauth/authorize?owner=user", 35 | tokenUri: "https://api.notion.com/v1/oauth/token", 36 | redirectUri: config?.redirectUri, 37 | defaults: { scope: config?.scope }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /lib/create_slack_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Slack. 7 | * 8 | * Requires `--allow-env[=SLACK_CLIENT_ID,SLACK_CLIENT_SECRET]` permissions and 9 | * environment variables: 10 | * 1. `SLACK_CLIENT_ID` 11 | * 2. `SLACK_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createSlackOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createSlackOAuthConfig({ 18 | * scope: "users.profile:read", 19 | * }); 20 | * ``` 21 | * 22 | * @see {@link https://api.slack.com/authentication/oauth-v2} 23 | */ 24 | export function createSlackOAuthConfig( 25 | config: { 26 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 27 | redirectUri?: string; 28 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 29 | scope: string | string[]; 30 | }, 31 | ): OAuth2ClientConfig { 32 | return { 33 | clientId: getRequiredEnv("SLACK_CLIENT_ID"), 34 | clientSecret: getRequiredEnv("SLACK_CLIENT_SECRET"), 35 | authorizationEndpointUri: "https://slack.com/oauth/v2/authorize", 36 | tokenUri: "https://slack.com/api/oauth.v2.access", 37 | redirectUri: config.redirectUri, 38 | defaults: { scope: config.scope }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/create_github_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for GitHub. 7 | * 8 | * Requires `--allow-env[=GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `GITHUB_CLIENT_ID` 11 | * 2. `GITHUB_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createGitHubOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createGitHubOAuthConfig(); 18 | * ``` 19 | * 20 | * @see {@link https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps} 21 | */ 22 | export function createGitHubOAuthConfig( 23 | config?: { 24 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 25 | redirectUri?: string; 26 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 27 | scope?: string | string[]; 28 | }, 29 | ): OAuth2ClientConfig { 30 | return { 31 | clientId: getRequiredEnv("GITHUB_CLIENT_ID"), 32 | clientSecret: getRequiredEnv("GITHUB_CLIENT_SECRET"), 33 | authorizationEndpointUri: "https://github.com/login/oauth/authorize", 34 | tokenUri: "https://github.com/login/oauth/access_token", 35 | redirectUri: config?.redirectUri, 36 | defaults: { scope: config?.scope }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /lib/create_dropbox_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Dropbox. 7 | * 8 | * Requires `--allow-env[=DROPBOX_CLIENT_ID,DROPBOX_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `DROPBOX_CLIENT_ID` 11 | * 2. `DROPBOX_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createDropboxOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createDropboxOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback" 19 | * }); 20 | * ``` 21 | * 22 | * @see {@link https://developers.dropbox.com/oauth-guide} 23 | */ 24 | export function createDropboxOAuthConfig( 25 | config: { 26 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 27 | redirectUri: string; 28 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 29 | scope?: string | string[]; 30 | }, 31 | ): OAuth2ClientConfig { 32 | return { 33 | clientId: getRequiredEnv("DROPBOX_CLIENT_ID"), 34 | clientSecret: getRequiredEnv("DROPBOX_CLIENT_SECRET"), 35 | authorizationEndpointUri: "https://www.dropbox.com/oauth2/authorize", 36 | tokenUri: "https://api.dropboxapi.com/oauth2/token", 37 | redirectUri: config.redirectUri, 38 | defaults: { scope: config.scope }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/create_gitlab_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for GitLab. 7 | * 8 | * Requires `--allow-env[=GITLAB_CLIENT_ID,GITLAB_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `GITLAB_CLIENT_ID` 11 | * 2. `GITLAB_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createGitLabOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createGitLabOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "profile", 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://docs.gitlab.com/ee/api/oauth2.html} 24 | */ 25 | export function createGitLabOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("GITLAB_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("GITLAB_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://gitlab.com/oauth/authorize", 37 | tokenUri: "https://gitlab.com/oauth/token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_spotify_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Spotify. 7 | * 8 | * Requires `--allow-env[=SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `SPOTIFY_CLIENT_ID` 11 | * 2. `SPOTIFY_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createSpotifyOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createSpotifyOAuthConfig({ 18 | * scope: "user-read-private user-read-email" 19 | * }); 20 | * ``` 21 | * 22 | * @see {@link https://developer.spotify.com/documentation/web-api/tutorials/code-flow} 23 | */ 24 | export function createSpotifyOAuthConfig( 25 | config: { 26 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 27 | redirectUri?: string; 28 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 29 | scope: string | string[]; 30 | }, 31 | ): OAuth2ClientConfig { 32 | return { 33 | clientId: getRequiredEnv("SPOTIFY_CLIENT_ID"), 34 | clientSecret: getRequiredEnv("SPOTIFY_CLIENT_SECRET"), 35 | authorizationEndpointUri: "https://accounts.spotify.com/authorize", 36 | tokenUri: "https://accounts.spotify.com/api/token", 37 | redirectUri: config.redirectUri, 38 | defaults: { scope: config.scope }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/create_discord_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Discord. 7 | * 8 | * Requires `--allow-env[=DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `DISCORD_CLIENT_ID` 11 | * 2. `DISCORD_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createDiscordOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createDiscordOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "identify", 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://discord.com/developers/docs/topics/oauth2} 24 | */ 25 | export function createDiscordOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("DISCORD_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("DISCORD_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://discord.com/oauth2/authorize", 37 | tokenUri: "https://discord.com/api/oauth2/token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_twitter_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Twitter. 7 | * 8 | * Requires `--allow-env[=TWITTER_CLIENT_ID,TWITTER_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `TWITTER_CLIENT_ID` 11 | * 2. `TWITTER_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createTwitterOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createTwitterOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "users.read", 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://github.com/twitterdev/twitter-api-typescript-sdk} 24 | */ 25 | export function createTwitterOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("TWITTER_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("TWITTER_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://twitter.com/i/oauth2/authorize", 37 | tokenUri: "https://api.twitter.com/2/oauth2/token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_patreon_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Patreon. 7 | * 8 | * Requires `--allow-env[=PATREON_CLIENT_ID,PATREON_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `PATREON_CLIENT_ID` 11 | * 2. `PATREON_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createPatreonOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createPatreonOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "identity identity[email]" 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://www.patreon.com/portal/registration/register-clients} 24 | */ 25 | export function createPatreonOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("PATREON_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("PATREON_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://www.patreon.com/oauth2/authorize", 37 | tokenUri: "https://www.patreon.com/api/oauth2/token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_google_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Google. 7 | * 8 | * Requires `--allow-env[=GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET]` permissions 9 | * and environment variables: 10 | * 1. `GOOGLE_CLIENT_ID` 11 | * 2. `GOOGLE_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createGoogleOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createGoogleOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "https://www.googleapis.com/auth/userinfo.profile" 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://developers.google.com/identity/protocols/oauth2/web-server} 24 | */ 25 | export function createGoogleOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("GOOGLE_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("GOOGLE_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://accounts.google.com/o/oauth2/v2/auth", 37 | tokenUri: "https://oauth2.googleapis.com/token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_facebook_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Facebook. 7 | * 8 | * Requires `--allow-env[=FACEBOOK_CLIENT_ID,FACEBOOK_CLIENT_SECRET]` 9 | * permissions and environment variables: 10 | * 1. `FACEBOOK_CLIENT_ID` 11 | * 2. `FACEBOOK_CLIENT_SECRET` 12 | * 13 | * @example Usage 14 | * ```ts 15 | * import { createFacebookOAuthConfig } from "jsr:@deno/kv-oauth"; 16 | * 17 | * const oauthConfig = createFacebookOAuthConfig({ 18 | * redirectUri: "http://localhost:8000/callback", 19 | * scope: "email", 20 | * }); 21 | * ``` 22 | * 23 | * @see {@link https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow} 24 | */ 25 | export function createFacebookOAuthConfig( 26 | config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }, 32 | ): OAuth2ClientConfig { 33 | return { 34 | clientId: getRequiredEnv("FACEBOOK_CLIENT_ID"), 35 | clientSecret: getRequiredEnv("FACEBOOK_CLIENT_SECRET"), 36 | authorizationEndpointUri: "https://www.facebook.com/v17.0/dialog/oauth", 37 | tokenUri: "https://graph.facebook.com/v17.0/oauth/access_token", 38 | redirectUri: config.redirectUri, 39 | defaults: { scope: config.scope }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/create_okta_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Okta. 7 | * 8 | * Requires `--allow-env[=OKTA_CLIENT_ID,OKTA_CLIENT_SECRET,OKTA_DOMAIN]` 9 | * permissions and environment variables: 10 | * 1. `OKTA_CLIENT_ID` 11 | * 2. `OKTA_CLIENT_SECRET` 12 | * 3. `OKTA_DOMAIN` 13 | * 14 | * @example Usage 15 | * ```ts 16 | * import { createOktaOAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createOktaOAuthConfig({ 19 | * redirectUri: "http://localhost:8000/callback", 20 | * scope: "openid", 21 | * }); 22 | * ``` 23 | * 24 | * @see {@link https://developer.okta.com/docs/reference/api/oidc} 25 | */ 26 | export function createOktaOAuthConfig( 27 | config: { 28 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 29 | redirectUri: string; 30 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 31 | scope: string | string[]; 32 | }, 33 | ): OAuth2ClientConfig { 34 | const domain = getRequiredEnv("OKTA_DOMAIN"); 35 | const baseURL = `https://${domain}/oauth2`; 36 | return { 37 | clientId: getRequiredEnv("OKTA_CLIENT_ID"), 38 | clientSecret: getRequiredEnv("OKTA_CLIENT_SECRET"), 39 | authorizationEndpointUri: `${baseURL}/v1/authorize`, 40 | tokenUri: `${baseURL}/v1/token`, 41 | redirectUri: config.redirectUri, 42 | defaults: { scope: config.scope }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /lib/create_auth0_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Auth0. 7 | * 8 | * Requires `--allow-env[=AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRET,AUTH0_DOMAIN]` 9 | * permissions and environment variables: 10 | * 1. `AUTH0_CLIENT_ID` 11 | * 2. `AUTH0_CLIENT_SECRET` 12 | * 3. `AUTH0_DOMAIN` 13 | * 14 | * @example Usage 15 | * ```ts 16 | * import { createAuth0OAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createAuth0OAuthConfig({ 19 | * redirectUri: "http://localhost:8000/callback", 20 | * scope: "openid" 21 | * }); 22 | * ``` 23 | * 24 | * @see {@link https://auth0.com/docs/authenticate/protocols/oauth} 25 | */ 26 | 27 | export function createAuth0OAuthConfig( 28 | config: { 29 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 30 | redirectUri: string; 31 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 32 | scope: string | string[]; 33 | }, 34 | ): OAuth2ClientConfig { 35 | const domain = getRequiredEnv("AUTH0_DOMAIN"); 36 | const baseURL = `https://${domain}/oauth2`; 37 | return { 38 | clientId: getRequiredEnv("AUTH0_CLIENT_ID"), 39 | clientSecret: getRequiredEnv("AUTH0_CLIENT_SECRET"), 40 | authorizationEndpointUri: `${baseURL}/authorize`, 41 | tokenUri: `${baseURL}/oauth/token`, 42 | redirectUri: config.redirectUri, 43 | defaults: { scope: config.scope }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/create_azure_ad_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Azure. 7 | * 8 | * Requires `--allow-env[=AZURE_AD_CLIENT_ID,AZURE_AD_CLIENT_SECRET,AZURE_AD_TENANT_ID]` 9 | * permissions and environment variables: 10 | * 1. `AZURE_AD_CLIENT_ID` 11 | * 2. `AZURE_AD_CLIENT_SECRET` 12 | * 4. `AZURE_AD_TENANT_ID` 13 | * 14 | * @example Usage 15 | * ```ts ignore 16 | * import { createAzureAdOAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createAzureAdOAuthConfig({ 19 | * redirectUri: "http://localhost:8000/callback", 20 | * scope: ["openid"] 21 | * }); 22 | * ``` 23 | * 24 | * @see {@link https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc} 25 | */ 26 | export function createAzureAdOAuthConfig(config: { 27 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 28 | redirectUri?: string; 29 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 30 | scope: string | string[]; 31 | }): OAuth2ClientConfig { 32 | const baseUrl = `https://login.microsoftonline.com/${ 33 | getRequiredEnv( 34 | "AZURE_AD_TENANT_ID", 35 | ) 36 | }/oauth2/v2.0`; 37 | 38 | return { 39 | clientId: getRequiredEnv("AZURE_AD_CLIENT_ID"), 40 | clientSecret: getRequiredEnv("AZURE_AD_CLIENT_SECRET"), 41 | authorizationEndpointUri: `${baseUrl}/authorize`, 42 | tokenUri: `${baseUrl}/token`, 43 | redirectUri: config.redirectUri, 44 | defaults: { 45 | scope: config.scope, 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /lib/create_aws_cognito_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for an Amazon Cognito user pool. 7 | * 8 | * Requires `--allow-env[=AWS_COGNITO_CLIENT_ID,AWS_COGNITO_CLIENT_SECRET,AWS_COGNITO_DOMAIN]` 9 | * permissions and environment variables: 10 | * 1. `AWS_COGNITO_CLIENT_ID` 11 | * 2. `AWS_COGNITO_CLIENT_SECRET` 12 | * 3. `AWS_COGNITO_DOMAIN` 13 | * 14 | * @example Usage 15 | * ```ts ignore 16 | * import { createAwsCognitoOAuthConfig } from "jsr:@deno/kv-oauth"; 17 | * 18 | * const oauthConfig = createAwsCognitoOAuthConfig({ 19 | * redirectUri: "http://localhost:8000/callback", 20 | * scope: "openid" 21 | * }); 22 | * ``` 23 | * 24 | * @see {@link https://docs.aws.amazon.com/cognito/latest/developerguide/federation-endpoints-oauth-grants.html} 25 | */ 26 | 27 | export function createAwsCognitoOAuthConfig( 28 | config: { 29 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 30 | redirectUri: string; 31 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 32 | scope?: string | string[]; 33 | }, 34 | ): OAuth2ClientConfig { 35 | const domain = getRequiredEnv("AWS_COGNITO_DOMAIN"); 36 | const baseURL = `https://${domain}/oauth2`; 37 | return { 38 | clientId: getRequiredEnv("AWS_COGNITO_CLIENT_ID"), 39 | clientSecret: getRequiredEnv("AWS_COGNITO_CLIENT_SECRET"), 40 | authorizationEndpointUri: `${baseURL}/authorize`, 41 | tokenUri: `${baseURL}/token`, 42 | redirectUri: config.redirectUri, 43 | defaults: { scope: config?.scope }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/_http_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { assertEquals } from "@std/assert"; 3 | import { getCookieName, getSuccessUrl, isHttps, redirect } from "./_http.ts"; 4 | import { assertRedirect } from "./_test_utils.ts"; 5 | 6 | Deno.test("isHttps()", () => { 7 | assertEquals(isHttps("https://example.com"), true); 8 | assertEquals(isHttps("http://example.com"), false); 9 | }); 10 | 11 | Deno.test("getCookieName()", () => { 12 | assertEquals(getCookieName("hello", true), "__Host-hello"); 13 | assertEquals(getCookieName("hello", false), "hello"); 14 | }); 15 | 16 | Deno.test("redirect() returns a redirect response", () => { 17 | const location = "/hello-there"; 18 | const response = redirect(location); 19 | assertRedirect(response, location); 20 | }); 21 | 22 | Deno.test("getSuccessUrl() returns `success_url` URL parameter, if defined", () => { 23 | assertEquals( 24 | getSuccessUrl( 25 | new Request( 26 | `http://example.com?success_url=${ 27 | encodeURIComponent("http://test.com") 28 | }`, 29 | ), 30 | ), 31 | "http://test.com", 32 | ); 33 | }); 34 | 35 | Deno.test("getSuccessUrl() returns referer header of same origin, if defined", () => { 36 | const referer = "http://example.com/path"; 37 | assertEquals( 38 | getSuccessUrl( 39 | new Request("http://example.com", { headers: { referer } }), 40 | ), 41 | referer, 42 | ); 43 | }); 44 | 45 | Deno.test("getSuccessUrl() returns root path if referer is of different origin", () => { 46 | assertEquals( 47 | getSuccessUrl( 48 | new Request("http://example.com", { 49 | headers: { referer: "http://test.com" }, 50 | }), 51 | ), 52 | "/", 53 | ); 54 | }); 55 | 56 | Deno.test("getSuccessUrl() returns root path by default", () => { 57 | assertEquals( 58 | getSuccessUrl(new Request("http://example.com")), 59 | "/", 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/create_azure_adb2c_oauth_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import type { OAuth2ClientConfig } from "@cmd-johnson/oauth2-client"; 3 | import { getRequiredEnv } from "./get_required_env.ts"; 4 | 5 | /** 6 | * Returns the OAuth configuration for Azure. 7 | * 8 | * Requires `--allow-env[=AZURE_ADB2C_CLIENT_ID,AZURE_ADB2C_CLIENT_SECRET,AZURE_ADB2C_DOMAIN,AZURE_ADB2C_POLICY,AZURE_ADB2C_TENANT_ID]` 9 | * permissions and environment variables: 10 | * 1. `AZURE_ADB2C_CLIENT_ID` 11 | * 2. `AZURE_ADB2C_CLIENT_SECRET` 12 | * 3. `AZURE_ADB2C_DOMAIN` 13 | * 4. `AZURE_ADB2C_POLICY` 14 | * 5. `AZURE_ADB2C_TENANT_ID` 15 | * 16 | * @example Usage 17 | * ```ts 18 | * import { createAzureAdb2cOAuthConfig } from "jsr:@deno/kv-oauth"; 19 | * 20 | * const oauthConfig = createAzureAdb2cOAuthConfig({ 21 | * redirectUri: "http://localhost:8000/callback", 22 | * scope: ["openid", Deno.env.get("AZURE_ADB2C_CLIENT_ID")!] 23 | * }); 24 | * ``` 25 | * 26 | * @see {@link https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc} 27 | */ 28 | export function createAzureAdb2cOAuthConfig(config: { 29 | /** @see {@linkcode OAuth2ClientConfig.redirectUri} */ 30 | redirectUri?: string; 31 | /** @see {@linkcode OAuth2ClientConfig.defaults.scope} */ 32 | scope: string | string[]; 33 | }): OAuth2ClientConfig { 34 | const baseUrl = `https://${ 35 | getRequiredEnv( 36 | "AZURE_ADB2C_DOMAIN", 37 | ) 38 | }/${getRequiredEnv("AZURE_ADB2C_TENANT_ID")}/${ 39 | getRequiredEnv( 40 | "AZURE_ADB2C_POLICY", 41 | ) 42 | }/oauth2/v2.0`; 43 | 44 | return { 45 | clientId: getRequiredEnv("AZURE_ADB2C_CLIENT_ID"), 46 | clientSecret: getRequiredEnv("AZURE_ADB2C_CLIENT_SECRET"), 47 | authorizationEndpointUri: `${baseUrl}/authorize`, 48 | tokenUri: `${baseUrl}/token`, 49 | redirectUri: config.redirectUri, 50 | defaults: { 51 | scope: config.scope, 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /lib/_http.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { type Cookie, getCookies } from "@std/http"; 3 | import { STATUS_CODE } from "@std/http"; 4 | 5 | export const OAUTH_COOKIE_NAME = "oauth-session"; 6 | export const SITE_COOKIE_NAME = "site-session"; 7 | 8 | export function isHttps(url: string): boolean { 9 | return url.startsWith("https://"); 10 | } 11 | 12 | /** 13 | * Dynamically prefixes the cookie name, depending on whether it's for a secure 14 | * origin (HTTPS). 15 | */ 16 | export function getCookieName(name: string, isHttps: boolean): string { 17 | return isHttps ? "__Host-" + name : name; 18 | } 19 | 20 | /** 21 | * @see {@link https://web.dev/first-party-cookie-recipes/#the-good-first-party-cookie-recipe} 22 | */ 23 | export const COOKIE_BASE = { 24 | secure: true, 25 | path: "/", 26 | httpOnly: true, 27 | // 90 days 28 | maxAge: 7776000, 29 | sameSite: "Lax", 30 | } as Required>; 31 | 32 | export function redirect(location: string): Response { 33 | return new Response(null, { 34 | headers: { 35 | location, 36 | }, 37 | status: STATUS_CODE.Found, 38 | }); 39 | } 40 | 41 | /** 42 | * @see {@link https://github.com/denoland/deno_kv_oauth/tree/main#redirects-after-sign-in-and-sign-out} 43 | */ 44 | export function getSuccessUrl(request: Request): string { 45 | const url = new URL(request.url); 46 | 47 | const successUrl = url.searchParams.get("success_url"); 48 | if (successUrl !== null) return successUrl; 49 | 50 | const referrer = request.headers.get("referer"); 51 | if (referrer !== null && (new URL(referrer).origin === url.origin)) { 52 | return referrer; 53 | } 54 | 55 | return "/"; 56 | } 57 | 58 | export function getSessionIdCookie( 59 | request: Request, 60 | cookieName = getCookieName(SITE_COOKIE_NAME, isHttps(request.url)), 61 | ): string | undefined { 62 | return getCookies(request.headers)[cookieName]; 63 | } 64 | -------------------------------------------------------------------------------- /demo.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { STATUS_CODE } from "@std/http"; 3 | import { createGitHubOAuthConfig, createHelpers } from "./mod.ts"; 4 | 5 | /** 6 | * Modify the OAuth configuration creation function when testing for providers. 7 | * 8 | * @example Usage 9 | * ```ts 10 | * import { createNotionOAuthConfig } from "./mod.ts"; 11 | * 12 | * const oauthConfig = createNotionOAuthConfig(); 13 | * ``` 14 | */ 15 | const oauthConfig = createGitHubOAuthConfig(); 16 | const { getSessionId, signIn, signOut, handleCallback } = createHelpers( 17 | oauthConfig, 18 | ); 19 | 20 | async function indexHandler(request: Request) { 21 | const sessionId = await getSessionId(request); 22 | const hasSessionIdCookie = sessionId !== undefined; 23 | 24 | const body = ` 25 |

Authorization endpoint URI: ${oauthConfig.authorizationEndpointUri}

26 |

Token URI: ${oauthConfig.tokenUri}

27 |

Scope: ${oauthConfig.defaults?.scope}

28 |

Signed in: ${hasSessionIdCookie}

29 |

30 | Sign in 31 |

32 |

33 | Sign out 34 |

35 |

36 | Source code 37 |

38 | `; 39 | 40 | return new Response(body, { 41 | headers: { "content-type": "text/html; charset=utf-8" }, 42 | }); 43 | } 44 | 45 | export async function handler(request: Request): Promise { 46 | if (request.method !== "GET") { 47 | return new Response(null, { status: STATUS_CODE.NotFound }); 48 | } 49 | 50 | switch (new URL(request.url).pathname) { 51 | case "/": { 52 | return await indexHandler(request); 53 | } 54 | case "/signin": { 55 | return await signIn(request); 56 | } 57 | case "/callback": { 58 | try { 59 | const { response } = await handleCallback(request); 60 | return response; 61 | } catch { 62 | return new Response(null, { status: STATUS_CODE.InternalServerError }); 63 | } 64 | } 65 | case "/signout": { 66 | return await signOut(request); 67 | } 68 | default: { 69 | return new Response(null, { status: STATUS_CODE.NotFound }); 70 | } 71 | } 72 | } 73 | 74 | if (import.meta.main) { 75 | Deno.serve(handler); 76 | } 77 | -------------------------------------------------------------------------------- /lib/_kv.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | const DENO_KV_PATH_KEY = "DENO_KV_PATH"; 3 | let path = undefined; 4 | if ( 5 | (await Deno.permissions.query({ name: "env", variable: DENO_KV_PATH_KEY })) 6 | .state === "granted" 7 | ) { 8 | path = Deno.env.get(DENO_KV_PATH_KEY); 9 | } 10 | const kv = await Deno.openKv(path); 11 | 12 | // Gracefully shutdown after tests 13 | addEventListener("beforeunload", async () => { 14 | await kv.close(); 15 | }); 16 | 17 | export interface OAuthSession { 18 | state: string; 19 | codeVerifier: string; 20 | successUrl: string; 21 | } 22 | 23 | function oauthSessionKey(id: string): [string, string] { 24 | return ["oauth_sessions", id]; 25 | } 26 | 27 | export async function getAndDeleteOAuthSession( 28 | id: string, 29 | ): Promise { 30 | const key = oauthSessionKey(id); 31 | const oauthSessionRes = await kv.get(key); 32 | const oauthSession = oauthSessionRes.value; 33 | if (oauthSession === null) { 34 | throw new Deno.errors.NotFound("OAuth session not found"); 35 | } 36 | 37 | const res = await kv.atomic() 38 | .check(oauthSessionRes) 39 | .delete(key) 40 | .commit(); 41 | 42 | if (!res.ok) throw new Error("Failed to delete OAuth session"); 43 | return oauthSession; 44 | } 45 | 46 | export async function setOAuthSession( 47 | id: string, 48 | value: OAuthSession, 49 | /** 50 | * OAuth session entry expiration isn't included in unit tests as it'd 51 | * require a persistent and restartable KV instance. This is difficult to do 52 | * in this module, as the KV instance is initialized top-level. 53 | */ 54 | options: { expireIn: number }, 55 | ) { 56 | await kv.set(oauthSessionKey(id), value, options); 57 | } 58 | 59 | /** 60 | * The site session is created on the server. It is stored in the database to 61 | * later validate that a session was created on the server. It has no purpose 62 | * beyond that. Hence, the value of the site session entry is arbitrary. 63 | */ 64 | type SiteSession = true; 65 | 66 | function siteSessionKey(id: string): [string, string] { 67 | return ["site_sessions", id]; 68 | } 69 | 70 | export async function isSiteSession(id: string): Promise { 71 | const res = await kv.get(siteSessionKey(id)); 72 | return res.value !== null; 73 | } 74 | 75 | export async function setSiteSession(id: string, expireIn?: number) { 76 | await kv.set(siteSessionKey(id), true, { expireIn }); 77 | } 78 | 79 | export async function deleteSiteSession(id: string) { 80 | await kv.delete(siteSessionKey(id)); 81 | } 82 | -------------------------------------------------------------------------------- /lib/create_oauth_config_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { assertEquals } from "@std/assert"; 3 | import { createAuth0OAuthConfig } from "./create_auth0_oauth_config.ts"; 4 | import { createAzureAdb2cOAuthConfig } from "./create_azure_adb2c_oauth_config.ts"; 5 | import { createAzureAdOAuthConfig } from "./create_azure_ad_oauth_config.ts"; 6 | import { createClerkOAuthConfig } from "./create_clerk_oauth_config.ts"; 7 | import { createDiscordOAuthConfig } from "./create_discord_oauth_config.ts"; 8 | import { createDropboxOAuthConfig } from "./create_dropbox_oauth_config.ts"; 9 | import { createFacebookOAuthConfig } from "./create_facebook_oauth_config.ts"; 10 | import { createGitHubOAuthConfig } from "./create_github_oauth_config.ts"; 11 | import { createGitLabOAuthConfig } from "./create_gitlab_oauth_config.ts"; 12 | import { createGoogleOAuthConfig } from "./create_google_oauth_config.ts"; 13 | import { createLogtoOAuthConfig } from "./create_logto_oauth_config.ts"; 14 | import { createNotionOAuthConfig } from "./create_notion_oauth_config.ts"; 15 | import { createOktaOAuthConfig } from "./create_okta_oauth_config.ts"; 16 | import { createPatreonOAuthConfig } from "./create_patreon_oauth_config.ts"; 17 | import { createSlackOAuthConfig } from "./create_slack_oauth_config.ts"; 18 | import { createSpotifyOAuthConfig } from "./create_spotify_oauth_config.ts"; 19 | import { createTwitterOAuthConfig } from "./create_twitter_oauth_config.ts"; 20 | 21 | [ 22 | { 23 | envPrefix: "AUTH0", 24 | createOAuthConfigFn: createAuth0OAuthConfig, 25 | }, 26 | { 27 | envPrefix: "AZURE_ADB2C", 28 | createOAuthConfigFn: createAzureAdb2cOAuthConfig, 29 | }, 30 | { 31 | envPrefix: "AZURE_AD", 32 | createOAuthConfigFn: createAzureAdOAuthConfig, 33 | }, 34 | { 35 | envPrefix: "CLERK", 36 | createOAuthConfigFn: createClerkOAuthConfig, 37 | }, 38 | { 39 | envPrefix: "DISCORD", 40 | createOAuthConfigFn: createDiscordOAuthConfig, 41 | }, 42 | { 43 | envPrefix: "DROPBOX", 44 | createOAuthConfigFn: createDropboxOAuthConfig, 45 | }, 46 | { 47 | envPrefix: "FACEBOOK", 48 | createOAuthConfigFn: createFacebookOAuthConfig, 49 | }, 50 | { 51 | envPrefix: "GITHUB", 52 | createOAuthConfigFn: createGitHubOAuthConfig, 53 | }, 54 | { 55 | envPrefix: "GITLAB", 56 | createOAuthConfigFn: createGitLabOAuthConfig, 57 | }, 58 | { 59 | envPrefix: "GOOGLE", 60 | createOAuthConfigFn: createGoogleOAuthConfig, 61 | }, 62 | { 63 | envPrefix: "LOGTO", 64 | createOAuthConfigFn: createLogtoOAuthConfig, 65 | }, 66 | { 67 | envPrefix: "NOTION", 68 | createOAuthConfigFn: createNotionOAuthConfig, 69 | }, 70 | { 71 | envPrefix: "OKTA", 72 | createOAuthConfigFn: createOktaOAuthConfig, 73 | }, 74 | { 75 | envPrefix: "PATREON", 76 | createOAuthConfigFn: createPatreonOAuthConfig, 77 | }, 78 | { 79 | envPrefix: "SLACK", 80 | createOAuthConfigFn: createSlackOAuthConfig, 81 | }, 82 | { 83 | envPrefix: "SPOTIFY", 84 | createOAuthConfigFn: createSpotifyOAuthConfig, 85 | }, 86 | { 87 | envPrefix: "TWITTER", 88 | createOAuthConfigFn: createTwitterOAuthConfig, 89 | }, 90 | ].map(({ envPrefix, createOAuthConfigFn }) => 91 | Deno.test(`${createOAuthConfigFn.name}()`, () => { 92 | const clientId = crypto.randomUUID(); 93 | const clientSecret = crypto.randomUUID(); 94 | const redirectUri = `http://${crypto.randomUUID}.com`; 95 | const scope = crypto.randomUUID(); 96 | 97 | Deno.env.set(`${envPrefix}_CLIENT_ID`, clientId); 98 | Deno.env.set(`${envPrefix}_CLIENT_SECRET`, clientSecret); 99 | // Only needed for Okta, Auth0, and AzureADB2C but set for all providers anyway 100 | Deno.env.set(`${envPrefix}_DOMAIN`, crypto.randomUUID()); 101 | // Only needed for Azure and AzureADB2C but set for all providers anyway 102 | Deno.env.set(`${envPrefix}_TENANT_ID`, crypto.randomUUID()); 103 | // Only needed for AzureADB2C but set for all providers anyway 104 | Deno.env.set(`${envPrefix}_POLICY`, crypto.randomUUID()); 105 | 106 | const oauthConfig = createOAuthConfigFn({ redirectUri, scope }); 107 | assertEquals(oauthConfig.clientId, clientId); 108 | assertEquals(oauthConfig.clientSecret, clientSecret); 109 | assertEquals(oauthConfig.redirectUri, redirectUri); 110 | assertEquals(oauthConfig.defaults?.scope, scope); 111 | }) 112 | ); 113 | -------------------------------------------------------------------------------- /logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/create_helpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { 3 | OAuth2Client, 4 | type OAuth2ClientConfig, 5 | type Tokens, 6 | } from "@cmd-johnson/oauth2-client"; 7 | import { SECOND } from "@std/datetime"; 8 | import { type Cookie, deleteCookie, getCookies, setCookie } from "@std/http"; 9 | import { 10 | COOKIE_BASE, 11 | getCookieName, 12 | getSessionIdCookie, 13 | getSuccessUrl, 14 | isHttps, 15 | OAUTH_COOKIE_NAME, 16 | redirect, 17 | SITE_COOKIE_NAME, 18 | } from "./_http.ts"; 19 | import { 20 | deleteSiteSession, 21 | getAndDeleteOAuthSession, 22 | isSiteSession, 23 | setOAuthSession, 24 | setSiteSession, 25 | } from "./_kv.ts"; 26 | 27 | /** Options for {@linkcode signIn}. */ 28 | export interface SignInOptions { 29 | /** URL parameters that are appended to the authorization URI, if defined. */ 30 | urlParams?: Record; 31 | } 32 | 33 | /** High-level OAuth 2.0 functions */ 34 | export interface Helpers { 35 | /** 36 | * Handles the sign-in request and process for the given OAuth configuration 37 | * and redirects the client to the authorization URL. 38 | * 39 | * @see {@link https://github.com/denoland/deno_kv_oauth/tree/main#redirects-after-sign-in-and-sign-out} 40 | * 41 | * @example Usage 42 | * ```ts ignore 43 | * import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 44 | * 45 | * const oauthConfig = createGitHubOAuthConfig(); 46 | * const { signIn } = createHelpers(oauthConfig); 47 | * 48 | * Deno.serve(async (request) => await signIn(request)); 49 | * ``` 50 | */ 51 | signIn(request: Request, options?: SignInOptions): Promise; 52 | /** 53 | * Handles the OAuth callback request for the given OAuth configuration, and 54 | * then redirects the client to the success URL set in 55 | * {@linkcode Handlers.signIn}. The request URL must match the redirect URL 56 | * of the OAuth application. 57 | * 58 | * @example Usage 59 | * ```ts ignore 60 | * import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 61 | * 62 | * const oauthConfig = createGitHubOAuthConfig(); 63 | * const { handleCallback } = createHelpers(oauthConfig); 64 | * 65 | * Deno.serve(async (request) => { 66 | * const { response } = await handleCallback(request); 67 | * return response; 68 | * }); 69 | * ``` 70 | */ 71 | handleCallback(request: Request): Promise<{ 72 | response: Response; 73 | sessionId: string; 74 | tokens: Tokens; 75 | }>; 76 | /** 77 | * Handles the sign-out process, and then redirects the client to the given 78 | * success URL. 79 | * 80 | * @see {@link https://github.com/denoland/deno_kv_oauth/tree/main#redirects-after-sign-in-and-sign-out} 81 | * 82 | * @example Usage 83 | * ```ts ignore 84 | * import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 85 | * 86 | * const oauthConfig = createGitHubOAuthConfig(); 87 | * const { signOut } = createHelpers(oauthConfig); 88 | * 89 | * Deno.serve(async (request) => await signOut(request)); 90 | * ``` 91 | */ 92 | signOut(request: Request): Promise; 93 | /** 94 | * Gets the session ID from the cookie header of a request. This can be used to 95 | * check whether the client is signed-in and whether the session ID was created 96 | * on the server by checking if the return value is defined. 97 | * 98 | * @example Usage 99 | * ```ts ignore 100 | * import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 101 | * 102 | * const oauthConfig = createGitHubOAuthConfig(); 103 | * const { getSessionId } = createHelpers(oauthConfig); 104 | * 105 | * Deno.serve(async (request) => { 106 | * const sessionId = await getSessionId(request); 107 | * return Response.json({ sessionId }); 108 | * }); 109 | * ``` 110 | */ 111 | getSessionId(request: Request): Promise; 112 | } 113 | 114 | /** Options for {@linkcode createHelpers}. */ 115 | export interface CreateHelpersOptions { 116 | /** 117 | * Options for overwriting the default cookie options throughout each of the 118 | * helpers. 119 | */ 120 | cookieOptions?: Partial; 121 | } 122 | 123 | /** 124 | * Creates the full set of helpers with the given OAuth configuration and 125 | * options. 126 | * 127 | * @example Usage 128 | * ```ts ignore 129 | * // server.ts 130 | * import { 131 | * createGitHubOAuthConfig, 132 | * createHelpers, 133 | * } from "jsr:@deno/kv-oauth"; 134 | * 135 | * const { 136 | * signIn, 137 | * handleCallback, 138 | * signOut, 139 | * getSessionId, 140 | * } = createHelpers(createGitHubOAuthConfig(), { 141 | * cookieOptions: { 142 | * name: "__Secure-triple-choc", 143 | * domain: "news.site", 144 | * }, 145 | * }); 146 | * 147 | * async function handler(request: Request) { 148 | * const { pathname } = new URL(request.url); 149 | * switch (pathname) { 150 | * case "/oauth/signin": 151 | * return await signIn(request); 152 | * case "/oauth/callback": 153 | * const { response } = await handleCallback(request); 154 | * return response; 155 | * case "/oauth/signout": 156 | * return await signOut(request); 157 | * case "/protected-route": 158 | * return await getSessionId(request) === undefined 159 | * ? new Response("Unauthorized", { status: 401 }) 160 | * : new Response("You are allowed"); 161 | * default: 162 | * return new Response(null, { status: 404 }); 163 | * } 164 | * } 165 | * 166 | * Deno.serve(handler); 167 | * ``` 168 | */ 169 | export function createHelpers( 170 | oauthConfig: OAuth2ClientConfig, 171 | options?: CreateHelpersOptions, 172 | ): Helpers { 173 | return { 174 | async signIn(request: Request, options?: SignInOptions) { 175 | const state = crypto.randomUUID(); 176 | const { uri, codeVerifier } = await new OAuth2Client(oauthConfig) 177 | .code.getAuthorizationUri({ state }); 178 | 179 | if (options?.urlParams) { 180 | Object.entries(options.urlParams).forEach(([key, value]) => 181 | uri.searchParams.append(key, value) 182 | ); 183 | } 184 | 185 | const oauthSessionId = crypto.randomUUID(); 186 | const cookie: Cookie = { 187 | ...COOKIE_BASE, 188 | name: getCookieName(OAUTH_COOKIE_NAME, isHttps(request.url)), 189 | value: oauthSessionId, 190 | secure: isHttps(request.url), 191 | /** 192 | * A maximum authorization code lifetime of 10 minutes is recommended. 193 | * This cookie lifetime matches that value. 194 | * 195 | * @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2} 196 | */ 197 | maxAge: 10 * 60, 198 | }; 199 | const successUrl = getSuccessUrl(request); 200 | await setOAuthSession( 201 | oauthSessionId, 202 | { state, codeVerifier, successUrl }, 203 | { 204 | expireIn: cookie.maxAge! * SECOND, 205 | }, 206 | ); 207 | const response = redirect(uri.toString()); 208 | setCookie(response.headers, cookie); 209 | return response; 210 | }, 211 | async handleCallback(request: Request) { 212 | const oauthCookieName = getCookieName( 213 | OAUTH_COOKIE_NAME, 214 | isHttps(request.url), 215 | ); 216 | const oauthSessionId = getCookies(request.headers)[oauthCookieName]; 217 | if (oauthSessionId === undefined) { 218 | throw new Error("OAuth cookie not found"); 219 | } 220 | const oauthSession = await getAndDeleteOAuthSession(oauthSessionId); 221 | 222 | const tokens = await new OAuth2Client(oauthConfig) 223 | .code.getToken(request.url, oauthSession); 224 | 225 | const sessionId = crypto.randomUUID(); 226 | const response = redirect(oauthSession.successUrl); 227 | const cookie: Cookie = { 228 | ...COOKIE_BASE, 229 | name: getCookieName(SITE_COOKIE_NAME, isHttps(request.url)), 230 | value: sessionId, 231 | secure: isHttps(request.url), 232 | ...options?.cookieOptions, 233 | }; 234 | setCookie(response.headers, cookie); 235 | await setSiteSession( 236 | sessionId, 237 | cookie.maxAge ? cookie.maxAge * SECOND : undefined, 238 | ); 239 | 240 | return { 241 | response, 242 | sessionId, 243 | tokens, 244 | }; 245 | }, 246 | async signOut(request: Request) { 247 | const successUrl = getSuccessUrl(request); 248 | const response = redirect(successUrl); 249 | 250 | const sessionId = getSessionIdCookie( 251 | request, 252 | options?.cookieOptions?.name, 253 | ); 254 | if (sessionId === undefined) return response; 255 | await deleteSiteSession(sessionId); 256 | 257 | const cookieName = options?.cookieOptions?.name ?? 258 | getCookieName(SITE_COOKIE_NAME, isHttps(request.url)); 259 | deleteCookie(response.headers, cookieName, { 260 | path: COOKIE_BASE.path, 261 | ...options?.cookieOptions, 262 | }); 263 | return response; 264 | }, 265 | async getSessionId(request: Request) { 266 | const sessionId = getSessionIdCookie( 267 | request, 268 | options?.cookieOptions?.name, 269 | ); 270 | return (sessionId !== undefined && await isSiteSession(sessionId)) 271 | ? sessionId 272 | : undefined; 273 | }, 274 | }; 275 | } 276 | -------------------------------------------------------------------------------- /lib/create_helpers_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 the Deno authors. All rights reserved. MIT license. 2 | import { createHelpers } from "./create_helpers.ts"; 3 | import { 4 | assert, 5 | assertEquals, 6 | assertNotEquals, 7 | assertRejects, 8 | } from "@std/assert"; 9 | import { returnsNext, stub } from "@std/testing/mock"; 10 | import { 11 | getAndDeleteOAuthSession, 12 | setOAuthSession, 13 | setSiteSession, 14 | } from "./_kv.ts"; 15 | import { OAUTH_COOKIE_NAME, SITE_COOKIE_NAME } from "./_http.ts"; 16 | import { 17 | assertRedirect, 18 | randomOAuthConfig, 19 | randomOAuthSession, 20 | randomTokensBody, 21 | } from "./_test_utils.ts"; 22 | import { type Cookie, getSetCookies } from "@std/http"; 23 | 24 | Deno.test("signIn() returns a response that signs-in the user", async () => { 25 | const { signIn } = createHelpers(randomOAuthConfig()); 26 | const request = new Request("http://example.com/signin"); 27 | const response = await signIn(request); 28 | assertRedirect(response); 29 | 30 | const [setCookie] = getSetCookies(response.headers); 31 | assert(setCookie !== undefined); 32 | assertEquals(setCookie.name, OAUTH_COOKIE_NAME); 33 | assertEquals(setCookie.httpOnly, true); 34 | assertEquals(setCookie.maxAge, 10 * 60); 35 | assertEquals(setCookie.sameSite, "Lax"); 36 | assertEquals(setCookie.path, "/"); 37 | 38 | const oauthSessionId = setCookie.value; 39 | const oauthSession = await getAndDeleteOAuthSession(oauthSessionId); 40 | assertNotEquals(oauthSession, null); 41 | const location = response.headers.get("location")!; 42 | const state = new URL(location).searchParams.get("state"); 43 | assertEquals(oauthSession!.state, state); 44 | }); 45 | 46 | Deno.test("signIn() returns a redirect response with URL params", async () => { 47 | const { signIn } = createHelpers(randomOAuthConfig()); 48 | const request = new Request("http://example.com/signin"); 49 | const response = await signIn(request, { 50 | urlParams: { foo: "bar" }, 51 | }); 52 | assertRedirect(response); 53 | const location = response.headers.get("location")!; 54 | assertEquals(new URL(location).searchParams.get("foo"), "bar"); 55 | }); 56 | 57 | Deno.test("handleCallback() rejects for no OAuth cookie", async () => { 58 | const { handleCallback } = createHelpers(randomOAuthConfig()); 59 | const request = new Request("http://example.com"); 60 | await assertRejects( 61 | async () => await handleCallback(request), 62 | Error, 63 | "OAuth cookie not found", 64 | ); 65 | }); 66 | 67 | Deno.test("handleCallback() rejects for non-existent OAuth session", async () => { 68 | const { handleCallback } = createHelpers(randomOAuthConfig()); 69 | const request = new Request("http://example.com", { 70 | headers: { cookie: `${OAUTH_COOKIE_NAME}=xxx` }, 71 | }); 72 | await assertRejects( 73 | async () => await handleCallback(request), 74 | Deno.errors.NotFound, 75 | "OAuth session not found", 76 | ); 77 | }); 78 | 79 | Deno.test("handleCallback() deletes the OAuth session KV entry", async () => { 80 | const { handleCallback } = createHelpers(randomOAuthConfig()); 81 | const oauthSessionId = crypto.randomUUID(); 82 | const oauthSession = randomOAuthSession(); 83 | await setOAuthSession(oauthSessionId, oauthSession, { expireIn: 1_000 }); 84 | const request = new Request("http://example.com", { 85 | headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` }, 86 | }); 87 | await assertRejects(() => handleCallback(request)); 88 | await assertRejects( 89 | async () => await getAndDeleteOAuthSession(oauthSessionId), 90 | Deno.errors.NotFound, 91 | "OAuth session not found", 92 | ); 93 | }); 94 | 95 | Deno.test("handleCallback() correctly handles the callback response", async () => { 96 | const { handleCallback } = createHelpers(randomOAuthConfig()); 97 | const tokensBody = randomTokensBody(); 98 | const fetchStub = stub( 99 | globalThis, 100 | "fetch", 101 | returnsNext([Promise.resolve(Response.json(tokensBody))]), 102 | ); 103 | const oauthSessionId = crypto.randomUUID(); 104 | const oauthSession = randomOAuthSession(); 105 | await setOAuthSession(oauthSessionId, oauthSession, { expireIn: 1_000 }); 106 | const searchParams = new URLSearchParams({ 107 | "response_type": "code", 108 | "client_id": "clientId", 109 | "code_challenge_method": "S256", 110 | code: "code", 111 | state: oauthSession.state, 112 | }); 113 | const request = new Request(`http://example.com/callback?${searchParams}`, { 114 | headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` }, 115 | }); 116 | const { response, tokens, sessionId } = await handleCallback(request); 117 | fetchStub.restore(); 118 | 119 | assertRedirect(response, oauthSession.successUrl); 120 | assertEquals( 121 | response.headers.get("set-cookie"), 122 | `site-session=${sessionId}; HttpOnly; Max-Age=7776000; SameSite=Lax; Path=/`, 123 | ); 124 | assertEquals(tokens.accessToken, tokensBody.access_token); 125 | assertEquals(typeof sessionId, "string"); 126 | await assertRejects( 127 | async () => await getAndDeleteOAuthSession(oauthSessionId), 128 | Deno.errors.NotFound, 129 | "OAuth session not found", 130 | ); 131 | }); 132 | 133 | Deno.test("handleCallback() correctly handles the callback response with options", async () => { 134 | const tokensBody = randomTokensBody(); 135 | const fetchStub = stub( 136 | globalThis, 137 | "fetch", 138 | returnsNext([Promise.resolve(Response.json(tokensBody))]), 139 | ); 140 | const oauthSessionId = crypto.randomUUID(); 141 | const oauthSession = randomOAuthSession(); 142 | await setOAuthSession(oauthSessionId, oauthSession, { expireIn: 1_000 }); 143 | const searchParams = new URLSearchParams({ 144 | "response_type": "code", 145 | "client_id": "clientId", 146 | "code_challenge_method": "S256", 147 | code: "code", 148 | state: oauthSession.state, 149 | }); 150 | const request = new Request(`http://example.com/callback?${searchParams}`, { 151 | headers: { cookie: `${OAUTH_COOKIE_NAME}=${oauthSessionId}` }, 152 | }); 153 | const cookieOptions: Partial = { 154 | name: "triple-choc", 155 | maxAge: 420, 156 | domain: "example.com", 157 | }; 158 | const { handleCallback } = createHelpers(randomOAuthConfig(), { 159 | cookieOptions, 160 | }); 161 | const { response, tokens, sessionId } = await handleCallback(request); 162 | fetchStub.restore(); 163 | 164 | assertRedirect(response, oauthSession.successUrl); 165 | assertEquals( 166 | response.headers.get("set-cookie"), 167 | `${cookieOptions.name}=${sessionId}; HttpOnly; Max-Age=${cookieOptions.maxAge}; Domain=${cookieOptions.domain}; SameSite=Lax; Path=/`, 168 | ); 169 | assertEquals(tokens.accessToken, tokensBody.access_token); 170 | assertEquals(typeof sessionId, "string"); 171 | await assertRejects( 172 | async () => await getAndDeleteOAuthSession(oauthSessionId), 173 | Deno.errors.NotFound, 174 | "OAuth session not found", 175 | ); 176 | }); 177 | 178 | Deno.test("getSessionId() returns undefined when cookie is not defined", async () => { 179 | const { getSessionId } = createHelpers(randomOAuthConfig()); 180 | const request = new Request("http://example.com"); 181 | 182 | assertEquals(await getSessionId(request), undefined); 183 | }); 184 | 185 | Deno.test("getSessionId() returns valid session ID", async () => { 186 | const { getSessionId } = createHelpers(randomOAuthConfig()); 187 | const sessionId = crypto.randomUUID(); 188 | await setSiteSession(sessionId); 189 | const request = new Request("http://example.com", { 190 | headers: { 191 | cookie: `${SITE_COOKIE_NAME}=${sessionId}`, 192 | }, 193 | }); 194 | 195 | assertEquals(await getSessionId(request), sessionId); 196 | }); 197 | 198 | Deno.test("getSessionId() returns valid session ID when cookie name is defined", async () => { 199 | const sessionId = crypto.randomUUID(); 200 | await setSiteSession(sessionId); 201 | const name = "triple-choc"; 202 | const { getSessionId } = createHelpers(randomOAuthConfig(), { 203 | cookieOptions: { name }, 204 | }); 205 | const request = new Request("http://example.com", { 206 | headers: { 207 | cookie: `${name}=${sessionId}`, 208 | }, 209 | }); 210 | 211 | assertEquals(await getSessionId(request), sessionId); 212 | }); 213 | 214 | Deno.test("signOut() returns a redirect response if the user is not signed-in", async () => { 215 | const { signOut } = createHelpers(randomOAuthConfig()); 216 | const request = new Request("http://example.com/signout"); 217 | const response = await signOut(request); 218 | 219 | assertRedirect(response, "/"); 220 | }); 221 | 222 | Deno.test("signOut() returns a response that signs out the signed-in user", async () => { 223 | const { signOut } = createHelpers(randomOAuthConfig()); 224 | const sessionId = crypto.randomUUID(); 225 | await setSiteSession(sessionId); 226 | const request = new Request("http://example.com/signout", { 227 | headers: { 228 | cookie: `${SITE_COOKIE_NAME}=${sessionId}`, 229 | }, 230 | }); 231 | const response = await signOut(request); 232 | 233 | assertRedirect(response, "/"); 234 | assertEquals( 235 | response.headers.get("set-cookie"), 236 | `${SITE_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, 237 | ); 238 | }); 239 | 240 | Deno.test("signOut() returns a response that signs out the signed-in user with cookie options", async () => { 241 | const cookieOptions = { 242 | name: "triple-choc", 243 | domain: "example.com", 244 | path: "/path", 245 | }; 246 | const { signOut } = createHelpers(randomOAuthConfig(), { cookieOptions }); 247 | const sessionId = crypto.randomUUID(); 248 | await setSiteSession(sessionId); 249 | const request = new Request("http://example.com/signout", { 250 | headers: { 251 | cookie: `${cookieOptions.name}=${sessionId}`, 252 | }, 253 | }); 254 | const response = await signOut(request); 255 | 256 | assertRedirect(response, "/"); 257 | assertEquals( 258 | response.headers.get("set-cookie"), 259 | `${cookieOptions.name}=; Domain=${cookieOptions.domain}; Path=${cookieOptions.path}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, 260 | ); 261 | }); 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno KV OAuth (Beta) 2 | 3 | [![JSR](https://jsr.io/badges/@deno/kv-oauth)](https://jsr.io/@deno/kv-oauth) 4 | [![CI](https://github.com/denoland/deno_kv_oauth/actions/workflows/ci.yml/badge.svg)](https://github.com/denoland/deno_kv_oauth/actions/workflows/ci.yml) 5 | [![codecov](https://codecov.io/gh/denoland/deno_kv_oauth/branch/main/graph/badge.svg?token=UZ570U128Z)](https://codecov.io/gh/denoland/deno_kv_oauth) 6 | [![Built with the Deno Standard Library](https://raw.githubusercontent.com/denoland/deno_std/main/badge.svg)](https://deno.land/std) 7 | 8 | High-level OAuth 2.0 powered by [Deno KV](https://deno.com/kv). 9 | 10 | ## Features 11 | 12 | - Uses [Deno KV](https://deno.com/kv) for persistent session storage. 13 | - Uses [oauth2_client](https://deno.land/x/oauth2_client) for OAuth workflows. 14 | - Automatically handles the authorization code flow with 15 | [Proof Key for Code Exchange (PKCE)](https://www.oauth.com/oauth2-servers/pkce/) 16 | and client redirection. 17 | - Provides [pre-defined OAuth configurations](#pre-defined-oauth-configurations) 18 | for popular providers. 19 | - Works locally and in the cloud, including 20 | [Deno Deploy](https://deno.com/deploy). 21 | - Based on the [Web API](https://developer.mozilla.org/en-US/docs/Web/API)'s 22 | [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and 23 | [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) 24 | interfaces. 25 | - Works with [Fresh](https://fresh.deno.dev/), 26 | [`Deno.serve()`](https://deno.land/api?s=Deno.serve&unstable=) and 27 | [Oak](https://oakserver.github.io/oak/) and other web frameworks. 28 | - Supports [custom session cookie options](#get-started-with-cookie-options) 29 | 30 | ## Documentation 31 | 32 | Check out the full documentation and API reference 33 | [here](https://jsr.io/@deno/kv-oauth/doc). 34 | 35 | ## How-to 36 | 37 | ### Get Started with a Pre-Defined OAuth Configuration 38 | 39 | See [here](#providers) for the list of OAuth providers with pre-defined 40 | configurations. 41 | 42 | 1. Create your OAuth application for your given provider. 43 | 44 | 1. Create your web server using Deno KV OAuth's request handlers, helpers and 45 | pre-defined OAuth configuration. 46 | 47 | ```ts ignore 48 | // server.ts 49 | import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 50 | 51 | const oauthConfig = createGitHubOAuthConfig(); 52 | const { 53 | signIn, 54 | handleCallback, 55 | getSessionId, 56 | signOut, 57 | } = createHelpers(oauthConfig); 58 | 59 | async function handler(request: Request) { 60 | const { pathname } = new URL(request.url); 61 | switch (pathname) { 62 | case "/oauth/signin": 63 | return await signIn(request); 64 | case "/oauth/callback": 65 | const { response } = await handleCallback(request); 66 | return response; 67 | case "/oauth/signout": 68 | return await signOut(request); 69 | case "/protected-route": 70 | return await getSessionId(request) === undefined 71 | ? new Response("Unauthorized", { status: 401 }) 72 | : new Response("You are allowed"); 73 | default: 74 | return new Response(null, { status: 404 }); 75 | } 76 | } 77 | 78 | Deno.serve(handler); 79 | ``` 80 | 81 | 1. Start your server with the necessary 82 | [environment variables](#environment-variables). 83 | 84 | ```bash 85 | GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts 86 | ``` 87 | 88 | > Check out a full implementation in the [demo source code](./demo.ts) which 89 | > runs https://kv-oauth.deno.dev. 90 | 91 | ### Get Started with a Custom OAuth Configuration 92 | 93 | 1. Create your OAuth application for your given provider. 94 | 95 | 1. Create your web server using Deno KV OAuth's request handlers and helpers, 96 | and custom OAuth configuration. 97 | 98 | ```ts ignore 99 | // server.ts 100 | import { 101 | createHelpers, 102 | getRequiredEnv, 103 | type OAuth2ClientConfig, 104 | } from "jsr:@deno/kv-oauth"; 105 | 106 | const oauthConfig: OAuth2ClientConfig = { 107 | clientId: getRequiredEnv("CUSTOM_CLIENT_ID"), 108 | clientSecret: getRequiredEnv("CUSTOM_CLIENT_SECRET"), 109 | authorizationEndpointUri: "https://custom.com/oauth/authorize", 110 | tokenUri: "https://custom.com/oauth/token", 111 | redirectUri: "https://my-site.com/another-dir/callback", 112 | }; 113 | const { 114 | signIn, 115 | handleCallback, 116 | getSessionId, 117 | signOut, 118 | } = createHelpers(oauthConfig); 119 | 120 | async function handler(request: Request) { 121 | const { pathname } = new URL(request.url); 122 | switch (pathname) { 123 | case "/oauth/signin": 124 | return await signIn(request); 125 | case "/another-dir/callback": 126 | const { response } = await handleCallback(request); 127 | return response; 128 | case "/oauth/signout": 129 | return await signOut(request); 130 | case "/protected-route": 131 | return await getSessionId(request) === undefined 132 | ? new Response("Unauthorized", { status: 401 }) 133 | : new Response("You are allowed"); 134 | default: 135 | return new Response(null, { status: 404 }); 136 | } 137 | } 138 | 139 | Deno.serve(handler); 140 | ``` 141 | 142 | 1. Start your server with the necessary 143 | [environment variables](#environment-variables). 144 | 145 | ```bash 146 | CUSTOM_CLIENT_ID=xxx CUSTOM_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts 147 | ``` 148 | 149 | ### Get Started with Cookie Options 150 | 151 | This is required for OAuth solutions that span more than one sub-domain. 152 | 153 | 1. Create your OAuth application for your given provider. 154 | 155 | 1. Create your web server using Deno KV OAuth's helpers factory function with 156 | cookie options defined. 157 | 158 | ```ts ignore 159 | // server.ts 160 | import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 161 | 162 | const { 163 | signIn, 164 | handleCallback, 165 | signOut, 166 | getSessionId, 167 | } = createHelpers(createGitHubOAuthConfig(), { 168 | cookieOptions: { 169 | name: "__Secure-triple-choc", 170 | domain: "news.site", 171 | }, 172 | }); 173 | 174 | async function handler(request: Request) { 175 | const { pathname } = new URL(request.url); 176 | switch (pathname) { 177 | case "/oauth/signin": 178 | return await signIn(request); 179 | case "/oauth/callback": 180 | const { response } = await handleCallback(request); 181 | return response; 182 | case "/oauth/signout": 183 | return await signOut(request); 184 | case "/protected-route": 185 | return await getSessionId(request) === undefined 186 | ? new Response("Unauthorized", { status: 401 }) 187 | : new Response("You are allowed"); 188 | default: 189 | return new Response(null, { status: 404 }); 190 | } 191 | } 192 | 193 | Deno.serve(handler); 194 | ``` 195 | 196 | 1. Start your server with the necessary 197 | [environment variables](#environment-variables). 198 | 199 | ```bash 200 | GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts 201 | ``` 202 | 203 | ### Get Started with [Fresh](https://fresh.deno.dev/) 204 | 205 | 1. Create your OAuth application for your given provider. 206 | 207 | 1. Create your OAuth configuration and Fresh plugin. 208 | 209 | ```ts ignore 210 | // plugins/kv_oauth.ts 211 | import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth"; 212 | import type { Plugin } from "$fresh/server.ts"; 213 | 214 | const { signIn, handleCallback, signOut, getSessionId } = createHelpers( 215 | createGitHubOAuthConfig(), 216 | ); 217 | 218 | export default { 219 | name: "kv-oauth", 220 | routes: [ 221 | { 222 | path: "/signin", 223 | async handler(req) { 224 | return await signIn(req); 225 | }, 226 | }, 227 | { 228 | path: "/callback", 229 | async handler(req) { 230 | // Return object also includes `accessToken` and `sessionId` properties. 231 | const { response } = await handleCallback(req); 232 | return response; 233 | }, 234 | }, 235 | { 236 | path: "/signout", 237 | async handler(req) { 238 | return await signOut(req); 239 | }, 240 | }, 241 | { 242 | path: "/protected", 243 | async handler(req) { 244 | return await getSessionId(req) === undefined 245 | ? new Response("Unauthorized", { status: 401 }) 246 | : new Response("You are allowed"); 247 | }, 248 | }, 249 | ], 250 | } as Plugin; 251 | ``` 252 | 253 | 1. [Add the plugin to your Fresh app.](https://fresh.deno.dev/docs/concepts/plugins) 254 | 255 | 1. Start your Fresh server with the necessary 256 | [environment variables](#environment-variables). 257 | 258 | ```bash 259 | GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno task start 260 | ``` 261 | 262 | ### Run the Demo Locally 263 | 264 | The demo uses GitHub as the OAuth provider. You can change the OAuth 265 | configuration by setting the `oauthConfig` constant as mentioned above. 266 | 267 | 1. Create your OAuth application for your given provider. 268 | 269 | 1. Start the demo with the necessary 270 | [environment variables](#environment-variables). 271 | 272 | ```bash 273 | TWITTER_CLIENT_ID=xxx TWITTER_CLIENT_SECRET=xxx deno task demo 274 | ``` 275 | 276 | ## Concepts 277 | 278 | ### Redirects after Sign-In and Sign-Out 279 | 280 | The URL that the client is redirected to upon successful sign-in or sign-out is 281 | determined by the request made to the sign-in or sign-out endpoint. This value 282 | is set in the following order of precedence: 283 | 284 | 1. The value of the `success_url` URL parameter of the request URL, if defined. 285 | E.g. a request to `http://example.com/signin?success_url=/success` redirects 286 | the client to `/success` after successful sign-in. 287 | 2. The value of the 288 | [`Referer`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) 289 | header, if of the same origin as the request. E.g. a request to 290 | `http://example.com/signin` with `Referer` header `http://example.com/about` 291 | redirects the client to `http://example.com/about` after successful sign-in. 292 | 3. The root path, "/". E.g. a request to `http://example.com/signin` without the 293 | `Referer` header redirects the client to `http://example.com` after 294 | successful sign-in. 295 | 296 | ### Pre-Defined OAuth Configurations 297 | 298 | #### Providers 299 | 300 | The following providers have pre-defined OAuth configurations: 301 | 302 | 1. [Auth0](https://jsr.io/@deno/kv-oauth/doc/~/createAuth0OAuthConfig) 303 | 1. [AWS Cognito User Pool](https://jsr.io/@deno/kv-oauth/doc/~/createAwsCognitoOAuthConfig) 304 | 1. [AzureAD](https://jsr.io/@deno/kv-oauth/doc/~/createAzureADAuthConfig) 305 | 1. [AzureADB2C](https://jsr.io/@deno/kv-oauth/doc/~/createAzureADB2CAuthConfig) 306 | 1. [Clerk](https://jsr.io/@deno/kv-oauth/doc/~/createClerkOAuthConfig) 307 | 1. [Discord](https://jsr.io/@deno/kv-oauth/doc/~/createDiscordOAuthConfig) 308 | 1. [Dropbox](https://jsr.io/@deno/kv-oauth/doc/~/createDropboxOAuthConfig) 309 | 1. [Facebook](https://jsr.io/@deno/kv-oauth/doc/~/createFacebookOAuthConfig) 310 | 1. [GitHub](https://jsr.io/@deno/kv-oauth/doc/~/createGitHubOAuthConfig) 311 | 1. [GitLab](https://jsr.io/@deno/kv-oauth/doc/~/createGitLabOAuthConfig) 312 | 1. [Google](https://jsr.io/@deno/kv-oauth/doc/~/createGoogleOAuthConfig) 313 | 1. [Logto](https://jsr.io/@deno/kv-oauth/doc/~/createLogtoOAuthConfig) 314 | 1. [Notion](https://jsr.io/@deno/kv-oauth/doc/~/createNotionOAuthConfig) 315 | 1. [Okta](https://jsr.io/@deno/kv-oauth/doc/~/createOktaOAuthConfig) 316 | 1. [Patreon](https://jsr.io/@deno/kv-oauth/doc/~/createPatreonOAuthConfig) 317 | 1. [Slack](https://jsr.io/@deno/kv-oauth/doc/~/createSlackOAuthConfig) 318 | 1. [Spotify](https://jsr.io/@deno/kv-oauth/doc/~/createSpotifyOAuthConfig) 319 | 1. [Twitter](https://jsr.io/@deno/kv-oauth/doc/~/createTwitterOAuthConfig) 320 | 321 | #### Environment Variables 322 | 323 | These must be set when starting a server with a pre-defined OAuth configuration. 324 | Replace the `PROVIDER` prefix with your given OAuth provider's name when 325 | starting your server. E.g. `DISCORD`, `GOOGLE`, or `SLACK`. 326 | 327 | 1. `PROVIDER_CLIENT_ID` - 328 | [Client ID](https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/) 329 | of a given OAuth application. 330 | 1. `PROVIDER_CLIENT_SECRET` - 331 | [Client secret](https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/) 332 | of a given OAuth application. 333 | 1. `PROVIDER_DOMAIN` (optional) - Server domain of a given OAuth application. 334 | Required for Auth0, AzureADB2C, AWS Cognito, and Okta. 335 | 336 | > Note: reading environment variables requires the 337 | > `--allow-env[=...]` permission flag. See 338 | > [the manual](https://deno.com/manual/basics/permissions) for further details. 339 | 340 | ## Built with Deno KV OAuth 341 | 342 | 1. [Deno KV OAuth live demo]() 343 | 1. [Deno SaaSKit](https://saaskit.deno.dev/) - A modern SaaS template built on 344 | Fresh and uses a custom Deno KV OAuth plugin. 345 | 1. [KV SketchBook](https://hashrock-kv-sketchbook.deno.dev/) - Dead simple 346 | sketchbook app. 347 | 1. [Fresh + Deno KV OAuth demo](https://github.com/denoland/fresh-deno-kv-oauth-demo) - 348 | A demo of Deno KV OAuth working in the 349 | [Fresh web framework](https://fresh.deno.dev/). 350 | 1. [Oak + Deno KV OAuth demo](https://dash.deno.com/playground/oak-deno-kv-oauth-demo) - 351 | A demo of Deno KV OAuth working in the 352 | [Oak web framework](https://oakserver.github.io/oak/). 353 | 1. [Ultra + Deno KV OAuth demo](https://github.com/mbhrznr/ultra-deno-kv-oauth-demo) - 354 | A demo of Deno KV OAuth working in the 355 | [Ultra web framework](https://ultrajs.dev/). 356 | 1. [Hono + Deno KV OAuth demo](https://dash.deno.com/playground/hono-deno-kv-oauth) - 357 | A demo of Deno KV OAuth working in the 358 | [Hono web framework](https://hono.dev/). 359 | 1. [Cheetah + Deno KV OAuth demo](https://dash.deno.com/playground/cheetah-deno-kv-oauth) - 360 | A demo of Deno KV OAuth working in the 361 | [Cheetah web framework](https://cheetah.mod.land/). 362 | 1. [Paquet](https://paquet.app) - A web app shop 363 | 1. [Fastro + Deno KV OAuth live demo](https://fastro.dev/auth) - A simple, 364 | reusable fastro module that implements Deno KV. 365 | 366 | > Do you have a project powered by Deno KV OAuth that you'd like to share? Feel 367 | > free to let us know in a new issue. 368 | 369 | ## Known Issues 370 | 371 | - Twitch is not supported as an OAuth provider because it does not support PKCE. 372 | See #79 and 373 | [this post](https://twitch.uservoice.com/forums/310213-developers/suggestions/39785686-add-pkce-support-to-the-oauth2-0-authorization-cod) 374 | for more information. 375 | 376 | ## Contributing Guide 377 | 378 | Check out the contributing guide [here](.github/CONTRIBUTING.md). 379 | 380 | ## Security Policy 381 | 382 | Check out the security policy [here](.github/SECURITY.md). 383 | --------------------------------------------------------------------------------