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

Add auth remix #810

Merged
merged 19 commits into from
Jan 3, 2024
Merged
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
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,62 @@ jobs:
body: ${{steps.github_auth_express_release.outputs.changelog}}
draft: true
prerelease: false

# @edgedb/auth-remix

- name: Build @edgedb/auth-remix
run: yarn workspace @edgedb/auth-remix run build

- id: check_publish_auth_remix
name: Dry-run publish '@edgedb/auth-remix' to npm
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552
with:
package: packages/auth-remix/package.json
token: ${{ secrets.NPM_TOKEN }}
dry-run: true

- name: If '@edgedb/auth-remix' version unchanged
if: steps.check_publish_auth_remix.outputs.type == ''
run: |
echo "Version in package.json has not changed. Creating canary release."
yarn workspace @edgedb/auth-remix version --no-git-tag-version --minor
CURRENT_VERSION=$(node -p "require('./packages/auth-remix/package.json').version")
yarn workspace @edgedb/auth-remix version --no-git-tag-version --new-version "${CURRENT_VERSION}-canary.$(date +'%Y%m%dT%H%M%S')"

- name: Publish '@edgedb/auth-remix'
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552
with:
package: packages/auth-remix/package.json
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: ${{ steps.check_publish_auth_remix.outputs.type == '' && 'canary' || 'latest' }}

- name: Build '@edgedb/auth-remix' Changelog
if: steps.check_publish_auth_remix.outputs.type != ''
id: github_auth_remix_release
uses: mikepenz/release-changelog-builder-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
fromTag: "${{ env.last_tag }}"
toTag: ${{ github.ref }}
commitMode: true
configurationJson: |
{
"template": "## Commits:\n\n#{{UNCATEGORIZED}}",
"pr_template": "- #{{MERGE_SHA}} #{{TITLE}}",
"categories": []
}

- name: Create '@edgedb/auth-remix' Release
if: steps.check_publish_auth_remix.outputs.type != ''
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: auth-remix-v${{ steps.check_publish_auth_remix.outputs.version }}
release_name: \@edgedb/auth-remix v${{ steps.check_publish_auth_remix.outputs.version }}
commitish: ${{ github.ref }}
body: ${{steps.github_auth_remix_release.outputs.changelog}}
draft: true
prerelease: false
36 changes: 36 additions & 0 deletions packages/auth-remix/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@edgedb/auth-remix",
"description": "Helper library to integrate the EdgeDB Auth extension with Remix",
"version": "0.1.0-alpha.1",
"author": "EdgeDB <[email protected]>",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/edgedb/edgedb-js.git",
"directory": "packages/auth-remix"
},
"license": "Apache-2.0",
"sideEffects": false,
"files": [
"/dist"
],
"scripts": {
"build": "tsc --project tsconfig.json"
},
"devDependencies": {
"@types/node": "^20.8.4",
"edgedb": "^1.3.6",
"typescript": "^5.2.2"
},
"peerDependencies": {
"edgedb": "^1.3.6"
},
"dependencies": {
"@edgedb/auth-core": "^0.1.0-beta.1",
"@remix-run/server-runtime": "^2.3.1",
"cookie": "^0.6.0"
},
"exports": {
"./*": "./dist/*.js"
}
}
149 changes: 149 additions & 0 deletions packages/auth-remix/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# @edgedb/auth-remix

This library provides a set of utilities to help you integrate authentication into your [Remix](https://remix.run/) application.
It supports authentication with various OAuth providers, as well as email/password authentication.

## Installation

This package is published on npm and can be installed with your package manager of choice.

```bash
npm install @edgedb/auth-remix
```

## Setup

**Prerequisites**: Before adding EdgeDB auth to your Remix app, you will first need to enable the `auth` extension in your EdgeDB schema, and have configured the extension with some providers (you can do this in CLI or EdgeDB UI). Refer to the auth extension docs for details on how to do this.

1. Initialize the client auth helper by passing configuration options to `createClientAuth()`. This will return a `RemixClientAuth` object which you can use in your components. You can skip this part if you find it unnecessary and provide all your data through the loader (the next step), but we suggest having the client auth too and use it directly in your components to get OAuth, BuiltinUI and signout URLs.

```ts
// app/services/auth.ts

import createClientAuth, {
type RemixAuthOptions,
} from "@edgedb/auth-remix/client";

export const options: RemixAuthOptions = {
baseUrl: "http://localhost:3000",
// ...
};

const auth = createClientAuth(options);

export default auth;
```

2. Initialize the server auth helper by passing an EdgeDB `Client` object to `createServerAuth()`, along with configuration options. This will return a `RemixServerAuth` object which you can use across your app on the server side.

```ts
// app/services/auth.server.ts

import createServerAuth from "@edgedb/auth-remix/server";
import { createClient } from "edgedb";
import { options } from "./auth.client";

export const client = createClient({
//Note: when developing locally you will need to set tls security to insecure, because the dev server uses self-signed certificates which will cause api calls with the fetch api to fail.
tlsSecurity: "insecure",
});

export const auth = createServerAuth(client, options);
```

The available auth config options are as follows:

- `baseUrl: string`, _required_, The url of your application; needed for various auth flows (eg. OAuth) to redirect back to.
- `authRoutesPath?: string`, The path to the auth route handlers, defaults to `'auth'`, see below for more details.
- `authCookieName?: string`, The name of the cookie where the auth token will be stored, defaults to `'edgedb-session'`.
- `pkceVerifierCookieName?: string`: The name of the cookie where the verifier for the PKCE flow will be stored, defaults to `'edgedb-pkce-verifier'`
- `passwordResetUrl?: string`: The url of the the password reset page; needed if you want to enable password reset emails in your app.

3. Setup the auth route handlers, with `auth.createAuthRouteHandlers()`. Callback functions can be provided to handle various auth events, where you can define what to do in the case of successful signin's or errors. You only need to configure callback functions for the types of auth you wish to use in your app.

```ts
// app/routes/auth.$.ts

import { redirect } from "@remix-run/node";
import { auth } from "~/services/auth.server";

export const { loader } = auth.createAuthRouteHandlers({
onOAuthCallback({ error, tokenData, provider, isSignUp }) {
return redirect("/");
},
onSignout() {
return redirect("/");
},
});
```

The currently available auth handlers are:

- `onOAuthCallback`
- `onBuiltinUICallback`
- `onEmailVerify`
- `onSignout`

By default the handlers expect to exist under the `/routes/auth` path in your app, however if you want to place them elsewhere, you will also need to configure the `authRoutesPath` option of `createServerAuth` to match.

4. Now we just need to setup the UI to allow your users to sign in/up, etc. The easiest way to get started is to use the EdgeDB Auth's builtin UI. Or alternatively you can implement your own custom UI.

**Builtin UI**

To use the builtin auth UI, first you will need to enable the UI in the auth ext configuration (see the auth ext docs for details). For the `redirect_to` and `redirect_to_on_signup` configuration options, set them to `{your_app_url}/auth/builtin/callback` and `{your_app_url}/auth/builtin/callback?isSignUp=true` respectively. (Note: if you have setup the auth route handlers under a custom path, replace `auth` in the above url with that path).

Then you just need to configure the `onBuiltinUICallback` route handler to define what to do once the builtin ui redirects back to your app, and place a link to the builtin UI url returned by `auth.getBuiltinUIUrl()` somewhere in your app.

**Custom UI**

To help with implementing your own custom auth UI, the `auth` object has a number of methods you can use:

- `emailPasswordSignUp`
- `emailPasswordSignIn`
- `emailPasswordResendVerificationEmail`
- `emailPasswordSendPasswordResetEmail`
- `emailPasswordResetPassword`
- `signout`
- `isPasswordResetTokenValid(resetToken: string)`: Checks if a password reset token is still valid.
- `getOAuthUrl(providerName: string)`: This method takes the name of an OAuth provider (make sure you configure providers you need in the auth ext config first using CLI or EdgeDB UI) and returns a link that will initiate the OAuth sign in flow for that provider. You will also need to configure the `onOAuthCallback` auth route handler.

## Usage

Now you have auth all configured and user's can signin/signup/etc. you can use the `auth.getSession()` method in your app pages to retrieve an `AuthSession` object. This session object allows you to check if the user is currently signed in with the `isSignedIn` method, and also provides a `Client` object automatically configured with the `ext::auth::client_token` global, so you can run queries using the `ext::auth::ClientTokenIdentity` of the currently signed in user.

```ts
// app/routes/_index.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, Link } from "@remix-run/react";
import auth, { client } from "~/services/auth.server";
import clientAuth from "~/services/auth.client";
import { transformSearchParams } from "~/utils";

export const loader = async ({ request }: LoaderFunctionArgs) => {
const session = auth.getSession(request);
const isSignedIn = await session.isSignedIn();

return json({
isSignedIn,
});
};

export default function Index() {
const { isSignedIn } = useLoaderData<typeof loader>();

return (
<main>
<h1>Home</h1>
{isSignedIn ? (
<h2>You are logged in</h2>
) : (
<>
<h2>You are not logged in</h2>
<Link to={clientAuth.getBuiltinUIUrl()}>Sign in with Builtin UI</Link>
</>
)}
</main>
);
}
```
56 changes: 56 additions & 0 deletions packages/auth-remix/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { BuiltinOAuthProviderNames } from "@edgedb/auth-core";

export interface RemixAuthOptions {
baseUrl: string;
authRoutesPath?: string;
authCookieName?: string;
pkceVerifierCookieName?: string;
passwordResetPath?: string;
}

type OptionalOptions = "passwordResetPath";

export default function createClientAuth(options: RemixAuthOptions) {
return new RemixClientAuth(options);
}

export class RemixClientAuth {
protected readonly options: Required<
Omit<RemixAuthOptions, OptionalOptions>
> &
Pick<RemixAuthOptions, OptionalOptions>;

/** @internal */
constructor(options: RemixAuthOptions) {
this.options = {
baseUrl: options.baseUrl.replace(/\/$/, ""),
authRoutesPath: options.authRoutesPath?.replace(/^\/|\/$/g, "") ?? "auth",
authCookieName: options.authCookieName ?? "edgedb-session",
pkceVerifierCookieName:
options.pkceVerifierCookieName ?? "edgedb-pkce-verifier",
passwordResetPath: options.passwordResetPath,
};
}

protected get _authRoute() {
return `${this.options.baseUrl}/${this.options.authRoutesPath}`;
}

getOAuthUrl(providerName: BuiltinOAuthProviderNames) {
return `${this._authRoute}/oauth?${new URLSearchParams({
provider_name: providerName,
}).toString()}`;
}

getBuiltinUIUrl() {
return `${this._authRoute}/builtin/signin`;
}

getBuiltinUISignUpUrl() {
return `${this._authRoute}/builtin/signup`;
}

getSignoutUrl() {
return `${this._authRoute}/signout`;
}
}
Loading
Loading