Skip to content

Commit

Permalink
fix: added verification check if data token is not changed
Browse files Browse the repository at this point in the history
  • Loading branch information
demeyerthom committed Jan 16, 2025
1 parent de2a9c7 commit f4a607a
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ node_modules/*
**/*.log
coverage/*
dist/*
test-reports/*
**/test-reports/*

.turbo/
29 changes: 28 additions & 1 deletion packages/apollo/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class GatewayAuthPlugin<TContext extends PublicFederatedTokenContext>

if (accessToken) {
try {
await token.loadAccessJWT(this.signer, accessToken, dataToken);
await token.loadAccessJWT(this.signer, accessToken);
} catch (e: unknown) {
this.tokenSource.deleteAccessToken(contextValue.req, contextValue.res);

Expand Down Expand Up @@ -88,6 +88,33 @@ export class GatewayAuthPlugin<TContext extends PublicFederatedTokenContext>
this.tokenSource.deleteRefreshToken(contextValue.req, contextValue.res);
}
}

if (dataToken) {
try {
await token.loadDataJWT(this.signer, dataToken);
} catch (e: unknown) {
this.tokenSource.deleteDataToken(contextValue.req, contextValue.res);
if (e instanceof TokenExpiredError) {
throw new GraphQLError("Your token has expired.", {
extensions: {
code: "UNAUTHENTICATED",
http: {
statusCode: 401,
},
},
});
} else {
throw new GraphQLError("Your token is invalid.", {
extensions: {
code: "INVALID_TOKEN",
http: {
statusCode: 400,
},
},
});
}
}
}
return this;
}

Expand Down
76 changes: 45 additions & 31 deletions packages/core/src/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,11 @@ describe("PublicFederatedToken", async () => {
sub: "exampleSubject",
},
};
token.values = {
value1: "exampleValue1",
value2: "exampleValue2",
};

const accessToken = await token.createAccessJWT(signer);
const dataToken = await token.createDataJWT(signer);

const newToken = new PublicFederatedToken();
await newToken.loadAccessJWT(signer, accessToken, dataToken);
await newToken.loadAccessJWT(signer, accessToken);
expect(newToken.tokens).toStrictEqual(token.tokens);
expect(newToken.refreshTokens).toStrictEqual(token.refreshTokens);
expect(newToken.values).toStrictEqual(token.values);
});

test("createAccessJWT with TokenSigner create hook", async () => {
Expand All @@ -62,31 +54,15 @@ describe("PublicFederatedToken", async () => {
sub: "exampleSubject",
},
};
token.values = {
value1: "exampleValue1",
value2: "exampleValue2",
};

const accessToken = await token.createAccessJWT(signer);
const dataToken = await token.createDataJWT(signer);

const newToken = new PublicFederatedToken();
await newToken.loadAccessJWT(signer, accessToken, dataToken);
await newToken.loadAccessJWT(signer, accessToken);
expect(newToken.tokens).toStrictEqual(token.tokens);
expect(newToken.refreshTokens).toStrictEqual(token.refreshTokens);
expect(newToken.values).toStrictEqual(token.values);
});

test("loadAccessJWT", async () => {
const time = 1729258233173;

const dataJWT = await signer.signJWT({
exp: Date.now() + 1000,
values: {
value1: "exampleValue1",
value2: "exampleValue2",
},
});
const tokenJWT = await signer.encryptJWT(
{
tokens: {
Expand All @@ -101,18 +77,14 @@ describe("PublicFederatedToken", async () => {
);

const token = new PublicFederatedToken();
await token.loadAccessJWT(signer, tokenJWT, dataJWT);
await token.loadAccessJWT(signer, tokenJWT);
expect(token.tokens).toStrictEqual({
exampleName: {
token: "exampleToken",
exp: time,
sub: "exampleSubject",
},
});
expect(token.values).toStrictEqual({
value1: "exampleValue1",
value2: "exampleValue2",
});
});

test("createRefreshJWT", async () => {
Expand All @@ -125,4 +97,46 @@ describe("PublicFederatedToken", async () => {
const jwt = await token.createRefreshJWT(signer);
expect(jwt).toBeDefined();
});

test("loadRefreshJWT", async () => {
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 90;
const payload = {
values: { value1: "exampleValue1" },
exp,
};

const jwt = await signer.encryptJWT(payload, exp);

const token = new PublicFederatedToken();
await token.loadRefreshJWT(signer, jwt);
expect(token.refreshTokens).toBeDefined();
expect(token.refreshTokens.values).toStrictEqual(payload.values);
});

test("createDataJWT", async () => {
// Write tests for createRefreshJWT if needed
const token = new PublicFederatedToken();
token.values = {
value1: "exampleValue1",
value2: "exampleValue2",
};

const jwt = await token.createDataJWT(signer);
expect(jwt).toBeDefined();
});

test("loadDataJWT", async () => {
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 90;
const payload = {
values: { value1: "exampleValue1" },
exp,
};

const jwt = await signer.signJWT(payload);

const token = new PublicFederatedToken();
await token.loadDataJWT(signer, jwt);
expect(token.values).toBeDefined();
expect(token.values).toStrictEqual(payload.values);
});
});
26 changes: 15 additions & 11 deletions packages/core/src/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ type JWTPayload = {
};

export class PublicFederatedToken extends FederatedToken {
// Create the access JWT. This JWT is send to the client. It is send as
// signed token (not encrypted). The jwe attribute is encrypted however.
// Create the access JWT. This JWT is sent to the client. It is sent as
// signed token (not encrypted). The jwe attribute is encrypted, however.
// This is all done when the GraphQL gateway sends the response back to the
// client.

Expand All @@ -22,7 +22,7 @@ export class PublicFederatedToken extends FederatedToken {
return;
}

const exp = this.getExpireTime();
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 90;
const payload = {
values: this.values,
exp,
Expand All @@ -32,7 +32,7 @@ export class PublicFederatedToken extends FederatedToken {
return await signer.signJWT(payload);
}

// Create the access JWT. This JWT is send to the client. Is used in the
// Create the access JWT. This JWT is sent to the client. Is used in the
// userToken / guestToken and should be encrypted and HTTP_ONLY
async createAccessJWT(signer: TokenSigner) {
const exp = this.getExpireTime();
Expand All @@ -43,7 +43,16 @@ export class PublicFederatedToken extends FederatedToken {
return await signer.encryptJWT(data, exp);
}

async loadAccessJWT(signer: TokenSigner, value: string, data?: string) {
async loadDataJWT(signer: TokenSigner, value: string) {
const result = await signer.verifyJWT(value);
if (!result) {
throw new TokenInvalidError("Invalid JWT");
}

this.values = result.payload.values as Record<string, any>;
}

async loadAccessJWT(signer: TokenSigner, value: string) {
const result = await signer.decryptJWT(value);
if (!result) {
throw new Error("Invalid JWT");
Expand All @@ -54,7 +63,7 @@ export class PublicFederatedToken extends FederatedToken {
throw new TokenInvalidError("Invalid JWT");
}

// The expire time should be absolute, now margin for error. The client
// The expiry time should be absolute, now margin for error. The client
// should refresh the token X second before it expires.
const unixTime = Math.floor(Date.now() / 1000);
if (!payload.exp || payload.exp < unixTime) {
Expand All @@ -67,11 +76,6 @@ export class PublicFederatedToken extends FederatedToken {
} else {
this.setIsAnonymous();
}

if (data) {
const result = await signer.verifyJWT(data);
this.values = result.payload.values as Record<string, any>;
}
}

// createRefreshJWT encrypts the refresh token and return a JWT. The token is
Expand Down

0 comments on commit f4a607a

Please sign in to comment.