Skip to content

Commit

Permalink
feat(clerk-sdk-node): Deprecate Session named middleware, introduce w…
Browse files Browse the repository at this point in the history
…ithAuth, requireAuth

feat(edge): Rename withSession to withAuth

feat(backend-core): Expose JWTPayload type
  • Loading branch information
igneel64 committed Feb 9, 2022
1 parent 052ff1e commit 4e69553
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 59 deletions.
3 changes: 2 additions & 1 deletion packages/backend-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './api/ClerkBackendAPI';
export * from './api/resources';
export type { ClerkFetcher } from './api/utils/RestClient';
export type { Session } from './api/resources/Session';
export type { Nullable } from "./util/nullable"
export type { Nullable } from './util/nullable';
export type { JWTPayload } from './util/types';
17 changes: 9 additions & 8 deletions packages/edge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ yarn add @clerk/edge
Methods for supported platforms can be imported from the specific path:

```ts
import { withSession } from '@clerk/edge/vercel-edge';
import { withAuth } from '@clerk/edge/vercel-edge';

async function handler(req, event) {
// ...
}

export const middleware = withSession(handler);
export const middleware = withAuth(handler);
```

## Supported platforms
Expand All @@ -42,32 +42,33 @@ Currently supported environments/platforms:
To use with [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) :

```ts
import { withSession } from '@clerk/edge/vercel-edge';
import { withAuth } from '@clerk/edge/vercel-edge';

async function handler(req, event) {
// ...
}

export const middleware = withSession(handler);
export const middleware = withAuth(handler);
```

Supported methods:

- `withSession`
- `withAuth`
- `verifySessionToken`
- Resources API through `ClerkAPI`

### Validate the Authorized Party of a session token

Clerk's JWT session token, contains the azp claim, which equals the Origin of the request during token generation. You can provide the middlewares with a list of whitelisted origins to verify against, to protect your application of the subdomain cookie leaking attack. You can find an example below:

```ts
import { withSession } from '@clerk/edge/vercel-edge';
import { withAuth } from '@clerk/edge/vercel-edge';

const authorizedParties = ['http://localhost:3000', 'https://example.com']
const authorizedParties = ['http://localhost:3000', 'https://example.com'];

async function handler(req, event) {
// ...
}

export const middleware = withSession(handler, { authorizedParties });
export const middleware = withAuth(handler, { authorizedParties });
```
26 changes: 17 additions & 9 deletions packages/edge/src/vercel-edge.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { JWTPayload } from '@clerk/backend-core';
import {
AuthStatus,
Base,
Expand All @@ -11,7 +12,7 @@ import { LIB_NAME, LIB_VERSION } from './info';

type Middleware = (
req: NextRequest,
event: NextFetchEvent
event: NextFetchEvent,
) => Response | void | Promise<Response | void>;

/**
Expand All @@ -33,7 +34,7 @@ const verifySignature = async (
algorithm: Algorithm,
key: CryptoKey,
signature: Uint8Array,
data: Uint8Array
data: Uint8Array,
) => {
return await crypto.subtle.verify(algorithm, key, signature, data);
};
Expand Down Expand Up @@ -64,7 +65,7 @@ export const ClerkAPI = new ClerkBackendAPI({
'X-Clerk-SDK': `vercel-edge/${LIB_VERSION}`,
},
...(body && { body: JSON.stringify(body) }),
}).then((body) => body.json());
}).then(body => body.json());
},
});

Expand All @@ -75,16 +76,22 @@ async function fetchInterstitial() {

/** Export middleware wrapper */

export type NextRequestWithSession = NextRequest & { session: Session };
export type NextRequestWithAuth = NextRequest & {
session?: Session;
sessionClaims?: JWTPayload;
};

export type MiddlewareOptions = {
authorizedParties?: string[];
};

export function withSession(handler: Middleware, { authorizedParties }: MiddlewareOptions = { authorizedParties: []}) {
export function withAuth(
handler: Middleware,
{ authorizedParties }: MiddlewareOptions = { authorizedParties: [] },
) {
return async function clerkAuth(req: NextRequest, event: NextFetchEvent) {
const { status, session, interstitial } = await vercelEdgeBase.getAuthState(
{
const { status, session, interstitial, sessionClaims } =
await vercelEdgeBase.getAuthState({
cookieToken: req.cookies['__session'],
clientUat: req.cookies['__client_uat'],
headerToken: req.headers.get('authorization'),
Expand All @@ -95,8 +102,7 @@ export function withSession(handler: Middleware, { authorizedParties }: Middlewa
referrer: req.headers.get('referrer'),
authorizedParties: authorizedParties,
fetchInterstitial,
}
);
});

if (status === AuthStatus.SignedOut) {
return handler(req, event);
Expand All @@ -112,6 +118,8 @@ export function withSession(handler: Middleware, { authorizedParties }: Middlewa
if (status === AuthStatus.SignedIn) {
// @ts-ignore
req.session = session;
// @ts-ignore
req.sessionClaims = sessionClaims;
return handler(req, event);
}
};
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

/** DO NOT EDIT: This file is automatically generated by ../scripts/info.js */
export const LIB_VERSION = '2.11.0';
export const LIB_NAME = '@clerk/clerk-react';
export const LIB_VERSION='2.11.0';
export const LIB_NAME='@clerk/clerk-react';
52 changes: 25 additions & 27 deletions packages/sdk-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,36 +547,36 @@ The error handling is pretty generic at the moment but more fine-grained errors

## Express middleware

For usage with <a href="https://github.com/expressjs/express" target="_blank">Express</a>, this package also exports `ClerkExpressWithSession` (lax) & `ClerkExpressRequireSession` (strict)
For usage with <a href="https://github.com/expressjs/express" target="_blank">Express</a>, this package also exports `ClerkExpressWithAuth` (lax) & `ClerkExpressRequireAuth` (strict)
middlewares that can be used in the standard manner:

```ts
import { ClerkWithSession } from '@clerk/clerk-sdk-node';
import { ClerkWithAuth } from '@clerk/clerk-sdk-node';

// Initialize express app the usual way

app.use(ClerkWithSession());
app.use(ClerkWithAuth());
```

The `ClerkWithSession` middleware will set the Clerk session on the request object as `req.session` and then call the next middleware.
The `ClerkWithAuth` middleware will set the Clerk session on the request object as `req.session` and then call the next middleware.

You can then implement your own logic for handling a logged-in or logged-out user in your express endpoints or custom
middleware, depending on whether your users are trying to access a public or protected resource.

If you want to use the express middleware of your custom `Clerk` instance, you can use:

```ts
app.use(clerk.expressWithSession());
app.use(clerk.expressWithAuth());
```

Where `clerk` is your own instance.

If you prefer that the middleware renders a 401 (Unauthenticated) itself, you can use the following variant instead:

```ts
import { ClerkExpressRequireSession } from '@clerk/clerk-sdk-node';
import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node';

app.use(ClerkExpressRequireSession());
app.use(ClerkExpressRequireAuth());
```

### onError option
Expand Down Expand Up @@ -635,51 +635,48 @@ The current package also offers a way of making
your <a href="https://nextjs.org/docs/api-routes/api-middlewares" target="_blank">Next.js api middleware</a> aware of the Clerk Session.

You can define your handler function with the usual signature (`function handler(req, res) {}`) then wrap it
with `withSession`:
with `withAuth`:

```ts
import { withSession, WithSessionProp } from '@clerk/clerk-sdk-node';
import { withAuth, WithAuthProp } from '@clerk/clerk-sdk-node';
```

Note: Since the request will be extended with a session property, the signature of your handler in TypeScript would be:

```ts
function handler(req: WithSessionProp<NextApiRequest>, res: NextApiResponse) {
function handler(req: WithAuthProp<NextApiRequest>, res: NextApiResponse) {
if (req.session) {
// do something with session.userId
} else {
// Respond with 401 or similar
}
}

export withSession(handler);
export withAuth(handler);
```

You can also pass an `onError` handler to the underlying Express middleware that is called (see previous section):

```ts
export withSession(handler, { clerk, onError: error => console.log(error) });
export withAuth(handler, { clerk, onError: error => console.log(error) });
```

In case you would like the request to be rejected automatically when no session exists,
without having to implement such logic yourself, you can opt for the stricter variant:

```ts
import clerk, {
requireSession,
RequireSessionProp,
} from '@clerk/clerk-sdk-node';
import clerk, { requireAuth, RequireAuthProp } from '@clerk/clerk-sdk-node';
```

In this case your handler can be even simpler because the existence of the session can be assumed, otherwise the
execution will never reach your handler:

```ts
function handler(req: RequireSessionProp<NextApiRequest>, res: NextApiResponse) {
function handler(req: RequireAuthProp<NextApiRequest>, res: NextApiResponse) {
// do something with session.userId
}

export requireSession(handler, { clerk, onError });
export requireAuth(handler, { clerk, onError });
```

Note that by default the error returned will be the Clerk server error encountered (or in case of misconfiguration, the error raised by the SDK itself).
Expand Down Expand Up @@ -711,34 +708,35 @@ The aforementioned usage pertains to the singleton case. If you would like to us
yourself (e.g. named `clerk`), you can use the following syntax instead:

```ts
export clerk.withSession(handler);
export clerk.withAuth(handler);
// OR
export clerk.requireSession(handler);
export clerk.requireAuth(handler);
```

## Validate the Authorized Party of a session token

Clerk's JWT session token, contains the azp claim, which equals the Origin of the request during token generation. You can provide the middlewares with a list of whitelisted origins to verify against, to protect your application of the subdomain cookie leaking attack. You can find an example below:

### Express

```ts
import { ClerkExpressRequireSession } from '@clerk/clerk-sdk-node';
import { ClerkExpressRequireAuth } from '@clerk/clerk-sdk-node';

const authorizedParties = ['http://localhost:3000', 'https://example.com']
const authorizedParties = ['http://localhost:3000', 'https://example.com'];

app.use(ClerkExpressRequireSession({ authorizedParties }));
app.use(ClerkExpressRequireAuth({ authorizedParties }));
```

### Next

```ts
const authorizedParties = ['http://localhost:3000', 'https://example.com']

function handler(req: RequireSessionProp<NextApiRequest>, res: NextApiResponse) {
function handler(req: RequireAuthProp<NextApiRequest>, res: NextApiResponse) {
// do something with session.userId
}

export requireSession(handler, { authorizedParties });
export requireAuth(handler, { authorizedParties });
```

## Troubleshooting
Expand All @@ -753,10 +751,10 @@ Please consult the following check-list for some potential quick fixes:
- In development mode, do your frontend & API reside on the same domain? Unless the clerk `__session` is sent to your API server, the SDK will fail to authenticate your user.
- If you are still experiencing issues, it is advisable to set the `CLERK_LOGGING` environment variable to `true` to get additional logging output that may help identify the issue.

Note: The strict middleware variants (i.e. the "require session" variants) will produce an erroneous response if the user is not signed in.
Note: The strict middleware variants (i.e. the "require auth" variants) will produce an erroneous response if the user is not signed in.
Please ensure you are not mounting them on routes that are meant to be publicly accessible.

## Feedback / Issue reporting

Please report issues or open feature request in
the [github issue section](https://github.com/clerkinc/clerk-sdk-node/issues).
the [github issue section](https://github.com/clerkinc/javascript/issues).
15 changes: 8 additions & 7 deletions packages/sdk-node/examples/express/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
ClerkExpressRequireSession,
ClerkExpressWithSession,
WithSessionProp,
ClerkExpressRequireAuth,
ClerkExpressWithAuth,
RequireAuthProp,
WithAuthProp,
} from '@clerk/clerk-sdk-node';
import dotenv from 'dotenv';
import express, { Application, Request, Response } from 'express';
Expand All @@ -14,17 +15,17 @@ const app: Application = express();
// Root path uses lax middleware
app.get(
'/',
ClerkExpressWithSession(),
(req: WithSessionProp<Request>, res: Response) => {
ClerkExpressWithAuth(),
(req: WithAuthProp<Request>, res: Response) => {
res.json(req.session || 'No session detected');
}
);

// /require-session path uses strict middleware
app.get(
'/require-session',
ClerkExpressRequireSession(),
(req: WithSessionProp<Request>, res) => {
ClerkExpressRequireAuth(),
(req: RequireAuthProp<Request>, res) => {
res.json(req.session);
}
);
Expand Down
23 changes: 19 additions & 4 deletions packages/sdk-node/src/Clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
Base,
ClerkBackendAPI,
ClerkFetcher,
JWTPayload,
Session,
} from '@clerk/backend-core';
import Cookies from 'cookies';
import deepmerge from 'deepmerge';
import type { NextFunction, Request, Response } from 'express';
import got, { OptionsOfUnknownResponseBody } from 'got';
import jwt, { JwtPayload } from 'jsonwebtoken';
import jwt from 'jsonwebtoken';
import jwks, { JwksClient } from 'jwks-rsa';
import querystring from 'querystring';

Expand All @@ -31,10 +32,24 @@ export type MiddlewareOptions = {
authorizedParties?: string[];
};

/** @deprecated DEPRECATED Use WithAuthProp Est. 2.10.0 */
export type WithSessionProp<T> = T & { session?: Session };
/** @deprecated DEPRECATED Use RequireAuthProp Est. 2.10.0 */
export type RequireSessionProp<T> = T & { session: Session };
export type WithSessionClaimsProp<T> = T & { sessionClaims?: JwtPayload };
export type RequireSessionClaimsProp<T> = T & { sessionClaims: JwtPayload };
/** @deprecated DEPRECATED Use WithAuthProp Est. 2.10.0 */
export type WithSessionClaimsProp<T> = T & { sessionClaims?: JWTPayload };
/** @deprecated DEPRECATED Use RequireAuthProp Est. 2.10.0 */
export type RequireSessionClaimsProp<T> = T & { sessionClaims: JWTPayload };

export type WithAuthProp<T> = T & {
session?: Session;
sessionClaims?: JWTPayload;
};

export type RequireAuthProp<T> = T & {
session: Session;
sessionClaims: JWTPayload;
};

import { Crypto, CryptoKey } from '@peculiar/webcrypto';

Expand Down Expand Up @@ -159,7 +174,7 @@ export default class Clerk extends ClerkBackendAPI {
async verifyToken(
token: string,
authorizedParties?: string[]
): Promise<JwtPayload> {
): Promise<JWTPayload> {
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error(`Failed to verify token: ${token}`);
Expand Down

0 comments on commit 4e69553

Please sign in to comment.