Skip to content

feat(remix): Add Auth-Result response header #174

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

Merged
merged 2 commits into from
Apr 14, 2022
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
4 changes: 3 additions & 1 deletion packages/remix/src/client/ClerkCatchBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { Interstitial } from './Interstitial';
export function ClerkCatchBoundary(RootCatchBoundary?: () => JSX.Element) {
return () => {
const { data } = useCatch();
const { __clerk_ssr_interstitial, __frontendApi } = data?.clerkState?.__internal_clerk_state || {};
const { __clerk_ssr_interstitial, __frontendApi, __lastAuthResult } =
data?.clerkState?.__internal_clerk_state || {};

if (__clerk_ssr_interstitial) {
return (
<Interstitial
frontendApi={__frontendApi}
version={LIB_VERSION}
debugData={{ __lastAuthResult }}
/>
);
}
Expand Down
11 changes: 8 additions & 3 deletions packages/remix/src/client/Interstitial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ const getScriptUrl = (frontendApi: string, libVersion: string) => {
return `https://${frontendApi}/npm/@clerk/clerk-js@${major}/dist/clerk.browser.js`;
};

const createInterstitialHTMLString = (frontendApi: string, libVersion: string) => {
const createInterstitialHTMLString = (frontendApi: string, libVersion: string, debugData: any) => {
return `
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
window.__clerk_debug = ${JSON.stringify(debugData || {})};
window.startClerk = async () => {
const Clerk = window.Clerk;
try {
Expand All @@ -42,6 +43,10 @@ const createInterstitialHTMLString = (frontendApi: string, libVersion: string) =
`;
};

export function Interstitial({ frontendApi, version }: { frontendApi: string; version: string }) {
return <html dangerouslySetInnerHTML={{ __html: createInterstitialHTMLString(frontendApi, version) }} />;
export function Interstitial(opts: { frontendApi: string; version: string; debugData: any }) {
return (
<html
dangerouslySetInnerHTML={{ __html: createInterstitialHTMLString(opts.frontendApi, opts.version, opts.debugData) }}
/>
);
}
10 changes: 7 additions & 3 deletions packages/remix/src/client/RemixClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { IsomorphicClerkOptions } from '@clerk/clerk-react/dist/types';
import React from 'react';

import { assertFrontendApi, assertValidClerkState, warnForSsr } from '../utils';
import { ClerkState } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';

export * from '@clerk/clerk-react';

export type RemixClerkProviderProps<ClerkStateT extends { __type: 'clerkState' } = any> = {
export type RemixClerkProviderProps = {
children: React.ReactNode;
clerkState: ClerkStateT;
clerkState: ClerkState;
} & IsomorphicClerkOptions;

export function ClerkProvider({ children, ...rest }: RemixClerkProviderProps): JSX.Element {
Expand All @@ -18,12 +19,15 @@ export function ClerkProvider({ children, ...rest }: RemixClerkProviderProps): J
ReactClerkProvider.displayName = 'ReactClerkProvider';

assertValidClerkState(clerkState);
const { __clerk_ssr_state, __frontendApi, __lastAuthResult } = clerkState?.__internal_clerk_state || {};

React.useEffect(() => {
warnForSsr(clerkState);
}, []);

const { __clerk_ssr_state, __frontendApi } = clerkState?.__internal_clerk_state || {};
React.useEffect(() => {
(window as any).__clerk_debug = { __lastAuthResult };
}, []);

assertFrontendApi(__frontendApi);

Expand Down
1 change: 1 addition & 0 deletions packages/remix/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type ClerkState = {
__clerk_ssr_interstitial: string;
__clerk_ssr_state: InitialState;
__frontendApi: string;
__lastAuthResult: string;
};
};

Expand Down
8 changes: 4 additions & 4 deletions packages/remix/src/ssr/getAuthData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ export type AuthData = {
export async function getAuthData(
req: Request,
opts: RootAuthLoaderOptions = {},
): Promise<{ authData: AuthData | null; showInterstitial?: boolean }> {
): Promise<{ authData: AuthData | null; showInterstitial?: boolean; errorReason?: string }> {
const { loadSession, loadUser, jwtKey, authorizedParties } = opts;
const { headers } = req;
const cookies = parseCookies(req);

try {
const cookieToken = cookies['__session'];
const headerToken = headers.get('authorization')?.replace('Bearer ', '');
const { status, sessionClaims } = await Clerk.base.getAuthState({
const { status, sessionClaims, errorReason } = await Clerk.base.getAuthState({
cookieToken,
headerToken,
clientUat: cookies['__client_uat'],
Expand All @@ -43,11 +43,11 @@ export async function getAuthData(
});

if (status === AuthStatus.Interstitial) {
return { authData: null, showInterstitial: true };
return { authData: null, showInterstitial: true, errorReason };
}

if (status === AuthStatus.SignedOut || !sessionClaims) {
return { authData: createSignedOutState() };
return { authData: createSignedOutState(), errorReason };
}

const sessionId = sessionClaims.sid;
Expand Down
25 changes: 14 additions & 11 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { json } from '@remix-run/server-runtime';

import { invalidRootLoaderCallbackResponseReturn, invalidRootLoaderCallbackReturn } from '../errors';
import { assertFrontendApi } from '../utils';
import { getAuthData } from './getAuthData';
import { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import { assertObject, injectAuthIntoRequest, isRedirect, isResponse, sanitizeAuthData, wrapClerkState } from './utils';
import {
assertObject,
injectAuthIntoRequest,
isRedirect,
isResponse,
returnLoaderResultJsonResponse,
sanitizeAuthData,
throwInterstitialJsonResponse,
} from './utils';

interface RootAuthLoader {
<Options extends RootAuthLoaderOptions>(
Expand All @@ -30,20 +36,17 @@ export const rootAuthLoader: RootAuthLoader = async (
const frontendApi = process.env.CLERK_FRONTEND_API || opts.frontendApi;
assertFrontendApi(frontendApi);

const { authData, showInterstitial } = await getAuthData(args.request, opts);
const { authData, showInterstitial, errorReason } = await getAuthData(args.request, opts);

if (showInterstitial) {
throw json(
wrapClerkState({ __clerk_ssr_interstitial: showInterstitial, __frontendApi: frontendApi }),
{ status: 401 }
);
throw throwInterstitialJsonResponse({ frontendApi, errorReason });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Is the throw needed here since throwInterstitialJsonResponse throws on its own ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's not needed per se, but I used it to further emphasize that we need to throw in order for the catch boundary to work - otherwise the SSR flow will break. It also helps TS infer that the execution stops there if the if is true, without using return

I was worried that wrapping the throwInterstitialJsonResponse into another function that uses a try/catch block during a future refactor could potentially break the flow.

Afaik immediately throwing a thrown error works as expected - but let me know if you have any objections

}

if (!callback) {
return { ...wrapClerkState({ __clerk_ssr_state: authData, __frontendApi: frontendApi }) };
return returnLoaderResultJsonResponse({ authData, frontendApi, errorReason });
}

const callbackResult = await callback?.(injectAuthIntoRequest(args, sanitizeAuthData(authData!)));
const callbackResult = await callback(injectAuthIntoRequest(args, sanitizeAuthData(authData!)));
assertObject(callbackResult, invalidRootLoaderCallbackReturn);

// Pass through custom responses
Expand All @@ -54,5 +57,5 @@ export const rootAuthLoader: RootAuthLoader = async (
throw new Error(invalidRootLoaderCallbackResponseReturn);
}

return { ...callbackResult, ...wrapClerkState({ __clerk_ssr_state: authData, __frontendApi: frontendApi }) };
return returnLoaderResultJsonResponse({ authData, frontendApi, errorReason, callbackResult });
};
54 changes: 44 additions & 10 deletions packages/remix/src/ssr/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { json } from '@remix-run/server-runtime';
import cookie from 'cookie';

import { AuthData } from './getAuthData';
import { LoaderFunctionArgs, LoaderFunctionArgsWithAuth } from './types';

/**
* Wraps obscured clerk internals with a readable `clerkState` key.
* This is intended to be passed by the user into <ClerkProvider>
*
* @internal
*/
export const wrapClerkState = (data: any) => {
return { clerkState: { __internal_clerk_state: { ...data } } };
};

/**
* Inject `auth`, `user` and `session` properties into `request`
* @internal
Expand Down Expand Up @@ -75,3 +66,46 @@ export function assertObject(val: any, error?: string): asserts val is Record<st
throw new Error(error || '');
}
}

/**
* @internal
*/
export const throwInterstitialJsonResponse = (opts: { frontendApi: string; errorReason: string | undefined }) => {
throw json(
wrapWithClerkState({
__clerk_ssr_interstitial: true,
__frontendApi: opts.frontendApi,
__lastAuthResult: opts.errorReason,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


What do you think about changing it to __errorReason to reduce the cognitive load on going through the flow ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point! I think we should go with something like __lastAuthResult or similar to be consistent with what we do in NextJS

}),
{ status: 401 },
);
};

/**
* @internal
*/
export const returnLoaderResultJsonResponse = (opts: {
authData: AuthData | null;
frontendApi: string;
errorReason: string | undefined;
callbackResult?: any;
}) => {
return json({
...(opts.callbackResult || {}),
...wrapWithClerkState({
__clerk_ssr_state: opts.authData,
__frontendApi: opts.frontendApi,
__lastAuthResult: opts.errorReason || '',
}),
});
};

/**
* Wraps obscured clerk internals with a readable `clerkState` key.
* This is intended to be passed by the user into <ClerkProvider>
*
* @internal
*/
export const wrapWithClerkState = (data: any) => {
return { clerkState: { __internal_clerk_state: { ...data } } };
};