Giter Club home page Giter Club logo

deno_kv_oauth's Introduction

Deno KV OAuth logo

High-level OAuth 2.0 powered by Deno KV.

Docs CI codecov

Deno KV OAuth (Beta)

Features

Documentation

Check out the full documentation and API reference here.

How-to

Get Started with a Pre-Defined OAuth Configuration

See here for the list of OAuth providers with pre-defined configurations.

  1. Create your OAuth application for your given provider.

  2. Create your web server using Deno KV OAuth's request handlers, helpers and pre-defined OAuth configuration.

    // server.ts
    import {
      createGitHubOAuthConfig,
      getSessionId,
      handleCallback,
      signIn,
      signOut,
    } from "https://deno.land/x/deno_kv_oauth/mod.ts";
    
    const oauthConfig = createGitHubOAuthConfig();
    
    async function handler(request: Request) {
      const { pathname } = new URL(request.url);
      switch (pathname) {
        case "/oauth/signin":
          return await signIn(request, oauthConfig);
        case "/oauth/callback":
          const { response } = await handleCallback(request, oauthConfig);
          return response;
        case "/oauth/signout":
          return await signOut(request);
        case "/protected-route":
          return await getSessionId(request) === undefined
            ? new Response("Unauthorized", { status: 401 })
            : new Response("You are allowed");
        default:
          return new Response(null, { status: 404 });
      }
    }
    
    Deno.serve(handler);
  3. Start your server with the necessary environment variables.

    GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts

Check out a full implementation in the demo source code which runs https://kv-oauth.deno.dev.

Get Started with a Custom OAuth Configuration

  1. Create your OAuth application for your given provider.

  2. Create your web server using Deno KV OAuth's request handlers and helpers, and custom OAuth configuration.

    // server.ts
    import {
      getRequiredEnv,
      getSessionId,
      handleCallback,
      type OAuth2ClientConfig,
      signIn,
      signOut,
    } from "https://deno.land/x/deno_kv_oauth/mod.ts";
    
    const oauthConfig: OAuth2ClientConfig = {
      clientId: getRequiredEnv("CUSTOM_CLIENT_ID"),
      clientSecret: getRequiredEnv("CUSTOM_CLIENT_SECRET"),
      authorizationEndpointUri: "https://custom.com/oauth/authorize",
      tokenUri: "https://custom.com/oauth/token",
      redirectUri: "https://my-site.com/another-dir/callback",
    };
    
    async function handler(request: Request) {
      const { pathname } = new URL(request.url);
      switch (pathname) {
        case "/oauth/signin":
          return await signIn(request, oauthConfig);
        case "/another-dir/callback":
          const { response } = await handleCallback(request, oauthConfig);
          return response;
        case "/oauth/signout":
          return await signOut(request);
        case "/protected-route":
          return await getSessionId(request) === undefined
            ? new Response("Unauthorized", { status: 401 })
            : new Response("You are allowed");
        default:
          return new Response(null, { status: 404 });
      }
    }
    
    Deno.serve(handler);
  3. Start your server with the necessary environment variables.

    CUSTOM_CLIENT_ID=xxx CUSTOM_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts

Get Started with Cookie Options

This is required for OAuth solutions that span more than one sub-domain.

  1. Create your OAuth application for your given provider.

  2. Create your web server using Deno KV OAuth's helpers factory function with cookie options defined.

    // server.ts
    import {
      createGitHubOAuthConfig,
      createHelpers,
    } from "https://deno.land/x/deno_kv_oauth/mod.ts";
    
    const {
      signIn,
      handleCallback,
      signOut,
      getSessionId,
    } = createHelpers(createGitHubOAuthConfig(), {
      cookieOptions: {
        name: "__Secure-triple-choc",
        domain: "news.site",
      },
    });
    
    async function handler(request: Request) {
      const { pathname } = new URL(request.url);
      switch (pathname) {
        case "/oauth/signin":
          return await signIn(request);
        case "/oauth/callback":
          const { response } = await handleCallback(request);
          return response;
        case "/oauth/signout":
          return await signOut(request);
        case "/protected-route":
          return await getSessionId(request) === undefined
            ? new Response("Unauthorized", { status: 401 })
            : new Response("You are allowed");
        default:
          return new Response(null, { status: 404 });
      }
    }
    
    Deno.serve(handler);
  3. Start your server with the necessary environment variables.

    GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno run --unstable-kv --allow-env --allow-net server.ts

Get Started with Fresh

  1. Create your OAuth application for your given provider.

  2. Create your OAuth configuration and Fresh plugin.

    // plugins/kv_oauth.ts
    import {
      createGitHubOAuthConfig,
      createHelpers,
    } from "https://deno.land/x/deno_kv_oauth/mod.ts";
    import type { Plugin } from "$fresh/server.ts";
    
    const { signIn, handleCallback, signOut, getSessionId } = createHelpers(
      createGitHubOAuthConfig(),
    );
    
    export default {
      name: "kv-oauth",
      routes: [
        {
          path: "/signin",
          async handler(req) {
            return await signIn(req);
          },
        },
        {
          path: "/callback",
          async handler(req) {
            // Return object also includes `accessToken` and `sessionId` properties.
            const { response } = await handleCallback(req);
            return response;
          },
        },
        {
          path: "/signout",
          async handler(req) {
            return await signOut(req);
          },
        },
        {
          path: "/protected",
          async handler(req) {
            return await getSessionId(req) === undefined
              ? new Response("Unauthorized", { status: 401 })
              : new Response("You are allowed");
          },
        },
      ],
    } as Plugin;
  3. Add the plugin to your Fresh app.

  4. Start your Fresh server with the necessary environment variables.

    GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno task start

Run the Demo Locally

The demo uses GitHub as the OAuth provider. You can change the OAuth configuration by setting the oauthConfig constant as mentioned above.

  1. Create your OAuth application for your given provider.

  2. Start the demo with the necessary environment variables.

    TWITTER_CLIENT_ID=xxx TWITTER_CLIENT_SECRET=xxx deno task demo

Concepts

Redirects after Sign-In and Sign-Out

The URL that the client is redirected to upon successful sign-in or sign-out is determined by the request made to the sign-in or sign-out endpoint. This value is set in the following order of precedence:

  1. The value of the success_url URL parameter of the request URL, if defined. E.g. a request to http://example.com/signin?success_url=/success redirects the client to /success after successful sign-in.
  2. The value of the Referer header, if of the same origin as the request. E.g. a request to http://example.com/signin with Referer header http://example.com/about redirects the client to http://example.com/about after successful sign-in.
  3. The root path, "/". E.g. a request to http://example.com/signin without the Referer header redirects the client to http://example.com after successful sign-in.

Pre-Defined OAuth Configurations

Providers

The following providers have pre-defined OAuth configurations:

  1. Auth0
  2. AzureAD
  3. AzureADB2C
  4. Discord
  5. Dropbox
  6. Facebook
  7. GitHub
  8. GitLab
  9. Google
  10. Notion
  11. Okta
  12. Patreon
  13. Slack
  14. Spotify
  15. Twitter

Environment Variables

These must be set when starting a server with a pre-defined OAuth configuration. Replace the PROVIDER prefix with your given OAuth provider's name when starting your server. E.g. DISCORD, GOOGLE, or SLACK.

  1. PROVIDER_CLIENT_ID - Client ID of a given OAuth application.
  2. PROVIDER_CLIENT_SECRET - Client secret of a given OAuth application.
  3. PROVIDER_DOMAIN (optional) - Server domain of a given OAuth application. Only required for Okta and Auth0.

Note: reading environment variables requires the --allow-env[=<VARIABLE_NAME>...] permission flag. See the manual for further details.

Built with Deno KV OAuth

  1. Deno KV OAuth live demo
  2. Deno SaaSKit - A modern SaaS template built on Fresh and uses a custom Deno KV OAuth plugin.
  3. KV SketchBook - Dead simple sketchbook app.
  4. Fresh + Deno KV OAuth demo - A demo of Deno KV OAuth working in the Fresh web framework.
  5. Oak + Deno KV OAuth demo - A demo of Deno KV OAuth working in the Oak web framework.
  6. Ultra + Deno KV OAuth demo - A demo of Deno KV OAuth working in the Ultra web framework.
  7. Hono + Deno KV OAuth demo - A demo of Deno KV OAuth working in the Hono web framework.
  8. Cheetah + Deno KV OAuth demo - A demo of Deno KV OAuth working in the Cheetah web framework.
  9. Paquet - A web app shop
  10. Fastro + Deno KV OAuth live demo - A simple, reusable fastro module that implements Deno KV.

Do you have a project powered by Deno KV OAuth that you'd like to share? Feel free to let us know in a new issue.

Known Issues

  • Twitch is not supported as an OAuth provider because it does not support PKCE. See #79 and this post for more information.

Contributing Guide

Check out the contributing guide here.

Security Policy

Check out the security policy here.

deno_kv_oauth's People

Contributors

adoublef avatar brettchalupa avatar cirolosapio avatar dependabot[bot] avatar github-actions[bot] avatar iuioiua avatar j3lte avatar jabolol avatar jollytoad avatar kt3k avatar lino-levan avatar mbhrznr avatar mcgear avatar mitchwadair avatar notangelmario avatar ntns avatar ynm3n avatar ynwd avatar zachauten avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

deno_kv_oauth's Issues

Suggestion: Providers return config only

Currently all the providers create a config and then they all do exactly the same thing, create an OAuth2Client instance using that config, this ties deno_kv_oauth to the existing oauth2_client API quite tightly.

An OAuth2Client contains an amount of redundant features that are not used by deno_kv_oauth (ie. implicit, ropc, clientCredentials), also you never need both code and refresh together.

If the providers just returned the config, and this config was passed to signIn/handleCallback etc, those fns could then create the client, and allow for future evolution of the client API or even a completely alternative oauth2 lib.

Users of deno_kv_oauth need not care about the underlying client lib at all, they'll just be exposed to the config interface.

todo: `demo_test.ts`

It should:

  • Import handler() from demo.ts and use serve() from std.
  • Use the signal property to close the server once tests are completed.
  • Basically, ensure the demo server starts fine and endpoints are responsive, but provide 100% test coverage, if possible. In-depth testing of deno_kv_oauth's functions is not needed, as that's taken care

Auth0 example

I've made the issue here but as this package depends on it and the docs show Auth0 being supported wanted to bring it to your attention too.

Auth0 will return an invalid JWT unless the audience is added to the Uri. Looking at the code it doesn't seem that there's currently a way to allow this

Expose methods to allow running cronjobs against session's expiry time

I have enjoyed using this library for handling sessions. However I have noticed that at some point there will be sessions that don't get deleted from the the store and so overtime could grow and will need a way to delete them.

As I understand the Kv store API currently doesn't have and true TTL capabilities natively but I think it should be possible to still expose a way to search/filter sessions by expiry time.

The official docs state this:

- A range selector selects all keys that are lexicographically between the given start and end keys (including the start, and excluding the end). For example, the selector ["users", "a"], ["users", "n"] will select all keys that start with the prefix ["users"] and have a second key part that is lexicographically between a and n, such as ["users", "alice"], ["users", "bob"], and ["users", "mike"], but not ["users", "noa"] or ["users", "zoe"].

Based on this I think if a key included a timestamp (could be [PREFIX, timestamp]) then could expose a function like getExpiredSessions that may look like this:

const db = await Deno.openKv();
const entries = db.list({ start: [KV_PREFIX, 0], end: [KV_PREFIX, unix_timestamp] })
for await (const entry of entries) {
  entry.key; // [KV_PREFIX, expiry_as_unix]
  entry.value; // { sessionId, expiryAsUnix, ...rest }
  entry.versionstamp; // "00000000000000010000"
}

I'm unsure on the deleteExpiredSessions method but I think it would need to use transactions for consistency.

Be interested to open the dialogue to see other opinions on the matter.

I saw this bit of code and felt it may be a good starting point

Functions to list providers and other things

I've been experimenting with deno_kv_oauth to provide a sign in for my personal site, I've created a handful of functions to discover available providers, and to dynamically configure providers based on the environment vars that have been set...

https://github.com/jollytoad/home/blob/main/lib/oauth2_clients.ts

  • getOAuth2ClientNames - Get a list of all supported OAuth2 client names
  • hasOAuth2ClientEnvVars - Detect whether environment vars have been set for a given OAuth2 client
  • getOAuth2ClientFn - Get the create client function for an OAuth2 client given it's name
  • getOAuth2ClientScope - Get the OAuth2 scope for a provider from environment vars

Would you be interested in having some or all of these fns within deno_kv_auth itself?
I'd be to raise a PR for this if so, and include some tests.

For an example of usage, see... https://github.com/jollytoad/home/blob/main/components/UserWidget.tsx
This JSX component lists sign in links for providers where the env vars have been set, using getOAuth2ClientNames & hasOAuth2ClientEnvVars, the sign in link includes the provider name in the path, and https://github.com/jollytoad/home/blob/main/routes/auth/_lib/oauth2_client.ts has a function used by the signin/callback handlers to extract the provider name and use getOAuth2ClientFn & getOAuth2ClientScope. (All of this stuff is specific to my site though, so I wouldn't suggest incorporating these, I'm referencing them simply as an example of usage)

How to get user details on callback success

I searched all the existing issues and docs, I was only able to figure out, how to get the sessionId or accessToken.

How can one get user details from accessToken? Should we connect to 3rd party APIs separately?

Suggestion: Factor out leaky abstractions of oauth2_client

kv_oauth sits atop oauth2_client, but exposes much of the inner workings of oauth2_client, this I feel is a leaky abstraction in that it leaves kv_oauth at risk from major oauth2_client API changes, and prevents (however unlikely) future consideration of alternatives, or even direct inclusion of just the essential parts of that lib should it ever become unmaintained.

One way to address this is to pass config rather than client as addressed by #174, but it's been discussed that the config adopts OAuth2ClientConfig directly.

There are three issues I have with this interface:

  1. It's exported from a module that also contains the OAuth2Client class, placing a dependency burden on anything that wants to reference this type
  2. It exposes a number of options that could be deemed unnecessary (at this time at least) for a highly level lib like kv_oauth, namely: defaults.requestOptions & defaults.stateValidator
  3. If we discard those, this leaves us with defaults.scope which feels unnecessarily nested

My proposal is for kv_oauth to expose it own config type that better fits it use case and developer experience, exposing only options that are necessary for kv_oauth, and to protect it from potential future changes, abandonment, or even the consideration of alternatives to the underlying oauth2_client API.

I feel that this is something that could be neatly addressed now whilst in beta stage, as it'll be a lot harder to plug the leaks later.

proposal: `createProvider(providerId: string)`

The 2nd parameter would be additionalOauth2ClientConfig?: OAuth2ClientConfig, allowing one to override the default OAuth config. This would also be used under the hood in other functions that use oauth2Client to provide additional customizability. E.g. signIn(provider: Provider) -> signIn(provider: ProviderId | Provider).

I think having a single function that constructs Provider objects over having multiple means better DX. Also, such a feature would be very cheap to implement if using a basic switch statement.

Callback redirection to the original page

How can you pass the callback redirection URL through the entire flow?

To clarify, I mean the URL that the callback handler redirects to - which is currently passed as the third arg to handleCallback.

I'd like the 'Referer' (or other arbitrary URL) to be passed from signin, all the way through the flow to the callback handler, so that the user ends up back on the same page after.

I've tried adding it to the callback URL, but some providers (eg. Google), don't allow this.

Proposal: Separate providers

I have been adding a few providers for this library. I am basically taking a few popular providers that are defined in NextAuth and porting (including testing if it works) it to Deno KV Auth.

Adding multiple providers becomes tedious when it is in 1 single file. I would propose:

  • Creating a separate folder for the providers, making it easier to maintain them. Basically mimicking a similar structure like NextAuth
  • Maybe make the demo.ts more generic, where you basically can import the OAuthProvider for easy testing. Right now I am changing demo.ts to locally test it and then revert the changes before I commit to the repo

If you want, I can make a PR with my suggested changes

Suggestion: Support for OpenID Connect

I raise this as it's been discussed elsewhere, and there is currently no explicit issue for it in this project.

There is a limitation in oauth2_client atm that prevents this: it doesn't expose the id_token, and there is no official way to
obtain that token from kv_oauth either. This can be worked around with a patch to oauth2_client (cmd-johnson/deno-oauth2-client#32) and use of the internal getTokens function.

Although experimental work is underway in oauth2_client to fully support the OIDC flow: https://github.com/cmd-johnson/deno-oauth2-client/tree/feature/oidc

Oak Demo (change to undefine)

Last week I was attempting to recreate the demo but with Auth0, all was good but I had to change this line to undefined as it seems getSessionId now returns undefined so this boolean checks fails.

const isSignedIn = sessionId !== null; // this may need to be changed to undefined

Suggestion: Common configuration function with validation

The providers are very repetitive atm, they don't validate the config, and attempt to get the env vars for id/secret even if those values are passed in the given additional config props, meaning that a consume of the lib must allow envs.

  return new OAuth2Client({
    clientId: Deno.env.get("GOOGLE_CLIENT_ID")!,
    clientSecret: Deno.env.get("GOOGLE_CLIENT_SECRET")!,
    authorizationEndpointUri: "https://accounts.google.com/o/oauth2/v2/auth",
    tokenUri: "https://oauth2.googleapis.com/token",
    ...additionalOAuth2ClientConfig,
  });

a simply function could standardize this, providing validation that all values are set as required and provide a place to improve/fix issues should they crop up (eg. fallback on env vars rather than requiring them).

a simple example of usage of such a fn:

const config = createOAuthConfig({
  name: "Google",
  authorizationEndpointUri: "https://accounts.google.com/o/oauth2/v2/auth",
  tokenUri: "https://oauth2.googleapis.com/token",
  ...additionalOAuth2ClientConfig,
});

Suggestion: support relative redirectUri

At present a redirectUri is passed wholesale through the flow, and so must be configured as an absolute URL.
This often means you have to know your origin in advance or you have to configure your redirectUri and client at request time.

Example:

const redirectUri = new URL("/callback", req.url).href;
const client = createSomeOAuth2Client({ redirectUri });
return signIn(req, client);

As you are already passing the Request to the signIn/callbackHandler, these could resolve the URL instead...

const client = createSomeOAuth2Client({ redirectUri: "/callback" });
return signIn(req, client);

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.