Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulAsjes committed Jun 12, 2024
1 parent e002c7d commit 449f0d9
Show file tree
Hide file tree
Showing 18 changed files with 2,328 additions and 147 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Available in your WorkOS dashboard
WORKOS_CLIENT_ID=
WORKOS_API_KEY=
WORKOS_REDIRECT_URI=
WORKOS_COOKIE_PASSWORD=
42 changes: 18 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
# Welcome to Remix!
# Remix integration example using AuthKit

- [Remix Docs](https://remix.run/docs)
An example application demonstrating how to authenticate users with AuthKit and the WorkOS Node SDK.

## Development
> Refer to the [User Management](https://workos.com/docs/user-management) documentation for reference.
From your terminal:
## Prerequisites

```sh
npm run dev
```
You will need a [WorkOS account](https://dashboard.workos.com/signup).

This starts your app in development mode, rebuilding assets on file changes.
## Running the example

## Deployment

First, build your app for production:
Make sure the following values are present in your `.env.local` environment variables file. The client ID and API key can be found in the [WorkOS dashboard](https://dashboard.workos.com), and the redirect URI can also be configured there.

```sh
npm run build
WORKOS_CLIENT_ID="client_..." # retrieved from the WorkOS dashboard
WORKOS_API_KEY="sk_test_..." # retrieved from the WorkOS dashboard
WORKOS_REDIRECT_URI="http://localhost:3000/callback" # configured in the WorkOS dashboard
WORKOS_COOKIE_PASSWORD="<your password>" # generate a secure password here
```

Then run the app in production mode:
`WORKOS_COOKIE_PASSWORD` is the private key used to encrypt the session cookie. It has to be at least 32 characters long. You can use the [1Password generator](https://1password.com/password-generator/) or the `openssl` library to generate a strong password via the command line:

```sh
npm start
```
openssl rand -base64 24
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `remix build`
Run the following command and navigate to [http://localhost:3000](http://localhost:3000).

- `build/`
- `public/build/`
```bash
npm run dev
```
16 changes: 16 additions & 0 deletions app/.server/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getAuthorizationUrl } from './get-authorization-url';
import { terminateSession } from './session';

async function getSignInUrl() {
return getAuthorizationUrl({ screenHint: 'sign-in' });
}

async function getSignUpUrl() {
return getAuthorizationUrl({ screenHint: 'sign-up' });
}

async function signOut(request: Request) {
return await terminateSession(request);
}

export { getSignInUrl, getSignUpUrl, signOut };
76 changes: 76 additions & 0 deletions app/.server/authkit-callback-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { HandleAuthOptions } from './interfaces';
import { WORKOS_CLIENT_ID } from '../.server/env-variables';
import { workos } from './workos';
import { encryptSession } from './session';
import { getSession, commitSession, cookieName } from './cookie';
import { redirect, json, LoaderFunctionArgs } from '@remix-run/node';

export function authLoader(options: HandleAuthOptions = {}) {
return async function loader({ request }: LoaderFunctionArgs) {
const { returnPathname: returnPathnameOption = '/' } = options;

const url = new URL(request.url);

const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const returnPathname = state ? JSON.parse(atob(state)).returnPathname : '/';

if (code) {
try {
const { accessToken, refreshToken, user, impersonator } =
await workos.userManagement.authenticateWithCode({
clientId: WORKOS_CLIENT_ID,
code,
});

// Clean up params
url.searchParams.delete('code');
url.searchParams.delete('state');

// Redirect to the requested path and store the session
url.pathname = returnPathname ?? returnPathnameOption;

// The refreshToken should never be accesible publicly, hence why we encrypt it in the cookie session
// Alternatively you could persist the refresh token in a backend database
const encryptedSession = await encryptSession({
accessToken,
refreshToken,
user,
impersonator,
});

const session = await getSession(cookieName);

session.set('jwt', encryptedSession);
const cookie = await commitSession(session);

return redirect(url.toString(), {
headers: {
'Set-Cookie': cookie,
},
});
} catch (error) {
const errorRes = {
error: error instanceof Error ? error.message : String(error),
};

console.error(errorRes);

return errorResponse();
}
}

function errorResponse() {
return json(
{
error: {
message: 'Something went wrong',
description:
'Couldn’t sign in. If you are not sure what happened, please contact your organization admin.',
},
},
{ status: 500 }
);
}
};
}
33 changes: 33 additions & 0 deletions app/.server/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
WORKOS_REDIRECT_URI,
WORKOS_COOKIE_MAX_AGE,
WORKOS_COOKIE_PASSWORD,
} from './env-variables';
import { createCookieSessionStorage } from '@remix-run/node';

const redirectUrl = new URL(WORKOS_REDIRECT_URI);
const isSecureProtocol = redirectUrl.protocol === 'https:';

const cookieName = 'wos-session';
const cookieOptions = {
path: '/',
httpOnly: true,
secure: isSecureProtocol,
sameSite: 'lax' as const,
// Defaults to 400 days, the maximum allowed by Chrome
// It's fine to have a long cookie expiry date as the access/refresh tokens
// act as the actual time-limited aspects of the session.
maxAge: WORKOS_COOKIE_MAX_AGE
? parseInt(WORKOS_COOKIE_MAX_AGE, 10)
: 60 * 60 * 24 * 400,
secrets: [WORKOS_COOKIE_PASSWORD],
};
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
cookie: {
name: cookieName,
...cookieOptions,
},
});

export { cookieName, getSession, commitSession, destroySession };
35 changes: 35 additions & 0 deletions app/.server/env-variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
function getEnvVariable(name: string): string {
const envVariable = process.env[name];
if (!envVariable) {
throw new Error(`${name} environment variable is not set`);
}
return envVariable;
}

function getOptionalEnvVariable(name: string): string | undefined {
return process.env[name];
}

const WORKOS_CLIENT_ID = getEnvVariable('WORKOS_CLIENT_ID');
const WORKOS_API_KEY = getEnvVariable('WORKOS_API_KEY');
const WORKOS_REDIRECT_URI = getEnvVariable('WORKOS_REDIRECT_URI');
const WORKOS_COOKIE_PASSWORD = getEnvVariable('WORKOS_COOKIE_PASSWORD');
const WORKOS_API_HOSTNAME = getOptionalEnvVariable('WORKOS_API_HOSTNAME');
const WORKOS_API_HTTPS = getOptionalEnvVariable('WORKOS_API_HTTPS');
const WORKOS_API_PORT = getOptionalEnvVariable('WORKOS_API_PORT');
const WORKOS_COOKIE_MAX_AGE = getOptionalEnvVariable('WORKOS_COOKIE_MAX_AGE');

if (WORKOS_COOKIE_PASSWORD.length < 32) {
throw new Error('WORKOS_COOKIE_PASSWORD must be at least 32 characters long');
}

export {
WORKOS_CLIENT_ID,
WORKOS_API_KEY,
WORKOS_REDIRECT_URI,
WORKOS_COOKIE_PASSWORD,
WORKOS_API_HOSTNAME,
WORKOS_API_HTTPS,
WORKOS_API_PORT,
WORKOS_COOKIE_MAX_AGE,
};
19 changes: 19 additions & 0 deletions app/.server/get-authorization-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { workos } from './workos';
import { WORKOS_CLIENT_ID, WORKOS_REDIRECT_URI } from './env-variables';
import { GetAuthURLOptions } from './interfaces';

async function getAuthorizationUrl(options: GetAuthURLOptions = {}) {
const { returnPathname, screenHint } = options;

return workos.userManagement.getAuthorizationUrl({
provider: 'authkit',
clientId: WORKOS_CLIENT_ID,
redirectUri: WORKOS_REDIRECT_URI,
state: returnPathname
? btoa(JSON.stringify({ returnPathname }))
: undefined,
screenHint,
});
}

export { getAuthorizationUrl };
53 changes: 53 additions & 0 deletions app/.server/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { User } from '@workos-inc/node';

export interface HandleAuthOptions {
returnPathname?: string;
}

export interface Impersonator {
email: string;
reason: string | null;
}
export interface Session {
accessToken: string;
refreshToken: string;
user: User;
impersonator?: Impersonator;
}

export interface UserInfo {
user: User;
sessionId: string;
organizationId?: string;
role?: string;
impersonator?: Impersonator;
accessToken: string;
}
export interface NoUserInfo {
user: null;
sessionId?: undefined;
organizationId?: undefined;
role?: undefined;
impersonator?: undefined;
}

export interface AccessToken {
sid: string;
org_id?: string;
role?: string;
}

export interface GetAuthURLOptions {
screenHint?: 'sign-up' | 'sign-in';
returnPathname?: string;
}

export interface AuthkitMiddlewareAuth {
enabled: boolean;
unauthenticatedPaths: string[];
}

export interface AuthkitMiddlewareOptions {
debug?: boolean;
middlewareAuth?: AuthkitMiddlewareAuth;
}
Loading

0 comments on commit 449f0d9

Please sign in to comment.