Skip to content
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

JWT condition #604

Draft
wants to merge 6 commits into
base: epic-v0.6.x
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion examples/pre/nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
2 changes: 1 addition & 1 deletion examples/taco/nextjs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
1 change: 1 addition & 0 deletions packages/taco/src/conditions/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
export * as contract from './contract';
export * as jsonApi from './json-api';
export * as jsonRpc from './json-rpc';
export * as jwt from './jwt';
export * as rpc from './rpc';
export * as time from './time';
23 changes: 23 additions & 0 deletions packages/taco/src/conditions/base/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Condition } from '../condition';
import {
JWTConditionProps,
jwtConditionSchema,
JWTConditionType,
} from '../schemas/jwt';
import { OmitConditionType } from '../shared';

export {
JWT_PARAM_DEFAULT,
JWTConditionProps,
jwtConditionSchema,
JWTConditionType,
} from '../schemas/jwt';

export class JWTCondition extends Condition {
constructor(value: OmitConditionType<JWTConditionProps>) {
super(jwtConditionSchema, {
conditionType: JWTConditionType,
...value,
});
}
}
3 changes: 3 additions & 0 deletions packages/taco/src/conditions/condition-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
JsonRpcConditionProps,
JsonRpcConditionType,
} from './base/json-rpc';
import { JWTCondition, JWTConditionProps, JWTConditionType } from './base/jwt';
import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc';
import {
TimeCondition,
Expand Down Expand Up @@ -53,6 +54,8 @@ export class ConditionFactory {
return new JsonApiCondition(props as JsonApiConditionProps);
case JsonRpcConditionType:
return new JsonRpcCondition(props as JsonRpcConditionProps);
case JWTConditionType:
return new JWTCondition(props as JWTConditionProps);
// Logical Conditions
case CompoundConditionType:
return new CompoundCondition(props as CompoundConditionProps);
Expand Down
21 changes: 21 additions & 0 deletions packages/taco/src/conditions/schemas/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod';

import { baseConditionSchema } from './common';
import { contextParamSchema } from './context';

export const JWT_PARAM_DEFAULT = ':jwtToken';

export const JWTConditionType = 'jwt';

export const jwtConditionSchema = baseConditionSchema.extend({
conditionType: z.literal(JWTConditionType).default(JWTConditionType),
publicKey: z.string(),
expectedIssuer: z.string().optional(),
// TODO see https://github.com/nucypher/taco-web/pull/604#discussion_r1901746814
// subject: contextParamSchema.optional(),
// expirationWindow: z.number().int().nonnegative().optional(),
// issuedWindow: z.number().int().nonnegative().optional(),
jwtToken: contextParamSchema.default(JWT_PARAM_DEFAULT),
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't the jwtToken what the user will provide at decryption time? rather than being defined in the condition

Copy link
Member Author

Choose a reason for hiding this comment

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

yep, you are correct, here is defined that some user param must be provided or used default :jwtToken

Copy link
Member

Choose a reason for hiding this comment

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

});

export type JWTConditionProps = z.infer<typeof jwtConditionSchema>;
2 changes: 2 additions & 0 deletions packages/taco/src/conditions/schemas/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { contractConditionSchema } from './contract';
import { ifThenElseConditionSchema } from './if-then-else';
import { jsonApiConditionSchema } from './json-api';
import { jsonRpcConditionSchema } from './json-rpc';
import { jwtConditionSchema } from './jwt';
import { rpcConditionSchema } from './rpc';
import { sequentialConditionSchema } from './sequential';
import { timeConditionSchema } from './time';
Expand All @@ -18,6 +19,7 @@ export const anyConditionSchema: z.ZodSchema = z.lazy(() =>
compoundConditionSchema,
jsonApiConditionSchema,
jsonRpcConditionSchema,
jwtConditionSchema,
sequentialConditionSchema,
ifThenElseConditionSchema,
]),
Expand Down
40 changes: 40 additions & 0 deletions packages/taco/test/conditions/base/jwt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { TEST_CONTRACT_ADDR } from '@nucypher/test-utils';
import { describe, expect, it } from 'vitest';

import {
JWTCondition,
jwtConditionSchema,
} from '../../../src/conditions/base/jwt';
import { testJWTConditionObj } from '../../test-utils';

describe('JWTCondition', () => {
describe('validation', () => {
it('accepts a valid schema', () => {
const result = JWTCondition.validate(
jwtConditionSchema,
testJWTConditionObj,
);

expect(result.error).toBeUndefined();
expect(result.data).toEqual(testJWTConditionObj);
});

it('rejects an invalid schema', () => {
const badJWTObj = {
...testJWTConditionObj,
jwtToken: TEST_CONTRACT_ADDR,
};

const result = JWTCondition.validate(jwtConditionSchema, badJWTObj);

expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
expect(result.error?.format()).toMatchObject({
jwtToken: {
_errors: ['Invalid'],
},
});
});
});
});
13 changes: 11 additions & 2 deletions packages/taco/test/conditions/compound-condition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../../src/conditions/compound-condition';
import {
testContractConditionObj,
testJWTConditionObj,
testRpcConditionObj,
testSequentialConditionObj,
testTimeConditionObj,
Expand Down Expand Up @@ -167,7 +168,11 @@ describe('validation', () => {
testRpcConditionObj,
{
operator: 'or',
operands: [testTimeConditionObj, testContractConditionObj],
operands: [
testTimeConditionObj,
testContractConditionObj,
testJWTConditionObj,
],
},
testSequentialConditionObj,
],
Expand All @@ -187,7 +192,11 @@ describe('validation', () => {
{
conditionType: CompoundConditionType,
operator: 'or',
operands: [testTimeConditionObj, testContractConditionObj],
operands: [
testTimeConditionObj,
testContractConditionObj,
testJWTConditionObj,
],
},
testSequentialConditionObj,
],
Expand Down
73 changes: 20 additions & 53 deletions packages/taco/test/conditions/lingo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { TEST_CHAIN_ID } from '@nucypher/test-utils';
import { describe, expect, it } from 'vitest';

import { ConditionExpression } from '../../src/conditions/condition-expr';
import {
testJsonApiConditionObj,
testJsonRpcConditionObj,
testJWTConditionObj,
testRpcConditionObj,
testTimeConditionObj,
} from '../test-utils';

describe('check that valid lingo in python is valid in typescript', () => {
const timeConditionProps = {
conditionType: 'time',
method: 'blocktime',
chain: TEST_CHAIN_ID,
returnValueTest: { value: 0, comparator: '>' },
};

const contractConditionProps = {
conditionType: 'contract',
chain: TEST_CHAIN_ID,
Expand All @@ -35,66 +35,32 @@ describe('check that valid lingo in python is valid in typescript', () => {
value: true,
},
};
const rpcConditionProps = {
conditionType: 'rpc',
chain: TEST_CHAIN_ID,
method: 'eth_getBalance',
parameters: ['0x3d2Bed3259b165EB02A7F0D0753e7a01912A68f8', 'latest'],
returnValueTest: {
comparator: '>=',
value: 10000000000000,
},
};
const jsonApiConditionProps = {
conditionType: 'json-api',
endpoint: 'https://api.example.com/data',
query: '$.store.book[0].price',
parameters: {
ids: 'ethereum',
vs_currencies: 'usd',
},
returnValueTest: {
comparator: '==',
value: 2,
},
};
const jsonRpcConditionProps = {
conditionType: 'json-rpc',
endpoint: 'https://math.example.com/',
method: 'subtract',
params: [42, 23],
query: '$.value',
returnValueTest: {
comparator: '==',
value: 2,
},
};
const sequentialConditionProps = {
conditionType: 'sequential',
conditionVariables: [
{
varName: 'timeValue',
condition: timeConditionProps,
condition: testTimeConditionObj,
},
{
varName: 'rpcValue',
condition: rpcConditionProps,
condition: testRpcConditionObj,
},
{
varName: 'contractValue',
condition: contractConditionProps,
},
{
varName: 'jsonValue',
condition: jsonApiConditionProps,
condition: testJsonApiConditionObj,
},
],
};
const ifThenElseConditionProps = {
conditionType: 'if-then-else',
ifCondition: jsonRpcConditionProps,
thenCondition: jsonApiConditionProps,
elseCondition: timeConditionProps,
ifCondition: testJsonRpcConditionObj,
thenCondition: testJsonApiConditionObj,
elseCondition: testTimeConditionObj,
};

const compoundConditionProps = {
Expand All @@ -104,21 +70,22 @@ describe('check that valid lingo in python is valid in typescript', () => {
contractConditionProps,
ifThenElseConditionProps,
sequentialConditionProps,
rpcConditionProps,
testRpcConditionObj,
{
conditionType: 'compound',
operator: 'not',
operands: [timeConditionProps],
operands: [testTimeConditionObj],
},
],
};

it.each([
rpcConditionProps,
timeConditionProps,
testRpcConditionObj,
testTimeConditionObj,
contractConditionProps,
jsonApiConditionProps,
jsonRpcConditionProps,
testJsonApiConditionObj,
testJsonRpcConditionObj,
testJWTConditionObj,
compoundConditionProps,
sequentialConditionProps,
ifThenElseConditionProps,
Expand Down
16 changes: 16 additions & 0 deletions packages/taco/test/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
fakeTDecFlow,
TEST_CHAIN_ID,
TEST_CONTRACT_ADDR,
TEST_ECDSA_PUBLIC_KEY,
} from '@nucypher/test-utils';
import { SpyInstance, vi } from 'vitest';

Expand All @@ -43,6 +44,11 @@ import {
JsonApiConditionProps,
JsonApiConditionType,
} from '../src/conditions/base/json-api';
import {
JWT_PARAM_DEFAULT,
JWTConditionProps,
JWTConditionType,
} from '../src/conditions/base/jwt';
import {
RpcConditionProps,
RpcConditionType,
Expand Down Expand Up @@ -259,6 +265,16 @@ export const testJsonRpcConditionObj: JsonRpcConditionProps = {
returnValueTest: testReturnValueTest,
};

export const testJWTConditionObj: JWTConditionProps = {
conditionType: JWTConditionType,
publicKey: TEST_ECDSA_PUBLIC_KEY,
expectedIssuer: '0xacbd',
// subject: ':userAddress',
// expirationWindow: 1800,
// issuedWindow: 86400,
jwtToken: JWT_PARAM_DEFAULT,
};

export const testRpcConditionObj: RpcConditionProps = {
conditionType: RpcConditionType,
chain: TEST_CHAIN_ID,
Expand Down
5 changes: 5 additions & 0 deletions packages/test-utils/src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export const TEST_SIWE_PARAMS = {
domain: 'localhost',
uri: 'http://localhost:3000',
};

export const TEST_ECDSA_PUBLIC_KEY =
Copy link
Member

@derekpierre derekpierre Dec 16, 2024

Choose a reason for hiding this comment

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

Is it possible to provide the public key without the PEM header and footer i.e. just use the base64, or hex if that is more familiar/acceptable (although less efficient in terms of space)? On the server-side we can do some adjustment if needed: convert it to PEM, OR not bother if the library allows base64/hex format, or ...

(cc @cygnusv )

'-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXHVxB7s5SR7I9cWwry' +
'/JkECIReka\nCwG3uOLCYbw5gVzn4dRmwMyYUJFcQWuFSfECRK+uQOOXD0YSEucBq0p5tA==\n-----END PUBLIC ' +
'KEY-----\n ';
Loading