Skip to content

Commit

Permalink
chore(*): Expose requestState.headers to response and populate debug …
Browse files Browse the repository at this point in the history
…headers only in backend (#2898)
  • Loading branch information
dimkl committed Mar 11, 2024
1 parent 430ebb9 commit 2964f8a
Show file tree
Hide file tree
Showing 14 changed files with 101 additions and 100 deletions.
9 changes: 9 additions & 0 deletions .changeset/lemon-starfishes-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@clerk/clerk-sdk-node': minor
'@clerk/backend': minor
'@clerk/fastify': minor
'@clerk/nextjs': minor
'@clerk/remix': minor
---

Expose debug headers in response for handshake / signed-out states from SDKs using headers returned from `authenticateRequest()`
40 changes: 40 additions & 0 deletions packages/backend/src/tokens/__tests__/authStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type QUnit from 'qunit';

import { handshake, signedIn, signedOut } from '../authStatus';

export default (QUnit: QUnit) => {
const { module, test } = QUnit;

module('signed-in', () => {
test('does not include debug headers', assert => {
const authObject = signedIn({} as any, {} as any, undefined, 'token');
assert.strictEqual(authObject.headers.get('x-clerk-auth-status'), null);
assert.strictEqual(authObject.headers.get('x-clerk-auth-reason'), null);
assert.strictEqual(authObject.headers.get('x-clerk-auth-message'), null);
});
});

module('signed-out', () => {
test('includes debug headers', assert => {
const headers = new Headers({ 'custom-header': 'value' });
const authObject = signedOut({} as any, 'auth-reason', 'auth-message', headers);

assert.strictEqual(authObject.headers.get('custom-header'), 'value');
assert.strictEqual(authObject.headers.get('x-clerk-auth-status'), 'signed-out');
assert.strictEqual(authObject.headers.get('x-clerk-auth-reason'), 'auth-reason');
assert.strictEqual(authObject.headers.get('x-clerk-auth-message'), 'auth-message');
});
});

module('handshake', () => {
test('includes debug headers', assert => {
const headers = new Headers({ location: '/' });
const authObject = handshake({} as any, 'auth-reason', 'auth-message', headers);

assert.strictEqual(authObject.headers.get('location'), '/');
assert.strictEqual(authObject.headers.get('x-clerk-auth-status'), 'handshake');
assert.strictEqual(authObject.headers.get('x-clerk-auth-reason'), 'auth-reason');
assert.strictEqual(authObject.headers.get('x-clerk-auth-message'), 'auth-message');
});
});
};
27 changes: 23 additions & 4 deletions packages/backend/src/tokens/authStatus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { JwtPayload } from '@clerk/types';

import { constants } from '../constants';
import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject, SignedOutAuthObject } from './authObjects';
Expand Down Expand Up @@ -105,7 +106,7 @@ export function signedOut(
message = '',
headers: Headers = new Headers(),
): SignedOutState {
return {
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason,
message,
Expand All @@ -121,7 +122,7 @@ export function signedOut(
headers,
toAuth: () => signedOutAuthObject({ ...authenticateContext, status: AuthStatus.SignedOut, reason, message }),
token: null,
};
});
}

export function handshake(
Expand All @@ -130,7 +131,7 @@ export function handshake(
message = '',
headers: Headers,
): HandshakeState {
return {
return withDebugHeaders({
status: AuthStatus.Handshake,
reason,
message,
Expand All @@ -146,5 +147,23 @@ export function handshake(
headers,
toAuth: () => null,
token: null,
};
});
}

const withDebugHeaders = <T extends RequestState>(requestState: T): T => {
const headers = new Headers(requestState.headers || {});

if (requestState.message) {
headers.set(constants.Headers.AuthMessage, requestState.message);
}
if (requestState.reason) {
headers.set(constants.Headers.AuthReason, requestState.reason);
}
if (requestState.status) {
headers.set(constants.Headers.AuthStatus, requestState.status);
}

requestState.headers = headers;

return requestState;
};
2 changes: 2 additions & 0 deletions packages/backend/tests/suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import cryptoKeysTest from './dist/jwt/__tests__/cryptoKeys.test.js';
import signJwtTest from './dist/jwt/__tests__/signJwt.test.js';
import verifyJwtTest from './dist/jwt/__tests__/verifyJwt.test.js';
import authObjectsTest from './dist/tokens/__tests__/authObjects.test.js';
import authStatusTest from './dist/tokens/__tests__/authStatus.test.js';
import clerkRequestTest from './dist/tokens/__tests__/clerkRequest.test.js';
import tokenFactoryTest from './dist/tokens/__tests__/factory.test.js';
import keysTest from './dist/tokens/__tests__/keys.test.js';
Expand All @@ -19,6 +20,7 @@ import pathTest from './dist/util/__tests__/path.test.js';
// Add them to the suite array
const suites = [
authObjectsTest,
authStatusTest,
cryptoKeysTest,
exportsTest,
factoryTest,
Expand Down
7 changes: 6 additions & 1 deletion packages/fastify/src/withClerkMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ describe('withClerkMiddleware(options)', () => {
status: 'handshake',
reason: 'auth-reason',
message: 'auth-message',
headers: new Headers({ location: 'https://fapi.example.com/v1/clients/handshake' }),
headers: new Headers({
location: 'https://fapi.example.com/v1/clients/handshake',
'x-clerk-auth-message': 'auth-message',
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-status': 'handshake',
}),
toAuth: () => 'mockedAuth',
});
const fastify = Fastify();
Expand Down
11 changes: 2 additions & 9 deletions packages/fastify/src/withClerkMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { RequestState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';
import type { FastifyReply, FastifyRequest } from 'fastify';

Expand All @@ -7,13 +6,6 @@ import * as constants from './constants';
import type { ClerkFastifyOptions } from './types';
import { fastifyRequestToRequest } from './utils';

const decorateResponseWithObservabilityHeaders = (reply: FastifyReply, requestState: RequestState): FastifyReply => {
return reply
.header(constants.Headers.AuthStatus, requestState.status)
.header(constants.Headers.AuthReason, requestState.reason)
.header(constants.Headers.AuthMessage, requestState.message);
};

export const withClerkMiddleware = (options: ClerkFastifyOptions) => {
return async (fastifyRequest: FastifyRequest, reply: FastifyReply) => {
const req = fastifyRequestToRequest(fastifyRequest);
Expand All @@ -23,11 +15,12 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => {
secretKey: options.secretKey || constants.SECRET_KEY,
publishableKey: options.publishableKey || constants.PUBLISHABLE_KEY,
});

requestState.headers.forEach((value, key) => reply.header(key, value));

const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
return decorateResponseWithObservabilityHeaders(reply, requestState).code(307).send();
return reply.code(307).send();
} else if (requestState.status === AuthStatus.Handshake) {
throw new Error('Clerk: handshake status without redirect');
}
Expand Down
12 changes: 2 additions & 10 deletions packages/nextjs/src/server/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ import { redirectToSignIn } from './redirectHelpers';
import type { RouteMatcherParam } from './routeMatcher';
import { createRouteMatcher } from './routeMatcher';
import type { NextMiddlewareReturn } from './types';
import {
apiEndpointUnauthorizedNextResponse,
decorateRequest,
decorateResponseWithObservabilityHeaders,
setRequestHeadersOnNextResponse,
} from './utils';
import { apiEndpointUnauthorizedNextResponse, decorateRequest, setRequestHeadersOnNextResponse } from './utils';

/**
* The default ideal matcher that excludes the _next directory (internals) and all static files,
Expand Down Expand Up @@ -184,10 +179,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
const locationHeader = requestState.headers.get('location');
if (locationHeader) {
// triggering a handshake redirect
return decorateResponseWithObservabilityHeaders(
new Response(null, { status: 307, headers: requestState.headers }),
requestState,
);
return new Response(null, { status: 307, headers: requestState.headers });
}

if (requestState.status === AuthStatus.Handshake) {
Expand Down
10 changes: 2 additions & 8 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ import { PUBLISHABLE_KEY, SECRET_KEY } from './constants';
import type { AuthProtect } from './protect';
import { createProtect } from './protect';
import type { NextMiddlewareEvtParam, NextMiddlewareRequestParam, NextMiddlewareReturn } from './types';
import {
decorateRequest,
decorateResponseWithObservabilityHeaders,
handleMultiDomainAndProxy,
setRequestHeadersOnNextResponse,
} from './utils';
import { decorateRequest, handleMultiDomainAndProxy, setRequestHeadersOnNextResponse } from './utils';

const CONTROL_FLOW_ERROR = {
FORCE_NOT_FOUND: 'CLERK_PROTECT_REWRITE',
Expand Down Expand Up @@ -79,8 +74,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {

const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
const res = new Response(null, { status: 307, headers: requestState.headers });
return decorateResponseWithObservabilityHeaders(res, requestState);
return new Response(null, { status: 307, headers: requestState.headers });
} else if (requestState.status === AuthStatus.Handshake) {
throw new Error('Clerk: handshake status without redirect');
}
Expand Down
7 changes: 0 additions & 7 deletions packages/nextjs/src/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,3 @@ export const handleMultiDomainAndProxy = (clerkRequest: ClerkRequest, opts: Auth
signInUrl,
};
};

export const decorateResponseWithObservabilityHeaders = (res: Response, requestState: RequestState): Response => {
requestState.message && res.headers.set(constants.Headers.AuthMessage, encodeURIComponent(requestState.message));
requestState.reason && res.headers.set(constants.Headers.AuthReason, encodeURIComponent(requestState.reason));
requestState.status && res.headers.set(constants.Headers.AuthStatus, encodeURIComponent(requestState.status));
return res;
};
1 change: 0 additions & 1 deletion packages/remix/src/ssr/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './rootAuthLoader';
export * from './getAuth';
export { getClerkDebugHeaders } from './utils';

/**
* Re-export resource types from @clerk/backend
Expand Down
32 changes: 1 addition & 31 deletions packages/remix/src/ssr/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,6 @@ export function assertValidHandlerResult(val: any, error?: string): asserts val
}
}

const observabilityHeadersFromRequestState = (requestState: RequestState): Headers => {
const headers = {} as Record<string, string>;

if (requestState.message) {
headers[constants.Headers.AuthMessage] = requestState.message;
}
if (requestState.reason) {
headers[constants.Headers.AuthReason] = requestState.reason;
}
if (requestState.status) {
headers[constants.Headers.AuthStatus] = requestState.status;
}

return new Headers(headers);
};

/**
* Retrieve Clerk auth headers. Should be used only for debugging and not in production.
* @internal
*/
export const getClerkDebugHeaders = (headers: Headers) => {
return {
[constants.Headers.AuthMessage]: headers.get(constants.Headers.AuthMessage),
[constants.Headers.AuthReason]: headers.get(constants.Headers.AuthReason),
[constants.Headers.AuthStatus]: headers.get(constants.Headers.AuthStatus),
};
};

export const injectRequestStateIntoResponse = async (
response: Response,
requestState: RequestState,
Expand Down Expand Up @@ -125,11 +97,9 @@ export function getResponseClerkState(requestState: RequestState, context: AppLo
__telemetryDebug: isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG', context)),
});

const headers = observabilityHeadersFromRequestState(requestState);

return {
clerkState,
headers,
headers: requestState.headers,
};
}

Expand Down
25 changes: 10 additions & 15 deletions packages/sdk-node/src/authenticateRequest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { RequestState } from '@clerk/backend/internal';
import { AuthStatus, constants, createClerkRequest } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative, isValidProxyUrl } from '@clerk/shared/proxy';
import type { Response } from 'express';
import type { IncomingMessage, ServerResponse } from 'http';
import type { IncomingMessage } from 'http';

import type { AuthenticateRequestParams } from './types';
import { loadApiEnv, loadClientEnv } from './utils';
Expand Down Expand Up @@ -57,21 +57,23 @@ const incomingMessageToRequest = (req: IncomingMessage): Request => {
});
};

export const setResponseHeaders = (requestState: RequestState, res: Response): Error | undefined => {
if (requestState.headers) {
requestState.headers.forEach((value, key) => res.appendHeader(key, value));
}
return setResponseForHandshake(requestState, res);
};

/**
* Depending on the auth state of the request, handles applying redirects and validating that a handshake state was properly handled.
*
* Returns an error if state is handshake without a redirect, otherwise returns undefined. res.writableEnded should be checked after this method is called.
*/
export const setResponseForHandshake = (requestState: RequestState, res: Response) => {
const setResponseForHandshake = (requestState: RequestState, res: Response): Error | undefined => {
const hasLocationHeader = requestState.headers.get('location');
if (hasLocationHeader) {
requestState.headers.forEach((value, key) => {
res.appendHeader(key, value);
});

// triggering a handshake redirect
res.status(307).end();

return;
}

Expand All @@ -82,13 +84,6 @@ export const setResponseForHandshake = (requestState: RequestState, res: Respons
return;
};

// TODO: Move to backend
export const decorateResponseWithObservabilityHeaders = (res: ServerResponse, requestState: RequestState) => {
requestState.message && res.setHeader(constants.Headers.AuthMessage, encodeURIComponent(requestState.message));
requestState.reason && res.setHeader(constants.Headers.AuthReason, encodeURIComponent(requestState.reason));
requestState.status && res.setHeader(constants.Headers.AuthStatus, encodeURIComponent(requestState.status));
};

const absoluteProxyUrl = (relativeOrAbsoluteUrl: string, baseUrl: string): string => {
if (!relativeOrAbsoluteUrl || !isValidProxyUrl(relativeOrAbsoluteUrl) || !isProxyUrlRelative(relativeOrAbsoluteUrl)) {
return relativeOrAbsoluteUrl;
Expand Down
9 changes: 2 additions & 7 deletions packages/sdk-node/src/clerkExpressRequireAuth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { createClerkClient } from '@clerk/backend';

import {
authenticateRequest,
decorateResponseWithObservabilityHeaders,
setResponseForHandshake,
} from './authenticateRequest';
import { authenticateRequest, setResponseHeaders } from './authenticateRequest';
import type { ClerkMiddlewareOptions, MiddlewareRequireAuthProp, RequireAuthProp } from './types';

export type CreateClerkExpressMiddlewareOptions = {
Expand All @@ -26,9 +22,8 @@ export const createClerkExpressRequireAuth = (createOpts: CreateClerkExpressMidd
req,
options,
});
decorateResponseWithObservabilityHeaders(res, requestState);

const err = setResponseForHandshake(requestState, res);
const err = setResponseHeaders(requestState, res);
if (err || res.writableEnded) {
if (err) {
next(err);
Expand Down
9 changes: 2 additions & 7 deletions packages/sdk-node/src/clerkExpressWithAuth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
authenticateRequest,
decorateResponseWithObservabilityHeaders,
setResponseForHandshake,
} from './authenticateRequest';
import { authenticateRequest, setResponseHeaders } from './authenticateRequest';
import type { CreateClerkExpressMiddlewareOptions } from './clerkExpressRequireAuth';
import type { ClerkMiddlewareOptions, MiddlewareWithAuthProp, WithAuthProp } from './types';

Expand All @@ -17,9 +13,8 @@ export const createClerkExpressWithAuth = (createOpts: CreateClerkExpressMiddlew
req,
options,
});
decorateResponseWithObservabilityHeaders(res, requestState);

const err = setResponseForHandshake(requestState, res);
const err = setResponseHeaders(requestState, res);
if (err || res.writableEnded) {
if (err) {
next(err);
Expand Down

0 comments on commit 2964f8a

Please sign in to comment.