Add Login to Your Remix App
This quickstart shows how to integrate Optare with Remix.
Prerequisites
- Node.js 18+ installed
- An Optare account
- A Remix project
1. Install the SDK
npm install @optare/optareid-js jose2. Get Your Credentials
- Log in to Optare Console (opens in a new tab)
- Create an OAuth client:
- Type: Regular Web Application
- Redirect URI:
https://yourapp.com/auth/callback
- Copy Client ID and Client Secret
3. Configure Environment
Create .env:
OPTARE_DOMAIN=https://id.optare.one
OPTARE_CLIENT_ID=your_client_id
OPTARE_CLIENT_SECRET=your_client_secret
SESSION_SECRET=your_session_secret4. Create Session Utility
Create app/utils/session.server.ts:
import { createCookieSessionStorage } from '@remix-run/node';
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
maxAge: 60 * 60 * 24 * 30,
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET!],
secure: process.env.NODE_ENV === 'production',
},
});5. Create Auth Routes
Create app/routes/auth.login.tsx:
import { redirect, type LoaderFunctionArgs } from '@remix-run/node';
export async function loader({ request }: LoaderFunctionArgs) {
const params = new URLSearchParams({
client_id: process.env.OPTARE_CLIENT_ID!,
redirect_uri: 'https://yourapp.com/auth/callback',
response_type: 'code',
scope: 'openid profile email organization',
});
return redirect(
`${process.env.OPTARE_DOMAIN}/oauth/authorize?${params}`
);
}Create app/routes/auth.callback.tsx:
import { redirect, type LoaderFunctionArgs } from '@remix-run/node';
import { sessionStorage } from '~/utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
if (!code) {
return redirect('/login?error=no_code');
}
// Exchange code for tokens
const tokenResponse = await fetch(
`${process.env.OPTARE_DOMAIN}/oauth/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: process.env.OPTARE_CLIENT_ID,
client_secret: process.env.OPTARE_CLIENT_SECRET,
redirect_uri: 'https://yourapp.com/auth/callback',
}),
}
);
const tokens = await tokenResponse.json();
// Store tokens in session
const session = await sessionStorage.getSession();
session.set('access_token', tokens.access_token);
session.set('refresh_token', tokens.refresh_token);
return redirect('/dashboard', {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session),
},
});
}6. Protect Routes
Create app/utils/auth.server.ts:
import { redirect } from '@remix-run/node';
import * as jose from 'jose';
import { sessionStorage } from './session.server';
export async function requireAuth(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get('Cookie')
);
const token = session.get('access_token');
if (!token) {
throw redirect('/auth/login');
}
// Validate token
const JWKS = jose.createRemoteJWKSet(
new URL(`${process.env.OPTARE_DOMAIN}/.well-known/jwks.json`)
);
try {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: process.env.OPTARE_DOMAIN,
});
return payload;
} catch {
throw redirect('/auth/login');
}
}7. Use in Loaders
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { requireAuth } from '~/utils/auth.server';
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireAuth(request);
return json({ user });
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user.email}</p>
</div>
);
}Next Steps
- Organizations - Multi-tenant support
- RBAC - Role-based access control