1- import jwt , { Algorithm } from 'jsonwebtoken' ;
1+ import jwt , { Algorithm , JwtHeader , VerifyOptions } from 'jsonwebtoken' ;
22import type { Response } from 'node-fetch' ;
33import fetch from 'node-fetch' ;
44import { getEnv } from '../../env.js' ;
@@ -14,6 +14,7 @@ import type {
1414 UserRoles ,
1515} from '../../types/auth.js' ;
1616import { loginSSO } from './adapters/CAMAuthAdapter.js' ;
17+ import { JwksClient } from 'jwks-rsa' ;
1718import { StringValue } from 'ms' ;
1819
1920const logger = getLogger ( 'packages/auth/functions' ) ;
@@ -107,14 +108,78 @@ export async function syncRolesToDB(username: string, default_role: string, allo
107108 await db . query ( 'commit;' ) ;
108109}
109110
110- export function decodeJwt ( authorizationHeader : string | undefined ) : JwtDecode {
111+ function enforcePEMFormatting ( publicKey : string ) : string {
112+ if ( publicKey . includes ( '-----BEGIN PUBLIC KEY-----' ) && publicKey . includes ( '-----END PUBLIC KEY-----' ) ) {
113+ return publicKey ;
114+ }
115+ else {
116+ return '-----BEGIN PUBLIC KEY-----\n' + publicKey + '\n-----END PUBLIC KEY-----'
117+ }
118+ }
119+
120+ export async function decodeJwt ( authorizationHeader : string | undefined ) : Promise < JwtDecode > {
111121 try {
112122 const token = authorizationHeaderToToken ( authorizationHeader ) ;
113- const { HASURA_GRAPHQL_JWT_SECRET , JWT_ALGORITHMS } = getEnv ( ) ;
114- const { key } : JwtSecret = JSON . parse ( HASURA_GRAPHQL_JWT_SECRET ) ;
115- const options : jwt . VerifyOptions = { algorithms : JWT_ALGORITHMS } ;
116- const jwtPayload = jwt . verify ( token , key , options ) as JwtPayload ;
117- return { jwtErrorMessage : '' , jwtPayload } ;
123+ // TODO: this ignores the JWT_ALGORITHMS env variable, because that's included in HASURA_GRAPHQL_JWT_SECRET, both the keycloak version, and even the local version...
124+ const { HASURA_GRAPHQL_JWT_SECRET } = getEnv ( ) ;
125+ const { type, key, jwk_url } : JwtSecret = JSON . parse ( HASURA_GRAPHQL_JWT_SECRET ) ;
126+
127+ // TODO: figure out the defaults, for some reason the JWT_ALGORITHM env variable and it's default were getting messed up...default _should_ be HS256, but if using Keycloak, need RS256
128+ const options : jwt . VerifyOptions = { algorithms : [ 'RS256' , 'HS256' ] } ;
129+
130+ type getKeyType = ( header : JwtHeader , callback : any ) => void ;
131+ let realKey : string | getKeyType ;
132+
133+ // if they are using a jwk_url instead, pull the key!
134+ if ( ! key && jwk_url ) {
135+ // https://www.npmjs.com/package/jsonwebtoken
136+ const client = new JwksClient ( {
137+ jwksUri : jwk_url
138+ } ) ;
139+
140+ realKey = function ( header , callback ) {
141+ client . getSigningKey ( header . kid , function ( err , key ) {
142+ if ( key ) {
143+ const signingKey = key ?. getPublicKey ( ) ;
144+ callback ( null , signingKey ) ;
145+ }
146+ else {
147+ console . log ( err )
148+ }
149+ } ) ;
150+ }
151+
152+ const verifyJwt = async function ( token : string , options : VerifyOptions = { } ) : Promise < any > {
153+ return new Promise ( ( resolve , reject ) => {
154+ jwt . verify ( token , realKey , options , ( err , decoded ) => {
155+ if ( err ) return reject ( err ) ;
156+ resolve ( decoded ) ;
157+ } ) ;
158+ } ) ;
159+ }
160+
161+ try {
162+ const jwtPayload = await verifyJwt ( token , options ) ;
163+ return { jwtErrorMessage : '' , jwtPayload : jwtPayload }
164+ } catch ( err ) {
165+ return { jwtErrorMessage : 'JWT verification failed: ' + err , jwtPayload : null }
166+ }
167+ }
168+ else if ( key ) {
169+ if ( type === "RS256" ) {
170+ realKey = enforcePEMFormatting ( key ) ;
171+ }
172+ else {
173+ realKey = key ;
174+ }
175+
176+ const jwtPayload = jwt . verify ( token , realKey , options ) as JwtPayload ;
177+ return { jwtErrorMessage : '' , jwtPayload } ;
178+ }
179+ else {
180+ const jwtErrorMessage = 'Neither a valid JWT Key or JWK URL were provided. A type (algorithm) and either of those two must be provided.'
181+ return { jwtErrorMessage, jwtPayload : null } ;
182+ }
118183 } catch ( e ) {
119184 console . error ( e ) ;
120185
@@ -139,17 +204,21 @@ export function generateJwt(
139204 try {
140205 const { HASURA_GRAPHQL_JWT_SECRET } = getEnv ( ) ;
141206 const { key, type } : JwtSecret = JSON . parse ( HASURA_GRAPHQL_JWT_SECRET ) ;
142- const options : jwt . SignOptions = { algorithm : type as Algorithm , expiresIn : expiry } ;
143- const payload : JwtPayload = {
144- 'https://hasura.io/jwt/claims' : {
145- 'x-hasura-allowed-roles' : allowedRoles ,
146- 'x-hasura-default-role' : defaultRole ,
147- 'x-hasura-user-id' : username ,
148- } ,
149- username,
150- } ;
207+ if ( key ) {
208+ const options : jwt . SignOptions = { algorithm : type as Algorithm , expiresIn : expiry } ;
209+ const payload : JwtPayload = {
210+ 'https://hasura.io/jwt/claims' : {
211+ 'x-hasura-allowed-roles' : allowedRoles ,
212+ 'x-hasura-default-role' : defaultRole ,
213+ 'x-hasura-user-id' : username ,
214+ } ,
215+ username,
216+ } ;
151217
152- return jwt . sign ( payload , key , options ) ;
218+ return jwt . sign ( payload , key , options ) ;
219+ }
220+ console . error ( 'using JWKS URL, so this JWT generation will not work. You also shouldn\'t be using this method if using JWKS' )
221+ return null ;
153222 } catch ( e ) {
154223 console . error ( e ) ;
155224 return null ;
@@ -210,7 +279,7 @@ export async function login(username: string, password: string): Promise<AuthRes
210279}
211280
212281export async function session ( authorizationHeader : string | undefined ) : Promise < SessionResponse > {
213- const { jwtErrorMessage, jwtPayload } = decodeJwt ( authorizationHeader ) ;
282+ const { jwtErrorMessage, jwtPayload } = await decodeJwt ( authorizationHeader ) ;
214283
215284 if ( jwtPayload ) {
216285 return { message : 'Token is valid' , success : true } ;
0 commit comments