Remix Auth Features
- Full server-side authentication
- Full TypeScript support
- Policy-based authentication
- Handle success and failure with ease
- Implement custom strategies
- Support persistent sessions
Article directory
- Remix Auth Features
- Install dependencies
- Packaging services
- Login and callback
- Logout/logout
- TypeScript types
- FAQ
Install dependencies
npm i --save remix-auth remix-auth-github
These two packages are required. Then create auth related files, refer to the following structure:
├── app │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── root.tsx │ └── routes │ ├── _index.tsx │ ├── auth.github.callback.ts │ ├── auth.github.ts │ └── private.tsx └── tsconfig.json
Create two core files, auth.github.ts
and auth.github.callback.ts
, in the routes
directory.
Packaging services
Since Cloudflare Pages cannot use process.env
, a very clumsy operation is used to achieve it:
// auth.server.ts import {<!-- --> createCookieSessionStorage } from '@remix-run/cloudflare'; import {<!-- --> Authenticator } from 'remix-auth'; import type {<!-- --> SessionStorage } from '@remix-run/cloudflare'; import {<!-- --> GitHubStrategy } from 'remix-auth-github'; import {<!-- --> z } from 'zod'; import type {<!-- --> Env } from '../env'; const UserSchema = z.object({<!-- --> username: z.string(), displayName: z.string(), email: z.string().email().nullable(), avatar: z.string().url(), githubId: z.string().min(1), isSponsor: z.boolean() }); const SessionSchema = z.object({<!-- --> user: UserSchema.optional(), strategy: z.string().optional(), 'oauth2:state': z.string().uuid().optional(), 'auth:error': z.object({<!-- --> message: z.string() }).optional() }); export type User = z.infer<typeof UserSchema>; export type Session = z.infer<typeof SessionSchema>; export interface IAuthService {<!-- --> readonly authenticator: Authenticator<User>; readonly sessionStorage: TypedSessionStorage<typeof SessionSchema>; } export class AuthService implements IAuthService {<!-- --> #sessionStorage: SessionStorage<typeof SessionSchema>; #authenticator: Authenticator<User>; constructor(env: Env, hostname: string) {<!-- --> let sessionStorage = createCookieSessionStorage({<!-- --> cookie: {<!-- --> name: 'sid', httpOnly: true, secure: env.CF_PAGES === 'production', sameSite: 'lax', path: '/', secrets: [env.COOKIE_SESSION_SECRET] } }); this.#sessionStorage = sessionStorage; this.#authenticator = new Authenticator<User>(this.#sessionStorage as unknown as SessionStorage, {<!-- --> throwOnError: true }); let callbackURL = new URL(env.GITHUB_CALLBACK_URL); callbackURL.hostname = hostname; this.#authenticator.use( new GitHubStrategy( {<!-- --> clientID: env.GITHUB_ID, clientSecret: env.GITHUB_SECRET, callbackURL: callbackURL.toString() }, async ({<!-- --> profile }) => {<!-- --> return {<!-- --> displayName: profile._json.name, username: profile._json.login, email: profile._json.email profile.emails?.at(0) null, avatar: profile._json.avatar_url, githubId: profile._json.node_id // isSponsor: await gh.isSponsoringMe(profile._json.node_id) }; } ) ); } get authenticator() {<!-- --> return this.#authenticator; } get sessionStorage() {<!-- --> return this.#sessionStorage; } }
zod
is also used to define the type. This part can be ignored.
If you are interested, you can visit the Zod documentation to learn: https://zod.dev/
Then find server.ts
in the root directory and modify it. The main modification method is the getLoadContext
section, where services are injected as dependencies:
import {<!-- --> logDevReady } from '@remix-run/cloudflare'; import {<!-- --> createPagesFunctionHandler } from '@remix-run/cloudflare-pages'; import * as build from '@remix-run/dev/server-build'; import {<!-- --> AuthService } from '~/server/services/auth'; import {<!-- --> EnvSchema } from './env'; if (process.env.NODE_ENV === 'development') {<!-- --> logDevReady(build); } export const onRequest = createPagesFunctionHandler({<!-- --> build, getLoadContext: (ctx) => {<!-- --> const env = EnvSchema.parse(ctx.env); const {<!-- --> hostname } = new URL(ctx.request.url); const auth = new AuthService(env, hostname); const services: RemixServer.Services = {<!-- --> auth }; return {<!-- --> env, services }; }, mode: build.mode });
Login and callback
For this part, you can refer to the remix-auth documentation: https://github.com/sergiodxa/remix-auth
// auth.github.ts import type {<!-- --> ActionFunction } from '@remix-run/cloudflare'; export const action: ActionFunction = async ({<!-- --> request, context }) => {<!-- --> return await context.services.auth.authenticator.authenticate('github', request, {<!-- --> successRedirect: '/private', failureRedirect: '/' }); };
If you want to use Get request to log in, change action
to loader
.
// auth.github.callback.ts import type {<!-- --> LoaderFunction } from '@remix-run/cloudflare'; export const loader: LoaderFunction = async ({<!-- --> request, context }) => {<!-- --> return await context.services.auth.authenticator.authenticate('github', request, {<!-- --> successRedirect: '/private', failureRedirect: '/' }); };
Here, the service is passed in through context to avoid repeated use of env
environment variables for initialization.
Then write a route to test the login result:
import type {<!-- --> ActionFunction, LoaderFunction } from '@remix-run/cloudflare'; import {<!-- --> json } from '@remix-run/cloudflare'; import {<!-- --> Form, useLoaderData } from '@remix-run/react'; export const action: ActionFunction = async ({<!-- --> request }) => {<!-- --> await auth.logout(request, {<!-- --> redirectTo: '/' }); }; export const loader: LoaderFunction = async ({<!-- --> request, context }) => {<!-- --> const profile = await context.services.auth.authenticator.isAuthenticated(request, {<!-- --> failureRedirect: '/' }); return json({<!-- --> profile }); }; export default function Screen() {<!-- --> const {<!-- --> profile } = useLoaderData<typeof loader>(); return ( <> <Form method='post'> <button>Log Out</button> </Form> <hr /> <pre> <code>{<!-- -->JSON.stringify(profile, null, 2)}</code>
>
);
}
Logout/logout
You can refer to the following code to create a new routing implementation:
export async function action({<!-- --> request }: ActionArgs) {<!-- --> await authenticator.logout(request, {<!-- --> redirectTo: "/login" }); };
TypeScript types
If you need to pass type hints, add a .d.ts
file, or add a type declaration in root.tsx
:
import type {<!-- --> Env } from './env'; import type {<!-- --> IAuthService } from './services/auth'; declare global {<!-- --> namespace RemixServer {<!-- --> export interface Services {<!-- --> auth:IAuthService; } } } declare module '@remix-run/cloudflare' {<!-- --> interface AppLoadContext {<!-- --> env: Env; DB: D1Database; services: RemixServer.Services; } }
Referring to this, Env is the type definition of the environment variable you need.
Finish. The complete sample code is at: https://github.com/willin/remix-cloudflare-pages-demo/tree/c8c350ce954d14cdc68f1f9cd11cecea00600483
FAQ
Note: After version v2, remix-utils cannot be used. There are compatibility issues.