unkey icon indicating copy to clipboard operation
unkey copied to clipboard

[WIP] Auth Provider - NOOP in the streets, WorkOS in the sheets

Open chronark opened this issue 1 year ago • 2 comments

Preliminary Checks

  • [X] I have reviewed https://unkey.com/docs for existing features that would solve my problem

  • [X] I have searched for existing feature requests: https://github.com/unkeyed/unkey/issues

  • [X] This issue is not a question, general help request, or anything other than a feature request directly related to Unkey. Please ask questions in our Discord community: https://unkey.com/discord.

Is your feature request related to a problem? Please describe.

Selfhosting Unkey requires setting up clerk.com right now, which is added friction for contributors and not ideal for enterprises.

At the same time, we - Unkey - do not want to roll browser-auth ourselves and want to keep using a service for it, who will develop their product further without any additional time investment from our side.

We came to a solution to drop auth by default when selfhosting and only using a provider for our managed service. This offers the best of both worlds.

When you are running unkey locally, there will be no auth. You will simply be dropped into a workspace and can use the dashboard as usual and can even create more workspaces, but there is no sign in page or anything.

Enterprises usually already have a mechanism to protect internal dashboards and can simply run Unkey behind their auth stack.

Describe the solution

We need 3 things:

  1. an interface that abstracts the auth provider
  2. a local implementation of said interface that just returns hard coded ids or looks in our db
  3. a WorkOS implementation of the interface

The beauty of WorkOS is that it's 100% serverside, so creating an interface isn't that hard. We can probably get started with something similar to this:

export interface Auth {
  // If there is none, it must trigger a redirect to the sign in page.
  getOrgId(): Promise<string>;

  // called in trpc, it returns just enough to know who's talking to us
  getSession(): Promise<{ userId: string, orgId: string} | null>;

  // called in RSC, giving us some display data for the user
  getUser(): Promise<{ userId: string, profileUrl: string, name: string }| null>;

  listOrganisations(): Promise<Organisation[]>

  // sign the user into a different workspace/organisation
  signIn(orgId?: string): Promise<void>

  signOut(): Promise<void>

  // update name, domain or picture
  updateOrg(org: Partial<Organisation>): Promise<void>
}

We will then have a WorkOS implementation for this interface, using a combination of authkit and the workos node sdk.

Specifying whether to use the Local or WorkOS implentation will be done with environment variables

// /apps/dashboard/lib/env.ts

AUTH_PROVIDER: z.enum(["workos", "local"]),

WORKOS_API_KEY: z.string().optional(),
WORKOS_CLIENT_ID: z.string().optional(),
NEXT_PUBLIC_WORKOS_REDIRECT_URI: z.string().default("http://localhost:3000/callback"),
WORKOS_COOKIE_PASSWORD: z.string().optional(),

and one implentation is chosen at runtime like so:

// /apps/dashboard/lib/auth/index.ts

import { env } from "@/lib/env";

import type { Auth } from "./interface";
import { LocalAuth } from "./local";
import { WorkosAuth } from "./workos";

export let auth: Auth;

let initialized = false;

function init() {
  if (initialized) {
    return;
  }

  switch (env().AUTH_PROVIDER) {
    case "workos":
      serverAuth = new WorkosAuth();
      break;
    case "local":
      serverAuth = new LocalAuth();
      break;
  }

  initialized = true;
}

init();

Throughout the app, we can then simply import auth and not care which implementation it is.

// anwhere/else.ts
import { auth } from "@/lib/auth"

const user = await auth.getUser()

Describe alternatives you have considered (if any)

No response

Additional context

No response

chronark avatar Sep 27 '24 14:09 chronark

Going to continue in a comment, cause it's been a while and you already started work.


Goals and expectations

  • We will do a full rip and replace, because we don't want to use both systems side by side. Before merging, there must be no clerk code left.
  • We want to reuse our existing auth pages, not authkits hosted pages.
  • We should do regular main pulls, especially during hacktober fest, there will be a lot of small contributions and it's probably much easier to address them one at a time, rather than getting caught in a large conflict at the end.
  • Don't follow the workos API too closely, create an abstraction that makes sense and is likely to work with different providers. If we use the workos API exactly, we didn't gain much from hiding it behind an interface at all.
  • Near the end, @perkinsjr can help with the clerk export
  • We can announce a downtime window for new signups and membership changes if we have to, while we migrate all users.

Things that are easy to forget:

  • Update the pnpm local script to remove clerk and insert the AUTH_PROVIDER env variable
  • Inbound webhooks are different now, but if you remove all of clerk's sdk, typescript should make this hard to forget. (Maybe we don't even need webhooks here and can actually trigger the email from within our signup flow)
  • Ping james or me to insert staging and production env vars

chronark avatar Oct 03 '24 11:10 chronark

delivered

mcstepp avatar Jun 06 '25 19:06 mcstepp