这是indexloc提供的服务,不要输入任何密码
Skip to content

ajoslin/openauthjs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenAuthJS

OpenAuthJS provides a centralized auth server that can be deployed both as a standalone service or embedded into an existing application. It works on nodejs, bun, aws lambda or cloudflare workers and is configured entirely through typescript.

Quick Start

If you just want to get started as fast as possible you check jump straight into the code examples folder and copy paste away. There are also SST components for deploying everything OpenAuthJS needs.

Approach

While there are many open source solutions for auth, almost all of them are libraries that are meant to be embedded into a single application. Centralized auth servers typically are delivered as SaaS services - eg Auth0 or Clerk.

OpenAuthJS instead is a centralized auth server that runs on your own infrastructure and has been designed for ease of self hosting. It can be used to authenticate all of your applications - web apps, mobile apps, internal admin tools, etc.

It adheres mostly to OAuth 2.0 specifications - which means anything that can speak oauth can use it to receive access and refresh tokens. When a client initiates an authorization flow, OpenAuthJS will hand off to one of the configured adapters - this can be third party identity providers like Google, Github, etc or built in flows like email/password or pin code.

Because it follows these specifications it can even be used to issue credentials for third party applications - allowing you to implement "login with myapp" flows.

OpenAuthJS very intentionally does not attempt to solve user management. We've found that this is a very difficult problem given the wide range of databases and drivers that are used in the JS ecosystem. Additionally it's quite hard to build data abstractions that work for every use case. Instead, once a user has identified themselves OpenAuthJS will invoke a callback where you can implement your own user lookup/creation logic.

While OpenAuthJS tries to be mostly stateless, it does need to store a minimal amount of data (refresh tokens, password hashes, etc). However this has been reduced to a simple KV store with various implementations for zero overhead systems like Cloudflare KV and DynamoDB. You should never need to directly access any data that is stored in there.

There is also a themeable UI that you can use to get going without implementing any designs yourself. This is built on top of a lower level system so you can copy paste the default UI and tweak it or opt out entirely and implement your own.

Finally, OpenAuthJS is created by the maintainers of SST which is a tool to manage all the infrastructure for your app. It contains components for OpenAuthJS that make deploying it to AWS or Cloudflare as simple as it can get.

Tutorial

We'll show how to deploy the auth server and then a sample app that uses it

Auth server

Start by importing the authorizer function from the @openauthjs/core package.

import { authorizer } from "@openauthjs/core";

OpenAuthJS is built on top of Hono which is a minimal web framework that can run anywhere. The authorizer function creates a Hono app with all of the auth server implemented that you can then deploy to AWS Lambda, Cloudflare Workers, or in a container running under nodejs or bun.

The authorizer function requires a few things:

const app = authorizer({
  providers: { ... },
  storage,
  subjects,
  allow: async () => true,
  success: async (ctx, value) => { ... }
})

First we need to define some providers that are enabled - these are either third party identity providers like Google, Github, etc or built in flows like email/password or pin code. You can also implement you own. Let's try the github provider.

import { GithubAdapter } from "@openauthjs/core/adapter/github";

const app = authorizer({
  providers: {
    github: GithubAdapter({
      clientID: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      scopes: ["user:email"],
    }),
  },
  ...
})

Adapters take some configuration - since this is a third party identity provider there is no UI to worry about and all it needs is a client ID, secret and some scopes. Let's add the password provider which is a bit more complicated.

import { PasswordAdapter } from "@openauthjs/core/adapter/password";

const app = authorizer({
  providers: {
    github: ...,
    password: PasswordAdapter(...),
  },
  ...
})

The password adapter is quite complicated as username/password involve a lot of flows so there are a lot of callbacks to implement. However you can opt into the default UI which has all of this already implemented for you. The only thing you have to specify is how to send a code for forgot password/email verification. In this case we'll log the code but you would send this over email.

import { PasswordAdapter } from "@openauthjs/core/adapter/password";
import { PasswordUI } from "@openauthjs/core/ui/password";

const app = authorizer({
  providers: {
    github: ...,
    password: PasswordAdapter(
      PasswordUI({
        sendCode: async (email, code) => {
          console.log(email, code);
        },
      }),
    ),
  },
  ...
})

Next up is the subjects field. Subjects are what the access token generated at the end of the auth flow will map to. Under the hood, the access token is a JWT that contains this data. You will likely just have a single subject to start but you can define additional ones for different types of users.

import { object, string } from "valibot";

const subjects = createSubjects({
  user: object({
    userID: string(),
    // may want to add workspaceID here if doing a multi-tenant app
    workspaceID: string(),
  }),
});

Note we are using valibot to define the shape of the subject so it can be validated proeprly. You can use any validation library that is following the standard-schema specification - the next version of Zod will support this.

You typically will want to place subjects in its own file as it can be imported by all of your apps. You can pass it to the authorizer in the subjects field.

import { subjects } from "./subjects.js";

const app = authorizer({
  providers: { ... },
  subjects,
  ...
})

Next we'll implement the success callback which receives the payload when a user successfully completes a provider flow.

const app = authorizer({
  providers: { ... },
  subjects,
  async success(ctx, value) {
    let userid;
    if (value.provider === "password") {
      console.log(value.email);
      userid = ... // lookup user or create them
    }
    if (value.provider === "github") {
      console.log(value.tokenset.access);
      userid = ... // lookup user or create them
    }
    return ctx.subject("user", {
        userid,
    });
  }
})

Note all of this is typesafe - based on the configured providers you will receive different properties in the value object. Also the subject method will only accept properties. Note - most callbacks in OpenAuthJS can return a Response object. In this case if something goes wrong, you can return a Response.redirect("...") sending them to a different place or rendering an error.

Next we have the storage field which defines where things like refresh tokens and password hashes are stored. If on AWS we recommend DynamoDB, if on Cloudflare we recommend Cloudflare KV. We also have a MemoryStore used for testing.

import { MemoryStorage } from "@openauthjs/core/storage/memory";

const app = authorizer({
  providers: { ... },
  subjects,
  async success(ctx, value) { ... },
  storage: new MemoryStore(),
})

Finally we have the allow callback. Typically you'll want to make sure the redirect_uri passed into it is for a valid domain so people don't hijack your auth server for apps you did not authorize. You can do something like this or allow: async () => true is fine for testing.

const app = authorizer({
  providers: { ... },
  subjects,
  async success(ctx, value) { ... },
  storage,
  async allow(input) {
    const url = new URL(input.redirectURI);
    if (url.hostname !== "myapp.com") throw new Error("Invalid redirect");
    return true;
  }
})

And now we are ready to deploy! Here's how you do that depending on your infrastructure.

// bun
export default app

// cloudflare
export default app

// lambda
import { handle } from "hono/aws-lambda"
export const handler = handle(app)

// nodejs
import { serve } from '@hono/node-server'
serve(app)

You now have a centralized auth server. Test it out by visiting /.well-known/oauth-authorization-server - you can see a live example here.

Auth client

Since this is a standard oauth server you can use any libraries for oauth and it will work. OpenAuthJS does provide some light tooling for this although even a manual flow is pretty simple. You can create a client like this:

import { createClient } form "@openauthjs/core"

const client = createClient("my-client", {
  issuer: "https://auth.myserver.com" // this is the url for your auth server
})

SSR Sites

If your frontend has a server component you can use the code flow. Redirect the user here

const redirect = await client.authorize(
  <client-id>,
  <redirect-uri>,
  "code",
);

You can make up a client_id that represents your app. This will initiate the auth flow and and user will be redirected to the redirect_uri you provided with a query parameter code which you can exchange for an access token.

const tokens = await client.exchange(query.get("code"), redirect_uri) // the redirect_uri is the original redirect_uri you passed in and is used for verification
console.log(tokens.access, tokens.refresh)

You likely want to store both the access token and refresh token in an HTTP only cookie so they are sent up with future requests. Then you can use the client to verify the tokens.

const verified = await client.verify(
  subjects,
  cookies.get("access_token")!,
  { refresh: cookies.get("refresh_token") || undefined },
);
console.log(verified.subject.type, verified.subject.properties, verified.refresh, verified.access);

Passing in the refresh token is optional but if you do, this function will automatically refresh the access token if it has expired. It will return a new access token and refresh token which you should set back into the cookies.

SPA Sites, Mobile apps, etc

In cases where you do not have a server, you can use the token flow with pkce on the frontend.

const [verifier, redirect] = await client.pkce(<client_id>, <redirect_uri>);
localStorage.setItem("verifier", verifier);
location.href = redirect;

When the auth flow is complete the user's browser will be redirected to the redirect_uri with a code query parameter. You can then exchange the code for access/refresh tokens.

const verifier = localStorage.getItem("verifier");
const tokens = await client.exchange(query.get("code"), redirect_uri, verifier);
localStorage.setItem("access_token", tokens.access);
localStorage.setItem("refresh_token", tokens.refresh);

Then when you make requests to your API you can include the access token in the Authorization header.

const accessToken = localStorage.getItem("access_token");
fetch("https://auth.example.com/api/user", {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

And then you can verify the access token on the server.

const verified = await client.verify(subjects, accessToken);
console.log(verified.subject)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 95.7%
  • CSS 4.3%