Skip to content

Commit 9354860

Browse files
authored
If possible, instantiate connection to JWKS on startup (#3795)
1 parent 79a652f commit 9354860

File tree

2 files changed

+46
-21
lines changed

2 files changed

+46
-21
lines changed

.changeset/unlucky-foxes-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@neo4j/graphql": patch
3+
---
4+
5+
If possible, instantiate JWKS endpoint connection on startup, to benefit from caching.

packages/graphql/src/classes/authorization/Neo4jGraphQLAuthorization.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
*/
1919

2020
import Debug from "debug";
21-
import type { Key, Neo4jAuthorizationSettings } from "../../types";
21+
import type { Key, Neo4jAuthorizationSettings, RemoteJWKS } from "../../types";
2222

2323
import { AUTHORIZATION_UNAUTHENTICATED, DEBUG_AUTH } from "../../constants";
2424
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
25-
import type { JWTPayload } from "jose";
25+
import type { JWTPayload, JWTVerifyGetKey } from "jose";
2626
import { parseBearerToken } from "./parse-request-token";
2727
import { Neo4jGraphQLError } from "../Error";
2828
import type { Neo4jGraphQLContext } from "../../types/neo4j-graphql-context";
@@ -32,7 +32,18 @@ const debug = Debug(DEBUG_AUTH);
3232
export class Neo4jGraphQLAuthorization {
3333
private authorization: Neo4jAuthorizationSettings;
3434

35+
// Assigned if input is a static symmetric secret or JWKS details
36+
private resolvedKey: Uint8Array | JWTVerifyGetKey | undefined;
37+
// Assigned if input is dynamic key which needs to be fetched using context details
38+
private unresolvedKey: ((context: Neo4jGraphQLContext) => Key) | undefined;
39+
3540
constructor(authorization: Neo4jAuthorizationSettings) {
41+
if (typeof authorization.key === "function") {
42+
this.unresolvedKey = authorization.key;
43+
} else {
44+
this.resolvedKey = this.serializeKey(authorization.key);
45+
}
46+
3647
this.authorization = authorization;
3748
}
3849

@@ -62,19 +73,6 @@ export class Neo4jGraphQLAuthorization {
6273
}
6374
}
6475

65-
public decodeBearerToken(bearerToken: string): JWTPayload | undefined {
66-
const token = parseBearerToken(bearerToken);
67-
if (!token) {
68-
throw new Neo4jGraphQLError(AUTHORIZATION_UNAUTHENTICATED);
69-
}
70-
try {
71-
return decodeJwt(token);
72-
} catch (error) {
73-
debug("%s", error);
74-
throw new Neo4jGraphQLError(AUTHORIZATION_UNAUTHENTICATED);
75-
}
76-
}
77-
7876
public async decodeBearerTokenWithVerify(bearerToken: string | undefined): Promise<JWTPayload | undefined> {
7977
if (!bearerToken) {
8078
throw new Neo4jGraphQLError(AUTHORIZATION_UNAUTHENTICATED);
@@ -89,22 +87,44 @@ export class Neo4jGraphQLAuthorization {
8987
debug("Skipping verifying JWT as verify is set to false");
9088
return decodeJwt(token);
9189
}
92-
return await this.verify(token, this.authorization.key as Key);
90+
return await this.verifyBearerToken(token, this.authorization.key as Key);
9391
} catch (error) {
9492
debug("%s", error);
9593
throw new Neo4jGraphQLError(AUTHORIZATION_UNAUTHENTICATED);
9694
}
9795
}
9896

99-
private resolveKey(context: Neo4jGraphQLContext): Key {
100-
if (typeof this.authorization.key === "function") {
101-
return this.authorization.key(context);
97+
private serializeKey(key: string | RemoteJWKS): Uint8Array | JWTVerifyGetKey {
98+
if (typeof key === "string") {
99+
return Buffer.from(key);
102100
} else {
103-
return this.authorization.key;
101+
return createRemoteJWKSet(new URL(key.url), key.options);
104102
}
105103
}
106104

107-
private async verify(token: string, secret: Key): Promise<JWTPayload> {
105+
private resolveKey(context: Neo4jGraphQLContext): Uint8Array | JWTVerifyGetKey {
106+
if (this.resolvedKey) {
107+
return this.resolvedKey;
108+
} else {
109+
// this.unresolvedKey is definitely defined due to typings and if/else
110+
const resolved = this.unresolvedKey!(context);
111+
112+
return this.serializeKey(resolved);
113+
}
114+
}
115+
116+
private async verify(token: string, secret: Uint8Array | JWTVerifyGetKey): Promise<JWTPayload> {
117+
if (secret instanceof Uint8Array) {
118+
debug("Verifying JWT using secret");
119+
const { payload } = await jwtVerify(token, secret, this.authorization.verifyOptions);
120+
return payload;
121+
}
122+
debug("Verifying JWKS using url");
123+
const { payload } = await jwtVerify(token, secret, this.authorization.verifyOptions);
124+
return payload;
125+
}
126+
127+
private async verifyBearerToken(token: string, secret: Key): Promise<JWTPayload> {
108128
if (typeof secret === "string") {
109129
debug("Verifying JWT using secret");
110130
const { payload } = await jwtVerify(token, Buffer.from(secret), this.authorization.verifyOptions);

0 commit comments

Comments
 (0)