Skip to content

Commit

Permalink
Merge branch 'main' into brk.feat/remix-handshake
Browse files Browse the repository at this point in the history
  • Loading branch information
BRKalow committed Dec 15, 2023
2 parents 6325c0e + 3b9e801 commit 6e4aa1b
Show file tree
Hide file tree
Showing 51 changed files with 682 additions and 647 deletions.
14 changes: 14 additions & 0 deletions .changeset/dull-ants-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@clerk/backend': major
---

Change return value of `verifyToken()` from `@clerk/backend` to `{ data, error}`.
To replicate the current behaviour use this:
```typescript
import { verifyToken } from '@clerk/backend'

const { data, error } = await verifyToken(...);
if(error){
throw error;
}
```
7 changes: 7 additions & 0 deletions .changeset/famous-penguins-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/types': major
---

- Remove `BuildUrlWithAuthParams` type
- `AuthConfigResource` no longer has a `urlBasedSessionSyncing` property
- `buildUrlWithAuth` no longer accepts an `options` argument of `BuildUrlWithAuthParams`.
2 changes: 2 additions & 0 deletions .changeset/honest-pigs-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/mighty-rice-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/types': minor
---

Introduce new `ResultWithError` type in `@clerk/types`
10 changes: 10 additions & 0 deletions .changeset/modern-buses-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': major
'@clerk/nextjs': major
'@clerk/shared': major
'@clerk/clerk-react': major
'@clerk/types': major
---

Remove hashing and third-party cookie functionality related to development instance session syncing in favor of URL-based session syncing with query parameters.
6 changes: 6 additions & 0 deletions .changeset/modern-mayflies-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': major
'@clerk/clerk-react': major
---

- `buildUrlWithAuth` no longer accepts an `options` argument.
2 changes: 2 additions & 0 deletions .changeset/odd-eels-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
23 changes: 23 additions & 0 deletions .changeset/proud-trees-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@clerk/backend': major
'@clerk/nextjs': major
'@clerk/types': major
---

Change return values of `signJwt`, `hasValidSignature`, `decodeJwt`, `verifyJwt`
to return `{ data, error }`. Example of keeping the same behavior using those utilities:
```typescript
import { signJwt, hasValidSignature, decodeJwt, verifyJwt } from '@clerk/backend/jwt';

const { data, error } = await signJwt(...)
if (error) throw error;

const { data, error } = await hasValidSignature(...)
if (error) throw error;

const { data, error } = decodeJwt(...)
if (error) throw error;

const { data, error } = await verifyJwt(...)
if (error) throw error;
```
25 changes: 12 additions & 13 deletions packages/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Verifies a Clerk generated JWT (i.e. Clerk Session JWT and Clerk JWT templates).
```js
import { verifyToken } from '@clerk/backend';

verifyToken(token, {
const { result, error } = await verifyToken(token, {
issuer: '...',
authorizedParties: '...',
});
Expand All @@ -114,11 +114,10 @@ verifyToken(token, {
Verifies a Clerk generated JWT (i.e. Clerk Session JWT and Clerk JWT templates). The key needs to be provided in the options.

```js
import { verifyJwt } from '@clerk/backend';
import { verifyJwt } from '@clerk/backend/jwt';

verifyJwt(token, {
const { result, error } = verifyJwt(token, {
key: JsonWebKey | string,
issuer: '...',
authorizedParties: '...',
});
```
Expand All @@ -128,27 +127,27 @@ verifyJwt(token, {
Decodes a JWT.

```js
import { decodeJwt } from '@clerk/backend';
import { decodeJwt } from '@clerk/backend/jwt';

decodeJwt(token);
const { result, error } = decodeJwt(token);
```

#### hasValidSignature(jwt: Jwt, key: JsonWebKey | string)

Verifies that the JWT has a valid signature. The key needs to be provided.

```js
import { hasValidSignature } from '@clerk/backend';
import { hasValidSignature } from '@clerk/backend/jwt';

hasValidSignature(token, jwk);
const { result, error } = await hasValidSignature(token, jwk);
```

#### debugRequestState(requestState)

Generates a debug payload for the request state

```js
import { debugRequestState } from '@clerk/backend';
import { debugRequestState } from '@clerk/backend/internal';

debugRequestState(requestState);
```
Expand All @@ -158,7 +157,7 @@ debugRequestState(requestState);
Builds the AuthObject when the user is signed in.

```js
import { signedInAuthObject } from '@clerk/backend';
import { signedInAuthObject } from '@clerk/backend/internal';

signedInAuthObject(jwtPayload, options);
```
Expand All @@ -168,7 +167,7 @@ signedInAuthObject(jwtPayload, options);
Builds the empty AuthObject when the user is signed out.

```js
import { signedOutAuthObject } from '@clerk/backend';
import { signedOutAuthObject } from '@clerk/backend/internal';

signedOutAuthObject();
```
Expand All @@ -178,7 +177,7 @@ signedOutAuthObject();
Removes sensitive private metadata from user and organization resources in the AuthObject

```js
import { sanitizeAuthObject } from '@clerk/backend';
import { sanitizeAuthObject } from '@clerk/backend/internal';

sanitizeAuthObject(authObject);
```
Expand All @@ -188,7 +187,7 @@ sanitizeAuthObject(authObject);
Removes any `private_metadata` and `privateMetadata` attributes from the object to avoid leaking sensitive information to the browser during SSR.

```js
import { prunePrivateMetadata } from '@clerk/backend';
import { prunePrivateMetadata } from '@clerk/backend/internal';

prunePrivateMetadata(obj);
```
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default (QUnit: QUnit) => {
module('subpath /errors exports', () => {
test('should not include a breaking change', assert => {
const exportedApiKeys = [
'SignJWTError',
'TokenVerificationError',
'TokenVerificationErrorAction',
'TokenVerificationErrorCode',
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ export class TokenVerificationError extends Error {
})`;
}
}

export class SignJWTError extends Error {}
11 changes: 7 additions & 4 deletions packages/backend/src/jwt/__tests__/signJwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
publicJwks,
signingJwks,
} from '../../fixtures';
import { assertOk } from '../../util/testUtils';
import { signJwt } from '../signJwt';
import { verifyJwt } from '../verifyJwt';

Expand All @@ -26,22 +27,24 @@ export default (QUnit: QUnit) => {
});

test('signs a JWT with a JWK formatted secret', async assert => {
const jwt = await signJwt(payload, signingJwks, {
const { data } = await signJwt(payload, signingJwks, {
algorithm: mockJwtHeader.alg,
header: mockJwtHeader,
});
assertOk(assert, data);

const verifiedPayload = await verifyJwt(jwt, { key: publicJwks });
const { data: verifiedPayload } = await verifyJwt(data, { key: publicJwks });
assert.deepEqual(verifiedPayload, payload);
});

test('signs a JWT with a pkcs8 formatted secret', async assert => {
const jwt = await signJwt(payload, pemEncodedSignKey, {
const { data } = await signJwt(payload, pemEncodedSignKey, {
algorithm: mockJwtHeader.alg,
header: mockJwtHeader,
});
assertOk(assert, data);

const verifiedPayload = await verifyJwt(jwt, { key: pemEncodedPublicKey });
const { data: verifiedPayload } = await verifyJwt(data, { key: pemEncodedPublicKey });
assert.deepEqual(verifiedPayload, payload);
});
});
Expand Down
91 changes: 52 additions & 39 deletions packages/backend/src/jwt/__tests__/verifyJwt.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Jwt } from '@clerk/types';
import type QUnit from 'qunit';
import sinon from 'sinon';

Expand All @@ -11,66 +12,72 @@ import {
signedJwt,
someOtherPublicKey,
} from '../../fixtures';
import { assertOk } from '../../util/testUtils';
import { decodeJwt, hasValidSignature, verifyJwt } from '../verifyJwt';

export default (QUnit: QUnit) => {
const { module, test } = QUnit;
const invalidTokenError = {
reason: 'token-invalid',
message: 'Invalid JWT form. A JWT consists of three parts separated by dots.',
};

module('hasValidSignature(jwt, key)', () => {
test('verifies the signature with a JWK formatted key', async assert => {
assert.true(await hasValidSignature(decodeJwt(signedJwt), publicJwks));
const { data: decodedResult } = decodeJwt(signedJwt);
assertOk<Jwt>(assert, decodedResult);
const { data: signatureResult } = await hasValidSignature(decodedResult, publicJwks);
assert.true(signatureResult);
});

test('verifies the signature with a PEM formatted key', async assert => {
assert.true(await hasValidSignature(decodeJwt(signedJwt), pemEncodedPublicKey));
const { data: decodedResult } = decodeJwt(signedJwt);
assertOk<Jwt>(assert, decodedResult);
const { data: signatureResult } = await hasValidSignature(decodedResult, pemEncodedPublicKey);
assert.true(signatureResult);
});

test('it returns false if the key is not correct', async assert => {
assert.false(await hasValidSignature(decodeJwt(signedJwt), someOtherPublicKey));
const { data: decodedResult } = decodeJwt(signedJwt);
assertOk<Jwt>(assert, decodedResult);
const { data: signatureResult } = await hasValidSignature(decodedResult, someOtherPublicKey);
assert.false(signatureResult);
});
});

module('decodeJwt(jwt)', () => {
test('decodes a valid JWT', assert => {
const { header, payload } = decodeJwt(mockJwt);
assert.propEqual(header, mockJwtHeader);
assert.propEqual(payload, mockJwtPayload);
// TODO: @dimkl assert signature is instance of Uint8Array
const { data } = decodeJwt(mockJwt);
assertOk<Jwt>(assert, data);

assert.propEqual(data.header, mockJwtHeader);
assert.propEqual(data.payload, mockJwtPayload);
// TODO(@dimkl): assert signature is instance of Uint8Array
});

test('throws an error if null is given as jwt', assert => {
assert.throws(
() => decodeJwt('null'),
new Error('Invalid JWT form. A JWT consists of three parts separated by dots.'),
);
test('returns an error if null is given as jwt', assert => {
const { error } = decodeJwt('null');
assert.propContains(error, invalidTokenError);
});

test('throws an error if undefined is given as jwt', assert => {
assert.throws(
() => decodeJwt('undefined'),
new Error('Invalid JWT form. A JWT consists of three parts separated by dots.'),
);
test('returns an error if undefined is given as jwt', assert => {
const { error } = decodeJwt('undefined');
assert.propContains(error, invalidTokenError);
});

test('throws an error if empty string is given as jwt', assert => {
assert.throws(
() => decodeJwt('undefined'),
new Error('Invalid JWT form. A JWT consists of three parts separated by dots.'),
);
test('returns an error if empty string is given as jwt', assert => {
const { error } = decodeJwt('');
assert.propContains(error, invalidTokenError);
});

test('throws an error if invalid string is given as jwt', assert => {
assert.throws(
() => decodeJwt('undefined'),
new Error('Invalid JWT form. A JWT consists of three parts separated by dots.'),
);
const { error } = decodeJwt('whatever');
assert.propContains(error, invalidTokenError);
});

test('throws an error if number is given as jwt', assert => {
assert.throws(
() => decodeJwt('42'),
new Error('Invalid JWT form. A JWT consists of three parts separated by dots.'),
);
const { error } = decodeJwt('42');
assert.propContains(error, invalidTokenError);
});
});

Expand All @@ -90,8 +97,8 @@ export default (QUnit: QUnit) => {
issuer: mockJwtPayload.iss,
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
};
const payload = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(payload, mockJwtPayload);
const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(data, mockJwtPayload);
});

test('returns the valid JWT payload if valid key & issuer method & azp is given', async assert => {
Expand All @@ -100,8 +107,8 @@ export default (QUnit: QUnit) => {
issuer: (iss: string) => iss.startsWith('https://clerk'),
authorizedParties: ['https://accounts.inspired.puma-74.lcl.dev'],
};
const payload = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(payload, mockJwtPayload);
const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(data, mockJwtPayload);
});

test('returns the valid JWT payload if valid key & issuer & list of azp (with empty string) is given', async assert => {
Expand All @@ -110,12 +117,18 @@ export default (QUnit: QUnit) => {
issuer: mockJwtPayload.iss,
authorizedParties: ['', 'https://accounts.inspired.puma-74.lcl.dev'],
};
const payload = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(payload, mockJwtPayload);
const { data } = await verifyJwt(mockJwt, inputVerifyJwtOptions);
assert.propEqual(data, mockJwtPayload);
});

// todo('returns the reason of the failure when verifications fail', assert => {
// assert.true(true);
// });
test('returns the reason of the failure when verifications fail', async assert => {
const inputVerifyJwtOptions = {
key: mockJwks.keys[0],
issuer: mockJwtPayload.iss,
authorizedParties: ['', 'https://accounts.inspired.puma-74.lcl.dev'],
};
const { error } = await verifyJwt('invalid-jwt', inputVerifyJwtOptions);
assert.propContains(error, invalidTokenError);
});
});
};
Loading

0 comments on commit 6e4aa1b

Please sign in to comment.