Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Providers create config instead of client #186

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ routes.
```ts
// main.ts
import { start } from "$fresh/server.ts";
import { createGitHubOAuth2Client } from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";
import { createGitHubOAuthConfig } from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";
import { kvOAuthPlugin } from "https://deno.land/x/deno_kv_oauth@$VERSION/fresh.ts";
import manifest from "./fresh.gen.ts";

await start(manifest, {
plugins: [
kvOAuthPlugin(createGitHubOAuth2Client()),
kvOAuthPlugin(createGitHubOAuthConfig()),
],
});
```
Expand Down Expand Up @@ -107,9 +107,9 @@ provider you like.

```ts
// Pre-configured OAuth 2.0 client
import { createGitHubOAuth2Client } from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";
import { createGitHubOAuthConfig } from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";

const oauth2Client = createGitHubOAuth2Client();
const oauthConfig = createGitHubOAuthConfig();
```

1. Using the OAuth 2.0 client instance, insert the authentication flow functions
Expand All @@ -118,20 +118,20 @@ provider you like.
```ts
// Sign-in, callback and sign-out handlers
import {
createGitHubOAuth2Client,
createGitHubOAuthConfig,
handleCallback,
signIn,
signOut,
} from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";

const oauth2Client = createGitHubOAuth2Client();
const oauthConfig = createGitHubOAuthConfig();

async function handleSignIn(request: Request) {
return await signIn(request, oauth2Client);
return await signIn(request, oauthConfig);
}

async function handleOAuth2Callback(request: Request) {
return await handleCallback(request, oauth2Client);
return await handleCallback(request, oauthConfig);
}

async function handleSignOut(request: Request) {
Expand All @@ -144,12 +144,12 @@ provider you like.
```ts
// Protected route
import {
createGitHubOAuth2Client,
createGitHubOAuthConfig,
getSessionAccessToken,
getSessionId,
} from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";

const oauth2Client = createGitHubOAuth2Client();
const oauthConfig = createGitHubOAuthConfig();

async function getGitHubUser(accessToken: string): Promise<any> {
const response = await fetch("https://api.github.com/user", {
Expand All @@ -168,7 +168,7 @@ provider you like.

if (!hasSessionIdCookie) return new Response(null, { status: 404 });

const accessToken = await getSessionAccessToken(oauth2Client, sessionId);
const accessToken = await getSessionAccessToken(oauthConfig, sessionId);
if (accessToken === null) return new Response(null, { status: 400 });

try {
Expand Down Expand Up @@ -198,51 +198,51 @@ provider you like.
await clearOAuthSessionsAndTokens();
```

### Pre-configured OAuth 2.0 Clients
### Pre-configured OAuth 2.0 Configs

This module comes with a suite of pre-configured OAuth 2.0 clients for the
This module comes with a suite of pre-configured OAuth 2.0 configs for the
following providers:

1. [Auth0](https://deno.land/x/deno_kv_oauth/mod.ts?s=createAuth0OAuth2Client)
1. [Discord](https://deno.land/x/deno_kv_oauth/mod.ts?s=createDiscordOAuth2Client)
1. [Dropbox](https://deno.land/x/deno_kv_oauth/mod.ts?s=createDropboxOAuth2Client)
1. [Facebook](https://deno.land/x/deno_kv_oauth/mod.ts?s=createFacebookOAuth2Client)
1. [GitHub](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGitHubOAuth2Client)
1. [GitLab](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGitLabOAuth2Client)
1. [Google](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGoogleOAuth2Client)
1. [Notion](https://deno.land/x/deno_kv_oauth/mod.ts?s=createNotionOAuth2Client)
1. [Okta](https://deno.land/x/deno_kv_oauth/mod.ts?s=createOktaOAuth2Client)
1. [Patreon](https://deno.land/x/deno_kv_oauth/mod.ts?s=createPatreonOAuth2Client)
1. [Slack](https://deno.land/x/deno_kv_oauth/mod.ts?s=createSlackOAuth2Client)
1. [Spotify](https://deno.land/x/deno_kv_oauth/mod.ts?s=createSpotifyOAuth2Client)
1. [Twitter](https://deno.land/x/deno_kv_oauth/mod.ts?s=createTwitterOAuth2Client)
1. [Auth0](https://deno.land/x/deno_kv_oauth/mod.ts?s=createAuth0OAuthConfig)
1. [Discord](https://deno.land/x/deno_kv_oauth/mod.ts?s=createDiscordOAuthConfig)
1. [Dropbox](https://deno.land/x/deno_kv_oauth/mod.ts?s=createDropboxOAuthConfig)
1. [Facebook](https://deno.land/x/deno_kv_oauth/mod.ts?s=createFacebookOAuthConfig)
1. [GitHub](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGitHubOAuthConfig)
1. [GitLab](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGitLabOAuthConfig)
1. [Google](https://deno.land/x/deno_kv_oauth/mod.ts?s=createGoogleOAuthConfig)
1. [Notion](https://deno.land/x/deno_kv_oauth/mod.ts?s=createNotionOAuthConfig)
1. [Okta](https://deno.land/x/deno_kv_oauth/mod.ts?s=createOktaOAuthConfig)
1. [Patreon](https://deno.land/x/deno_kv_oauth/mod.ts?s=createPatreonOAuthConfig)
1. [Slack](https://deno.land/x/deno_kv_oauth/mod.ts?s=createSlackOAuthConfig)
1. [Spotify](https://deno.land/x/deno_kv_oauth/mod.ts?s=createSpotifyOAuthConfig)
1. [Twitter](https://deno.land/x/deno_kv_oauth/mod.ts?s=createTwitterOAuthConfig)

Each function is typed so that their respective platform's requirements are met.

> If there's a pre-configured OAuth 2.0 client for a provider you'd like added,
> please submit a pull request or
> [create a new issue](https://github.com/denoland/deno_kv_oauth/issues/new).

### Custom OAuth 2.0 Client
### Custom OAuth 2.0 Configs

If you require custom OAuth 2.0 configuration, you must define your `client`
using
[`new OAuth2Client()`](https://deno.land/x/oauth2_client/mod.ts?s=OAuth2Client)
from the [`oauth2_client` module](https://deno.land/x/oauth2_client/mod.ts).
E.g.:
If you require a custom OAuth 2.0 configuration, use
[createCustomOAuthConfig](https://deno.land/x/deno_kv_oauth/mod.ts?s=createCustomOAuthConfig):

```ts
import { OAuth2Client } from "https://deno.land/x/oauth2_client/mod.ts";
import { createCustomOAuthConfig } from "https://deno.land/x/oauth2_client/mod.ts";

const client = new OAuth2Client({
clientId: Deno.env.get("CUSTOM_CLIENT_ID")!,
clientSecret: Deno.env.get("CUSTOM_CLIENT_SECRET")!,
const oauthConfig = createCustomOAuthConfig({
name: "Custom",
authorizationEndpointUri: "https://custom.com/oauth/authorize",
tokenUri: "https://custom.com/oauth/token",
redirectUri: "https://my-site.com",
scope: ["email"],
redirectUri: "/callback",
});
```

this will even use the environment variables: `${name}_CLIENT_ID`,
`${name}_CLIENT_SECRET`, where `name` is given above (uppercased).

### Environment Variables

- `KV_PATH` (optional) - defines the path that Deno KV uses. See
Expand Down
117 changes: 65 additions & 52 deletions demo.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,96 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { loadSync, type OAuth2ClientConfig, Status } from "./dev_deps.ts";
import { loadSync, Status } from "./dev_deps.ts";
import {
createAuth0OAuth2Client,
createDiscordOAuth2Client,
createDropboxOAuth2Client,
createFacebookOAuth2Client,
createGitHubOAuth2Client,
createGitLabOAuth2Client,
createGoogleOAuth2Client,
createNotionOAuth2Client,
createOktaOAuth2Client,
createPatreonOAuth2Client,
createSlackOAuth2Client,
createSpotifyOAuth2Client,
createTwitterOAuth2Client,
createAuth0OAuthConfig,
createDiscordOAuthConfig,
createDropboxOAuthConfig,
createFacebookOAuthConfig,
createGitHubOAuthConfig,
createGitLabOAuthConfig,
createGoogleOAuthConfig,
createNotionOAuthConfig,
createOktaOAuthConfig,
createPatreonOAuthConfig,
createSlackOAuthConfig,
createSpotifyOAuthConfig,
createTwitterOAuthConfig,
getSessionAccessToken,
getSessionId,
handleCallback,
type OAuthConfig,
type OAuthUserConfig,
signIn,
signOut,
} from "./mod.ts";

loadSync({ export: true });

type CreateOAuthConfigFn = (config: OAuthUserConfig) => OAuthConfig;

const providers: Record<string, CreateOAuthConfigFn> = {
Auth0: createAuth0OAuthConfig,
Discord: createDiscordOAuthConfig,
Dropbox: createDropboxOAuthConfig,
Facebook: createFacebookOAuthConfig,
GitHub: createGitHubOAuthConfig,
GitLab: createGitLabOAuthConfig,
Google: createGoogleOAuthConfig,
Notion: createNotionOAuthConfig,
Okta: createOktaOAuthConfig,
Patreon: createPatreonOAuthConfig,
Slack: createSlackOAuthConfig,
Spotify: createSpotifyOAuthConfig,
Twitter: createTwitterOAuthConfig,
};

/**
* Allows for dynamic provider selection useful for testing.
* In production, just use import and use the provider's OAuth 2.0 client creator.
* In production, just use import and use the provider's OAuth 2.0 config creator.
*
* @example
* ```ts
* import { createGitHubOAuth2Client } from "https://deno.land/x/deno_kv_oauth@$VERSION/mod.ts";
* import { createGitHubOAuthConfig } from "https://deno.land/x/deno_kv_oauth@$VERSION/src/providers/github.ts";
*
* const oauth2Client = createGitHubOAuth2Client();
* const oauthConfig = createGitHubOAuthConfig({
* redirectUri: "/callback"
* });
* ```
*/
const provider = Deno.env.get("PROVIDER") ?? "GitHub";
const createOAuth2ClientFn = {
Auth0: createAuth0OAuth2Client,
Discord: createDiscordOAuth2Client,
Dropbox: createDropboxOAuth2Client,
Facebook: createFacebookOAuth2Client,
GitHub: createGitHubOAuth2Client,
GitLab: createGitLabOAuth2Client,
Google: createGoogleOAuth2Client,
Notion: createNotionOAuth2Client,
Okta: createOktaOAuth2Client,
Patreon: createPatreonOAuth2Client,
Slack: createSlackOAuth2Client,
Spotify: createSpotifyOAuth2Client,
Twitter: createTwitterOAuth2Client,
}[provider];

if (createOAuth2ClientFn === undefined) {
throw new Error("Provider not found");
}
function getOAuthConfig() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to rejig this a little so that the test case could set the necessary env vars.

const provider = Deno.env.get("PROVIDER") ?? "GitHub";

const additionalOAuth2ClientConfig: Partial<OAuth2ClientConfig> = {
redirectUri: Deno.env.get("DENO_DEPLOYMENT_ID") === undefined
? "http://localhost:8000/callback"
: undefined,
defaults: {
scope: Deno.env.get("SCOPE"),
},
};
const createOAuthConfigFn = providers[provider];

// @ts-ignore Trust me
const oauth2Client = createOAuth2ClientFn(additionalOAuth2ClientConfig);
if (!createOAuthConfigFn) {
throw new Error("Provider not found");
}

const oauthConfig = createOAuthConfigFn({
redirectUri: "/callback",
});

if (Deno.env.has("SCOPE")) {
oauthConfig.scope = Deno.env.get("SCOPE")!.split(/\s+/);
}

return oauthConfig;
}

async function indexHandler(request: Request) {
const oauthConfig = getOAuthConfig();
const sessionId = getSessionId(request);
const hasSessionIdCookie = sessionId !== undefined;
const accessToken = hasSessionIdCookie
? await getSessionAccessToken(oauth2Client, sessionId)
? await getSessionAccessToken(oauthConfig, sessionId)
: null;

const accessTokenInnerText = accessToken !== null
? `<span style="filter:blur(3px)">${accessToken}</span> (intentionally blurred for security)`
: accessToken;

const body = `
<p>Provider: ${provider}</p>
<p>Scope: ${oauth2Client.config.defaults?.scope}</p>
<p>Provider: ${oauthConfig.name}</p>
<p>Scope: ${oauthConfig.scope}</p>
<p>Signed in: ${hasSessionIdCookie}</p>
<p>Your access token: ${accessTokenInnerText}</p>
<p>
Expand Down Expand Up @@ -108,11 +119,13 @@ export async function handler(request: Request): Promise<Response> {
return await indexHandler(request);
}
case "/signin": {
return await signIn(request, oauth2Client);
const oauthConfig = getOAuthConfig();
return await signIn(request, oauthConfig);
}
case "/callback": {
const oauthConfig = getOAuthConfig();
try {
const { response } = await handleCallback(request, oauth2Client);
const { response } = await handleCallback(request, oauthConfig);
return response;
} catch {
return new Response(null, { status: Status.InternalServerError });
Expand Down
3 changes: 3 additions & 0 deletions demo_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { genTokens } from "./src/test_utils.ts";
const baseUrl = "http://localhost";

Deno.test("demo", async (test) => {
Deno.env.set("GITHUB_CLIENT_ID", "dummy_id");
Deno.env.set("GITHUB_CLIENT_SECRET", "dummy_secret");

await test.step("non-GET * serves a not found response", async () => {
const request = new Request(baseUrl, { method: "POST" });
const response = await handler(request);
Expand Down
3 changes: 3 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"compilerOptions": {
"verbatimModuleSyntax": true
},
"lock": false,
"imports": {
"https://deno.land/x/deno_kv_oauth@$VERSION/": "./",
Expand Down
6 changes: 0 additions & 6 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,4 @@ export {
setCookie,
} from "https://deno.land/[email protected]/http/cookie.ts";
export { assert } from "https://deno.land/[email protected]/assert/assert.ts";
export {
OAuth2Client,
type OAuth2ClientConfig,
OAuth2ResponseError,
type Tokens,
} from "https://deno.land/x/[email protected]/mod.ts";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed because nothing else requires this other than the _internal/oauth2_client.ts module

export { SECOND } from "https://deno.land/[email protected]/datetime/constants.ts";
2 changes: 2 additions & 0 deletions dev_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ export {
assert,
assertArrayIncludes,
assertEquals,
AssertionError,
assertNotEquals,
assertRejects,
assertStrictEquals,
assertStringIncludes,
assertThrows,
} from "https://deno.land/[email protected]/assert/mod.ts";
Expand Down
3 changes: 3 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
export * from "./src/providers.ts";
export * from "./src/providers/custom.ts";
export * from "./src/config.ts";
export * from "./src/clear_oauth_sessions_and_tokens.ts";
export * from "./src/get_session_access_token.ts";
export * from "./src/handle_callback.ts";
export * from "./src/get_session_id.ts";
export * from "./src/sign_in.ts";
export * from "./src/sign_out.ts";
export type * from "./src/types.ts";
Loading