@workos-inc/authkit-remix
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -15,5 +15,3 @@ "use strict"; | ||
const state = url.searchParams.get('state'); | ||
const returnPathname = state | ||
? JSON.parse(atob(state)).returnPathname | ||
: null; | ||
const returnPathname = state ? JSON.parse(atob(state)).returnPathname : null; | ||
if (code) { | ||
@@ -37,2 +35,3 @@ try { | ||
impersonator, | ||
headers: {}, | ||
}); | ||
@@ -39,0 +38,0 @@ const session = await (0, cookie_js_1.getSession)(cookie_js_1.cookieName); |
@@ -18,5 +18,3 @@ "use strict"; | ||
// act as the actual time-limited aspects of the session. | ||
maxAge: env_variables_js_1.WORKOS_COOKIE_MAX_AGE | ||
? parseInt(env_variables_js_1.WORKOS_COOKIE_MAX_AGE, 10) | ||
: 60 * 60 * 24 * 400, | ||
maxAge: env_variables_js_1.WORKOS_COOKIE_MAX_AGE ? parseInt(env_variables_js_1.WORKOS_COOKIE_MAX_AGE, 10) : 60 * 60 * 24 * 400, | ||
secrets: [env_variables_js_1.WORKOS_COOKIE_PASSWORD], | ||
@@ -23,0 +21,0 @@ }; |
@@ -12,5 +12,3 @@ "use strict"; | ||
redirectUri: env_variables_js_1.WORKOS_REDIRECT_URI, | ||
state: returnPathname | ||
? btoa(JSON.stringify({ returnPathname })) | ||
: undefined, | ||
state: returnPathname ? btoa(JSON.stringify({ returnPathname })) : undefined, | ||
screenHint, | ||
@@ -17,0 +15,0 @@ }); |
import { authLoader } from './authkit-callback-route.js'; | ||
import { withAuth } from './session.js'; | ||
import { authkitLoader } from './session.js'; | ||
import { getSignInUrl, getSignUpUrl, signOut } from './auth.js'; | ||
export { authLoader, getSignInUrl, getSignUpUrl, signOut, withAuth, }; | ||
export { authLoader, getSignInUrl, getSignUpUrl, signOut, authkitLoader, }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.withAuth = exports.signOut = exports.getSignUpUrl = exports.getSignInUrl = exports.authLoader = void 0; | ||
exports.authkitLoader = exports.signOut = exports.getSignUpUrl = exports.getSignInUrl = exports.authLoader = void 0; | ||
const authkit_callback_route_js_1 = require("./authkit-callback-route.js"); | ||
Object.defineProperty(exports, "authLoader", { enumerable: true, get: function () { return authkit_callback_route_js_1.authLoader; } }); | ||
const session_js_1 = require("./session.js"); | ||
Object.defineProperty(exports, "withAuth", { enumerable: true, get: function () { return session_js_1.withAuth; } }); | ||
Object.defineProperty(exports, "authkitLoader", { enumerable: true, get: function () { return session_js_1.authkitLoader; } }); | ||
const auth_js_1 = require("./auth.js"); | ||
@@ -9,0 +9,0 @@ Object.defineProperty(exports, "getSignInUrl", { enumerable: true, get: function () { return auth_js_1.getSignInUrl; } }); |
@@ -14,19 +14,4 @@ import { User } from '@workos-inc/node'; | ||
impersonator?: Impersonator; | ||
headers: Record<string, string>; | ||
} | ||
export interface UserInfo { | ||
user: User; | ||
sessionId: string; | ||
organizationId?: string; | ||
role?: string; | ||
permissions?: string[]; | ||
impersonator?: Impersonator; | ||
accessToken: string; | ||
} | ||
export interface NoUserInfo { | ||
user: null; | ||
sessionId?: undefined; | ||
organizationId?: undefined; | ||
role?: undefined; | ||
impersonator?: undefined; | ||
} | ||
export interface AccessToken { | ||
@@ -42,9 +27,23 @@ sid: string; | ||
} | ||
export interface AuthkitMiddlewareAuth { | ||
enabled: boolean; | ||
unauthenticatedPaths: string[]; | ||
} | ||
export interface AuthkitMiddlewareOptions { | ||
export interface AuthKitLoaderOptions { | ||
ensureSignedIn?: boolean; | ||
debug?: boolean; | ||
middlewareAuth?: AuthkitMiddlewareAuth; | ||
} | ||
export interface AuthorizedData { | ||
user: User; | ||
sessionId: string; | ||
accessToken: string; | ||
organizationId: string | null; | ||
role: string | null; | ||
permissions: string[]; | ||
impersonator: Impersonator | null; | ||
} | ||
export interface UnauthorizedData { | ||
user: null; | ||
sessionId: null; | ||
accessToken: null; | ||
organizationId: null; | ||
role: null; | ||
permissions: null; | ||
impersonator: null; | ||
} |
@@ -1,12 +0,21 @@ | ||
import { NoUserInfo, Session, UserInfo } from './interfaces.js'; | ||
import type { LoaderFunctionArgs, TypedResponse } from '@remix-run/node'; | ||
import type { AuthorizedData, UnauthorizedData, AuthKitLoaderOptions, Session } from './interfaces.js'; | ||
declare function encryptSession(session: Session): Promise<string>; | ||
declare function withAuth(request: Request, options?: { | ||
ensureSignedIn?: false; | ||
debug?: boolean; | ||
}): Promise<UserInfo | NoUserInfo>; | ||
declare function withAuth(request: Request, options: { | ||
ensureSignedIn?: true; | ||
debug?: boolean; | ||
}): Promise<UserInfo>; | ||
declare function terminateSession(request: Request): Promise<import("@remix-run/node").TypedResponse<never>>; | ||
export { encryptSession, withAuth, terminateSession }; | ||
type LoaderValue<Data> = Response | TypedResponse<Data> | NonNullable<Data> | null; | ||
type LoaderReturnValue<Data> = Promise<LoaderValue<Data>> | LoaderValue<Data>; | ||
type AuthLoader<Data> = (args: LoaderFunctionArgs & { | ||
auth: AuthorizedData | UnauthorizedData; | ||
}) => LoaderReturnValue<Data>; | ||
type AuthorizedAuthLoader<Data> = (args: LoaderFunctionArgs & { | ||
auth: AuthorizedData; | ||
}) => LoaderReturnValue<Data>; | ||
declare function authkitLoader(loaderArgs: LoaderFunctionArgs, options: AuthKitLoaderOptions & { | ||
ensureSignedIn: true; | ||
}): Promise<TypedResponse<AuthorizedData>>; | ||
declare function authkitLoader(loaderArgs: LoaderFunctionArgs, options?: AuthKitLoaderOptions): Promise<TypedResponse<AuthorizedData | UnauthorizedData>>; | ||
declare function authkitLoader<Data = unknown>(loaderArgs: LoaderFunctionArgs, loader: AuthorizedAuthLoader<Data>, options: AuthKitLoaderOptions & { | ||
ensureSignedIn: true; | ||
}): Promise<TypedResponse<Data & AuthorizedData>>; | ||
declare function authkitLoader<Data = unknown>(loaderArgs: LoaderFunctionArgs, loader: AuthLoader<Data>, options?: AuthKitLoaderOptions): Promise<TypedResponse<Data & (AuthorizedData | UnauthorizedData)>>; | ||
declare function terminateSession(request: Request): Promise<TypedResponse<never>>; | ||
export { encryptSession, terminateSession, authkitLoader }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.terminateSession = exports.withAuth = exports.encryptSession = void 0; | ||
exports.authkitLoader = exports.terminateSession = exports.encryptSession = void 0; | ||
const node_1 = require("@remix-run/node"); | ||
@@ -39,2 +39,3 @@ const env_variables_js_1 = require("./env-variables.js"); | ||
impersonator: session.impersonator, | ||
headers: {}, | ||
}; | ||
@@ -44,3 +45,5 @@ // Encrypt session with new access and refresh tokens | ||
updatedSession.set('jwt', await encryptSession(newSession)); | ||
await (0, cookie_js_1.commitSession)(updatedSession); | ||
newSession.headers = { | ||
'Set-Cookie': await (0, cookie_js_1.commitSession)(updatedSession), | ||
}; | ||
return newSession; | ||
@@ -63,3 +66,7 @@ } | ||
exports.encryptSession = encryptSession; | ||
async function withAuth(request, { ensureSignedIn = false, debug = false } = {}) { | ||
async function authkitLoader(loaderArgs, loaderOrOptions, options = {}) { | ||
var _a; | ||
const loader = typeof loaderOrOptions === 'function' ? loaderOrOptions : undefined; | ||
const { ensureSignedIn = false, debug = false } = typeof loaderOrOptions === 'object' ? loaderOrOptions : options; | ||
const { request } = loaderArgs; | ||
const session = await updateSession(request, debug); | ||
@@ -76,21 +83,57 @@ if (!session) { | ||
} | ||
return { user: null }; | ||
const auth = { | ||
user: null, | ||
accessToken: null, | ||
impersonator: null, | ||
organizationId: null, | ||
permissions: null, | ||
role: null, | ||
sessionId: null, | ||
}; | ||
return await handleAuthLoader(loader, loaderArgs, auth); | ||
} | ||
const { sid: sessionId, org_id: organizationId, role, permissions } = (0, jose_1.decodeJwt)(session.accessToken); | ||
return { | ||
const { sessionId, organizationId = null, role = null, permissions = [], } = getClaimsFromAccessToken(session.accessToken); | ||
const auth = { | ||
user: session.user, | ||
sessionId, | ||
user: session.user, | ||
accessToken: session.accessToken, | ||
organizationId, | ||
role, | ||
permissions, | ||
impersonator: session.impersonator, | ||
accessToken: session.accessToken, | ||
impersonator: (_a = session.impersonator) !== null && _a !== void 0 ? _a : null, | ||
}; | ||
return await handleAuthLoader(loader, loaderArgs, auth, session); | ||
} | ||
exports.withAuth = withAuth; | ||
exports.authkitLoader = authkitLoader; | ||
async function handleAuthLoader(loader, args, auth, session) { | ||
if (!loader) { | ||
return (0, node_1.json)(auth, session ? { headers: { ...session.headers } } : undefined); | ||
} | ||
// If there's a custom loader, get the resulting data and return it with our | ||
// auth data plus session cookie header | ||
const loaderResult = await loader({ ...args, auth: auth }); | ||
if (loaderResult instanceof Response) { | ||
// If the result is a redirect, return it unedited | ||
if (loaderResult.status >= 300 && loaderResult.status < 400) { | ||
return loaderResult; | ||
} | ||
const newResponse = new Response(loaderResult.body, loaderResult); | ||
const data = await newResponse.json(); | ||
// Set the content type in case the user returned a Response instead of the | ||
// json helper method | ||
newResponse.headers.set('Content-Type', 'application/json; charset=utf-8'); | ||
if (session) { | ||
newResponse.headers.append('Set-Cookie', session.headers['Set-Cookie']); | ||
} | ||
return (0, node_1.json)({ ...data, ...auth }, newResponse); | ||
} | ||
// If the loader returns a non-Response, assume it's a data object | ||
return (0, node_1.json)({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined); | ||
} | ||
async function terminateSession(request) { | ||
const { sessionId } = await withAuth(request); | ||
const cookieSession = await (0, cookie_js_1.getSession)(request.headers.get('Cookie')); | ||
const encryptedSession = await (0, cookie_js_1.getSession)(request.headers.get('Cookie')); | ||
const { accessToken } = (await getSessionFromCookie(request.headers.get('Cookie'), encryptedSession)); | ||
const { sessionId } = getClaimsFromAccessToken(accessToken); | ||
const headers = { | ||
'Set-Cookie': await (0, cookie_js_1.destroySession)(cookieSession), | ||
'Set-Cookie': await (0, cookie_js_1.destroySession)(encryptedSession), | ||
}; | ||
@@ -107,4 +150,15 @@ if (sessionId) { | ||
exports.terminateSession = terminateSession; | ||
async function getSessionFromCookie(cookie) { | ||
const session = await (0, cookie_js_1.getSession)(cookie); | ||
function getClaimsFromAccessToken(accessToken) { | ||
const { sid: sessionId, org_id: organizationId, role, permissions } = (0, jose_1.decodeJwt)(accessToken); | ||
return { | ||
sessionId, | ||
organizationId, | ||
role, | ||
permissions, | ||
}; | ||
} | ||
async function getSessionFromCookie(cookie, session) { | ||
if (!session) { | ||
session = await (0, cookie_js_1.getSession)(cookie); | ||
} | ||
if (session.has('jwt')) { | ||
@@ -111,0 +165,0 @@ return (0, iron_session_1.unsealData)(session.get('jwt'), { |
@@ -6,2 +6,3 @@ "use strict"; | ||
const env_variables_js_1 = require("./env-variables.js"); | ||
const VERSION = '0.3.0'; | ||
const options = { | ||
@@ -13,3 +14,3 @@ apiHostname: env_variables_js_1.WORKOS_API_HOSTNAME, | ||
name: 'authkit-remix', | ||
version: '0.2.0', | ||
version: VERSION, | ||
}, | ||
@@ -16,0 +17,0 @@ }; |
{ | ||
"name": "@workos-inc/authkit-remix", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Authentication and session helpers for using WorkOS & AuthKit with Remix", | ||
@@ -29,3 +29,3 @@ "sideEffects": false, | ||
"peerDependencies": { | ||
"@remix-run/node": "^2.7.2", | ||
"@remix-run/node": "^2.4.1", | ||
"react": "^18.2.0" | ||
@@ -39,2 +39,3 @@ }, | ||
"@typescript-eslint/eslint-plugin": "^6.7.4", | ||
"prettier": "^3.3.3", | ||
"typescript": "^5.4.2", | ||
@@ -41,0 +42,0 @@ "typescript-eslint": "^7.2.0" |
108
README.md
@@ -43,3 +43,3 @@ # AuthKit Remix Library | ||
```sh | ||
WORKOS_COOKIE_MAX_AGE='600' # maximum age of the cookie in seconds. Defaults to 31 days | ||
WORKOS_COOKIE_MAX_AGE='600' # maximum age of the cookie in seconds. Defaults to 400 days | ||
WORKOS_API_HOSTNAME='api.workos.com' # base WorkOS API URL | ||
@@ -54,3 +54,3 @@ WORKOS_API_HTTPS=true # whether to use HTTPS in API calls | ||
WorkOS requires that you have a callback URL to redirect users back to after they've authenticated. In your Remix app, [create a new route](https://remix.run/docs/en/main/discussion/routes) and add the following: | ||
AuthKit requires that you have a callback URL to redirect users back to after they've authenticated. In your Remix app, [create a new route](https://remix.run/docs/en/main/discussion/routes) and add the following: | ||
@@ -63,3 +63,3 @@ ```ts | ||
Make sure this route matches the `WORKOS_REDIRECT_URI` variable and the configured redirect URI in your WorkOS dashboard. For instance if your redirect URI is `http://localhost:5173/callback` then you'd put the above code in `/app/routes/callback.ts`. | ||
Make sure this route matches the `WORKOS_REDIRECT_URI` variable and the configured redirect URI in your WorkOS dashboard. For instance if your redirect URI is `http://localhost:2884/callback` then you'd put the above code in `/app/routes/callback.ts`. | ||
@@ -74,21 +74,34 @@ You can also control the pathname the user will be sent to after signing-in by passing a `returnPathname` option to `authLoader` like so: | ||
### Get the current user | ||
### Access authentication data in your Remix application | ||
For pages where you want to display a signed-in and signed-out view, use `withAuth` to retrieve the user profile from WorkOS. | ||
Use `authkitLoader` to configure AuthKit for your Remix application routes. | ||
```jsx | ||
```tsx | ||
import type { LoaderFunctionArgs } from '@remix-run/node'; | ||
import { authkitLoader } from '@workos-inc/authkit-remix'; | ||
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args); | ||
export function App() { | ||
return ( | ||
<div> | ||
<p>Welcome back {user?.firstName && `, ${user?.firstName}`}</p> | ||
</div> | ||
); | ||
} | ||
``` | ||
For pages where you want to display a signed-in and signed-out view, use `authkitLoader` to retrieve the user profile from WorkOS. You can pass in additional data by providing a loader function directly to `authkitLoader`. | ||
```tsx | ||
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'; | ||
import { Link, useLoaderData, json, Form } from '@remix-run/react'; | ||
import { getSignInUrl, getSignUpUrl, withAuth, signOut } from '@workos-inc/authkit-remix'; | ||
import { getSignInUrl, getSignUpUrl, signOut, authkitLoader } from '@workos-inc/authkit-remix'; | ||
export async function loader({request}: LoaderFunctionArgs) { | ||
// additional properties include: sessionId, organizationId, role, impersonator, accessToken | ||
const {user} = await withAuth(request); | ||
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, async ({ request, auth }) => { | ||
return json({ | ||
signInUrl: await getSignInUrl(), | ||
signUpUrl: await getSignUpUrl(), | ||
user, | ||
signInUrl: await getSignInUrl(); | ||
signUpUrl: await getSignUpUrl(); | ||
}); | ||
} | ||
}); | ||
@@ -101,2 +114,3 @@ export async function action({ request }: ActionFunctionArgs) { | ||
// Retrieves the user from the session or returns `null` if no user is signed in | ||
// Other supported values include sessionId, accessToken, organizationId, role, permissions and impersonator | ||
const { user, signInUrl, signUpUrl } = useLoaderData<typeof loader>(); | ||
@@ -127,4 +141,4 @@ | ||
```jsx | ||
const { user } = await withAuth(request, { ensureSignedIn: true }); | ||
```tsx | ||
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true }); | ||
``` | ||
@@ -142,23 +156,24 @@ | ||
```jsx | ||
```tsx | ||
import type { LoaderFunctionArgs, json } from '@remix-run/node'; | ||
import { withAuth } from '@workos-inc/authkit-remix'; | ||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const { accessToken } = await withAuth(request); | ||
export const loader = (args: LoaderFunctionArgs) => | ||
authkitLoader(args, async ({ auth }) => { | ||
const { accessToken } = auth; | ||
if (!accesstoken) { | ||
// Not signed in | ||
} | ||
if (!accessToken) { | ||
// Not signed in | ||
} | ||
const serviceData = await fetch('/api/path', { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}); | ||
const serviceData = await fetch('/api/path', { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
}); | ||
return json({ | ||
data: serviceData, | ||
return json({ | ||
data: serviceData, | ||
}); | ||
}); | ||
} | ||
``` | ||
@@ -168,18 +183,23 @@ | ||
To enable debug logs, pass in the debug flag when using `withAuth`. | ||
To enable debug logs, pass in the debug flag when using `authkitLoader`. | ||
```js | ||
import { withAuth, getSignInUrl, getSignUpUrl } from '@workos-inc/authkit-remix'; | ||
```ts | ||
import { authkitLoader } from '@workos-inc/authkit-remix'; | ||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const { user } = await withAuth(request, { | ||
debug: true, | ||
}); | ||
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { debug: true }); | ||
``` | ||
return json({ | ||
signInUrl: await getSignInUrl(), | ||
signUpUrl: await getSignUpUrl(), | ||
user, | ||
}); | ||
} | ||
If providing a loader function, you can pass the options object as the third parameter | ||
```ts | ||
import { authkitLoader } from '@workos-inc/authkit-remix'; | ||
export const loader = (args: LoaderFunctionArgs) => | ||
authkitLoader( | ||
args, | ||
async ({ auth }) => { | ||
return json({ foo: 'bar' }); | ||
}, | ||
{ debug: true }, | ||
); | ||
``` |
@@ -16,13 +16,10 @@ import { HandleAuthOptions } from './interfaces.js'; | ||
const state = url.searchParams.get('state'); | ||
const returnPathname = state | ||
? JSON.parse(atob(state)).returnPathname | ||
: null; | ||
const returnPathname = state ? JSON.parse(atob(state)).returnPathname : null; | ||
if (code) { | ||
try { | ||
const { accessToken, refreshToken, user, impersonator } = | ||
await workos.userManagement.authenticateWithCode({ | ||
clientId: WORKOS_CLIENT_ID, | ||
code, | ||
}); | ||
const { accessToken, refreshToken, user, impersonator } = await workos.userManagement.authenticateWithCode({ | ||
clientId: WORKOS_CLIENT_ID, | ||
code, | ||
}); | ||
@@ -43,2 +40,3 @@ // Clean up params | ||
impersonator, | ||
headers: {}, | ||
}); | ||
@@ -72,7 +70,6 @@ | ||
message: 'Something went wrong', | ||
description: | ||
'Couldn’t sign in. If you are not sure what happened, please contact your organization admin.', | ||
description: 'Couldn’t sign in. If you are not sure what happened, please contact your organization admin.', | ||
}, | ||
}, | ||
{ status: 500 } | ||
{ status: 500 }, | ||
); | ||
@@ -79,0 +76,0 @@ } |
@@ -1,6 +0,2 @@ | ||
import { | ||
WORKOS_REDIRECT_URI, | ||
WORKOS_COOKIE_MAX_AGE, | ||
WORKOS_COOKIE_PASSWORD, | ||
} from './env-variables.js'; | ||
import { WORKOS_REDIRECT_URI, WORKOS_COOKIE_MAX_AGE, WORKOS_COOKIE_PASSWORD } from './env-variables.js'; | ||
import { createCookieSessionStorage } from '@remix-run/node'; | ||
@@ -20,15 +16,12 @@ | ||
// 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, | ||
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, | ||
}, | ||
}); | ||
const { getSession, commitSession, destroySession } = createCookieSessionStorage({ | ||
cookie: { | ||
name: cookieName, | ||
...cookieOptions, | ||
}, | ||
}); | ||
export { cookieName, getSession, commitSession, destroySession }; |
@@ -12,5 +12,3 @@ import { workos } from './workos.js'; | ||
redirectUri: WORKOS_REDIRECT_URI, | ||
state: returnPathname | ||
? btoa(JSON.stringify({ returnPathname })) | ||
: undefined, | ||
state: returnPathname ? btoa(JSON.stringify({ returnPathname })) : undefined, | ||
screenHint, | ||
@@ -17,0 +15,0 @@ }); |
import { authLoader } from './authkit-callback-route.js'; | ||
import { withAuth } from './session.js'; | ||
import { authkitLoader } from './session.js'; | ||
import { getSignInUrl, getSignUpUrl, signOut } from './auth.js'; | ||
@@ -12,3 +12,3 @@ | ||
// | ||
withAuth, | ||
authkitLoader, | ||
}; |
@@ -11,2 +11,3 @@ import { User } from '@workos-inc/node'; | ||
} | ||
export interface Session { | ||
@@ -17,21 +18,5 @@ accessToken: string; | ||
impersonator?: Impersonator; | ||
headers: Record<string, string>; | ||
} | ||
export interface UserInfo { | ||
user: User; | ||
sessionId: string; | ||
organizationId?: string; | ||
role?: string; | ||
permissions?: string[]; | ||
impersonator?: Impersonator; | ||
accessToken: string; | ||
} | ||
export interface NoUserInfo { | ||
user: null; | ||
sessionId?: undefined; | ||
organizationId?: undefined; | ||
role?: undefined; | ||
impersonator?: undefined; | ||
} | ||
export interface AccessToken { | ||
@@ -49,10 +34,25 @@ sid: string; | ||
export interface AuthkitMiddlewareAuth { | ||
enabled: boolean; | ||
unauthenticatedPaths: string[]; | ||
export interface AuthKitLoaderOptions { | ||
ensureSignedIn?: boolean; | ||
debug?: boolean; | ||
} | ||
export interface AuthkitMiddlewareOptions { | ||
debug?: boolean; | ||
middlewareAuth?: AuthkitMiddlewareAuth; | ||
export interface AuthorizedData { | ||
user: User; | ||
sessionId: string; | ||
accessToken: string; | ||
organizationId: string | null; | ||
role: string | null; | ||
permissions: string[]; | ||
impersonator: Impersonator | null; | ||
} | ||
export interface UnauthorizedData { | ||
user: null; | ||
sessionId: null; | ||
accessToken: null; | ||
organizationId: null; | ||
role: null; | ||
permissions: null; | ||
impersonator: null; | ||
} |
@@ -1,4 +0,5 @@ | ||
import { redirect } from '@remix-run/node'; | ||
import { json, redirect } from '@remix-run/node'; | ||
import type { LoaderFunctionArgs, SessionData, TypedResponse } from '@remix-run/node'; | ||
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD } from './env-variables.js'; | ||
import { AccessToken, NoUserInfo, Session, UserInfo } from './interfaces.js'; | ||
import type { AccessToken, AuthorizedData, UnauthorizedData, AuthKitLoaderOptions, Session } from './interfaces.js'; | ||
import { getSession, destroySession, commitSession } from './cookie.js'; | ||
@@ -14,3 +15,3 @@ import { getAuthorizationUrl } from './get-authorization-url.js'; | ||
async function updateSession(request: Request, debug: boolean) { | ||
const session = await getSessionFromCookie(request.headers.get('Cookie')); | ||
const session = await getSessionFromCookie(request.headers.get('Cookie') as string); | ||
@@ -45,2 +46,3 @@ // If no session, just continue | ||
impersonator: session.impersonator, | ||
headers: {}, | ||
}; | ||
@@ -51,4 +53,7 @@ | ||
updatedSession.set('jwt', await encryptSession(newSession)); | ||
await commitSession(updatedSession); | ||
newSession.headers = { | ||
'Set-Cookie': await commitSession(updatedSession), | ||
}; | ||
return newSession; | ||
@@ -72,19 +77,42 @@ } catch (e) { | ||
async function withAuth( | ||
request: Request, | ||
options?: { | ||
ensureSignedIn?: false; | ||
debug?: boolean; | ||
}, | ||
): Promise<UserInfo | NoUserInfo>; | ||
type LoaderValue<Data> = Response | TypedResponse<Data> | NonNullable<Data> | null; | ||
type LoaderReturnValue<Data> = Promise<LoaderValue<Data>> | LoaderValue<Data>; | ||
async function withAuth( | ||
request: Request, | ||
options: { | ||
ensureSignedIn?: true; | ||
debug?: boolean; | ||
}, | ||
): Promise<UserInfo>; | ||
type AuthLoader<Data> = ( | ||
args: LoaderFunctionArgs & { auth: AuthorizedData | UnauthorizedData }, | ||
) => LoaderReturnValue<Data>; | ||
async function withAuth(request: Request, { ensureSignedIn = false, debug = false } = {}) { | ||
type AuthorizedAuthLoader<Data> = (args: LoaderFunctionArgs & { auth: AuthorizedData }) => LoaderReturnValue<Data>; | ||
async function authkitLoader( | ||
loaderArgs: LoaderFunctionArgs, | ||
options: AuthKitLoaderOptions & { ensureSignedIn: true }, | ||
): Promise<TypedResponse<AuthorizedData>>; | ||
async function authkitLoader( | ||
loaderArgs: LoaderFunctionArgs, | ||
options?: AuthKitLoaderOptions, | ||
): Promise<TypedResponse<AuthorizedData | UnauthorizedData>>; | ||
async function authkitLoader<Data = unknown>( | ||
loaderArgs: LoaderFunctionArgs, | ||
loader: AuthorizedAuthLoader<Data>, | ||
options: AuthKitLoaderOptions & { ensureSignedIn: true }, | ||
): Promise<TypedResponse<Data & AuthorizedData>>; | ||
async function authkitLoader<Data = unknown>( | ||
loaderArgs: LoaderFunctionArgs, | ||
loader: AuthLoader<Data>, | ||
options?: AuthKitLoaderOptions, | ||
): Promise<TypedResponse<Data & (AuthorizedData | UnauthorizedData)>>; | ||
async function authkitLoader<Data = unknown>( | ||
loaderArgs: LoaderFunctionArgs, | ||
loaderOrOptions?: AuthLoader<Data> | AuthorizedAuthLoader<Data> | AuthKitLoaderOptions, | ||
options: AuthKitLoaderOptions = {}, | ||
) { | ||
const loader = typeof loaderOrOptions === 'function' ? loaderOrOptions : undefined; | ||
const { ensureSignedIn = false, debug = false } = typeof loaderOrOptions === 'object' ? loaderOrOptions : options; | ||
const { request } = loaderArgs; | ||
const session = await updateSession(request, debug); | ||
@@ -103,25 +131,84 @@ | ||
} | ||
return { user: null }; | ||
const auth: UnauthorizedData = { | ||
user: null, | ||
accessToken: null, | ||
impersonator: null, | ||
organizationId: null, | ||
permissions: null, | ||
role: null, | ||
sessionId: null, | ||
}; | ||
return await handleAuthLoader(loader, loaderArgs, auth); | ||
} | ||
const { sid: sessionId, org_id: organizationId, role, permissions } = decodeJwt<AccessToken>(session.accessToken); | ||
const { | ||
sessionId, | ||
organizationId = null, | ||
role = null, | ||
permissions = [], | ||
} = getClaimsFromAccessToken(session.accessToken); | ||
return { | ||
const auth: AuthorizedData = { | ||
user: session.user, | ||
sessionId, | ||
user: session.user, | ||
accessToken: session.accessToken, | ||
organizationId, | ||
role, | ||
permissions, | ||
impersonator: session.impersonator, | ||
accessToken: session.accessToken, | ||
impersonator: session.impersonator ?? null, | ||
}; | ||
return await handleAuthLoader(loader, loaderArgs, auth, session); | ||
} | ||
async function handleAuthLoader( | ||
loader: AuthLoader<unknown> | AuthorizedAuthLoader<unknown> | undefined, | ||
args: LoaderFunctionArgs, | ||
auth: AuthorizedData | UnauthorizedData, | ||
session?: Session, | ||
) { | ||
if (!loader) { | ||
return json(auth, session ? { headers: { ...session.headers } } : undefined); | ||
} | ||
// If there's a custom loader, get the resulting data and return it with our | ||
// auth data plus session cookie header | ||
const loaderResult = await loader({ ...args, auth: auth as AuthorizedData }); | ||
if (loaderResult instanceof Response) { | ||
// If the result is a redirect, return it unedited | ||
if (loaderResult.status >= 300 && loaderResult.status < 400) { | ||
return loaderResult; | ||
} | ||
const newResponse = new Response(loaderResult.body, loaderResult); | ||
const data = await newResponse.json(); | ||
// Set the content type in case the user returned a Response instead of the | ||
// json helper method | ||
newResponse.headers.set('Content-Type', 'application/json; charset=utf-8'); | ||
if (session) { | ||
newResponse.headers.append('Set-Cookie', session.headers['Set-Cookie']); | ||
} | ||
return json({ ...data, ...auth }, newResponse); | ||
} | ||
// If the loader returns a non-Response, assume it's a data object | ||
return json({ ...loaderResult, ...auth }, session ? { headers: { ...session.headers } } : undefined); | ||
} | ||
async function terminateSession(request: Request) { | ||
const { sessionId } = await withAuth(request); | ||
const encryptedSession = await getSession(request.headers.get('Cookie')); | ||
const { accessToken } = (await getSessionFromCookie( | ||
request.headers.get('Cookie') as string, | ||
encryptedSession, | ||
)) as Session; | ||
const cookieSession = await getSession(request.headers.get('Cookie')); | ||
const { sessionId } = getClaimsFromAccessToken(accessToken); | ||
const headers = { | ||
'Set-Cookie': await destroySession(cookieSession), | ||
'Set-Cookie': await destroySession(encryptedSession), | ||
}; | ||
@@ -134,2 +221,3 @@ | ||
} | ||
return redirect('/', { | ||
@@ -140,5 +228,18 @@ headers, | ||
async function getSessionFromCookie(cookie: string | null) { | ||
const session = await getSession(cookie); | ||
function getClaimsFromAccessToken(accessToken: string) { | ||
const { sid: sessionId, org_id: organizationId, role, permissions } = decodeJwt<AccessToken>(accessToken); | ||
return { | ||
sessionId, | ||
organizationId, | ||
role, | ||
permissions, | ||
}; | ||
} | ||
async function getSessionFromCookie(cookie: string, session?: SessionData) { | ||
if (!session) { | ||
session = await getSession(cookie); | ||
} | ||
if (session.has('jwt')) { | ||
@@ -162,2 +263,2 @@ return unsealData<Session>(session.get('jwt'), { | ||
export { encryptSession, withAuth, terminateSession }; | ||
export { encryptSession, terminateSession, authkitLoader }; |
import { WorkOS } from '@workos-inc/node'; | ||
import { WORKOS_API_HOSTNAME, WORKOS_API_HTTPS, WORKOS_API_KEY, WORKOS_API_PORT } from './env-variables.js'; | ||
const VERSION = '0.3.0'; | ||
const options = { | ||
@@ -10,3 +12,3 @@ apiHostname: WORKOS_API_HOSTNAME, | ||
name: 'authkit-remix', | ||
version: '0.2.0', | ||
version: VERSION, | ||
}, | ||
@@ -13,0 +15,0 @@ }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
57220
890
198
0
8