feat: add JWT authentication support (#5079)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
e1f78b185a
commit
82d9367843
27 changed files with 1301 additions and 818 deletions
|
|
@ -52,7 +52,7 @@
|
|||
"qs": "6.13.0",
|
||||
"verzod": "0.2.4",
|
||||
"xmlbuilder2": "3.1.1",
|
||||
"zod": "3.23.8"
|
||||
"zod": "3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"qs": "6.11.2",
|
||||
"semver": "7.6.3",
|
||||
"tsup": "8.3.0",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.8.3",
|
||||
"vitest": "2.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"v": 3,
|
||||
"name": "JWT Auth (headers) - collection",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "13",
|
||||
"id": "cm0dm70cw000687bnxi830zz3",
|
||||
"auth": {
|
||||
"authType": "jwt",
|
||||
"authActive": true,
|
||||
"addTo": "HEADERS",
|
||||
"algorithm": "<<algorithm>>",
|
||||
"secret": "<<secret>>",
|
||||
"privateKey": "<<privateKey>>",
|
||||
"payload": "<<payload>>",
|
||||
"jwtHeaders": "<<jwtHeaders>>",
|
||||
"headerPrefix": "<<headerPrefix>>",
|
||||
"isSecretBase64Encoded": false
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "jwt-auth-headers",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"endpoint": "<<url>>",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);});",
|
||||
"preRequestScript": "",
|
||||
"responses": {},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"v": 3,
|
||||
"name": "JWT Auth (params) - collection",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "13",
|
||||
"id": "cm0dm70cw000687bnxi830zz4",
|
||||
"auth": {
|
||||
"authType": "jwt",
|
||||
"authActive": true,
|
||||
"addTo": "QUERY_PARAMS",
|
||||
"algorithm": "<<algorithm>>",
|
||||
"secret": "<<secret>>",
|
||||
"privateKey": "<<privateKey>>",
|
||||
"payload": "<<payload>>",
|
||||
"jwtHeaders": "<<jwtHeaders>>",
|
||||
"paramName": "<<paramName>>",
|
||||
"isSecretBase64Encoded": false
|
||||
},
|
||||
"body": {
|
||||
"body": null,
|
||||
"contentType": null
|
||||
},
|
||||
"name": "jwt-auth-params",
|
||||
"method": "GET",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"endpoint": "<<url>>",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);});",
|
||||
"preRequestScript": "",
|
||||
"responses": {},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"v": 1,
|
||||
"id": "cm0dsn3v70004p4qk3l9b7sjn",
|
||||
"name": "JWT Auth - environments",
|
||||
"variables": [
|
||||
{
|
||||
"key": "url",
|
||||
"value": "http://localhost:8888/auth/jwt"
|
||||
},
|
||||
{
|
||||
"key": "algorithm",
|
||||
"value": "HS256"
|
||||
},
|
||||
{
|
||||
"key": "secret",
|
||||
"value": "secret-1"
|
||||
},
|
||||
{
|
||||
"key": "privateKey",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "payload",
|
||||
"value": "{\"user\":\"test\",\"role\":\"admin\"}"
|
||||
},
|
||||
{
|
||||
"key": "jwtHeaders",
|
||||
"value": "{}"
|
||||
},
|
||||
{
|
||||
"key": "headerPrefix",
|
||||
"value": "Bearer "
|
||||
},
|
||||
{
|
||||
"key": "paramName",
|
||||
"value": "token"
|
||||
},
|
||||
{
|
||||
"key": "isSecretBase64Encoded",
|
||||
"value": "false"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -485,17 +485,17 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
|
|||
export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] =
|
||||
[
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1f86hv000010f8szcfya0t",
|
||||
name: "Multiple child collections with authorization & headers set at each level",
|
||||
folders: [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fjgah000110f8a5bs68gd",
|
||||
name: "folder-1",
|
||||
folders: [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fjwmm000410f8l1gkkr1a",
|
||||
name: "folder-11",
|
||||
folders: [],
|
||||
|
|
@ -537,7 +537,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fjyxm000510f8pv90dt43",
|
||||
name: "folder-12",
|
||||
folders: [],
|
||||
|
|
@ -595,7 +595,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fk1cv000610f88kc3aupy",
|
||||
name: "folder-13",
|
||||
folders: [],
|
||||
|
|
@ -707,12 +707,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fjk9o000210f8j0573pls",
|
||||
name: "folder-2",
|
||||
folders: [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fk516000710f87sfpw6bo",
|
||||
name: "folder-21",
|
||||
folders: [],
|
||||
|
|
@ -752,7 +752,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fk72t000810f8gfwkpi5y",
|
||||
name: "folder-22",
|
||||
folders: [],
|
||||
|
|
@ -810,7 +810,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fk95g000910f8bunhaoo8",
|
||||
name: "folder-23",
|
||||
folders: [],
|
||||
|
|
@ -915,12 +915,12 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1fjmlq000310f86o4d3w2o",
|
||||
name: "folder-3",
|
||||
folders: [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1iwq0p003e10f8u8zg0p85",
|
||||
name: "folder-31",
|
||||
folders: [],
|
||||
|
|
@ -960,7 +960,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1izut7003m10f894ip59zg",
|
||||
name: "folder-32",
|
||||
folders: [],
|
||||
|
|
@ -1018,7 +1018,7 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
|
|||
],
|
||||
},
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
id: "clx1j2ka9003q10f8cdbzpgpg",
|
||||
name: "folder-33",
|
||||
folders: [],
|
||||
|
|
@ -1372,42 +1372,50 @@ export const WORKSPACE_ENVIRONMENT_MOCK: WorkspaceEnvironment = {
|
|||
variables: [
|
||||
{
|
||||
key: "firstName",
|
||||
value: "John",
|
||||
initialValue: "John",
|
||||
currentValue: "John",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "lastName",
|
||||
value: "Doe",
|
||||
initialValue: "Doe",
|
||||
currentValue: "Doe",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "id",
|
||||
value: "7",
|
||||
initialValue: "7",
|
||||
currentValue: "7",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "fullName",
|
||||
value: "<<firstName>> <<lastName>>",
|
||||
initialValue: "<<firstName>> <<lastName>>",
|
||||
currentValue: "<<firstName>> <<lastName>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "recursiveVarX",
|
||||
value: "<<recursiveVarY>>",
|
||||
initialValue: "<<recursiveVarY>>",
|
||||
currentValue: "<<recursiveVarY>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "recursiveVarY",
|
||||
value: "<<salutation>>",
|
||||
initialValue: "<<salutation>>",
|
||||
currentValue: "<<salutation>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "salutation",
|
||||
value: "Hello",
|
||||
initialValue: "Hello",
|
||||
currentValue: "Hello",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "greetText",
|
||||
value: "<<salutation>> <<fullName>>",
|
||||
initialValue: "<<salutation>> <<fullName>>",
|
||||
currentValue: "<<salutation>> <<fullName>>",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
|
|
@ -1420,42 +1428,50 @@ export const TRANSFORMED_ENVIRONMENT_MOCK: Environment = {
|
|||
variables: [
|
||||
{
|
||||
key: "firstName",
|
||||
value: "John",
|
||||
initialValue: "John",
|
||||
currentValue: "John",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "lastName",
|
||||
value: "Doe",
|
||||
initialValue: "Doe",
|
||||
currentValue: "Doe",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "id",
|
||||
value: "7",
|
||||
initialValue: "7",
|
||||
currentValue: "7",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "fullName",
|
||||
value: "<<firstName>> <<lastName>>",
|
||||
initialValue: "<<firstName>> <<lastName>>",
|
||||
currentValue: "<<firstName>> <<lastName>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "recursiveVarX",
|
||||
value: "<<recursiveVarY>>",
|
||||
initialValue: "<<recursiveVarY>>",
|
||||
currentValue: "<<recursiveVarY>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "recursiveVarY",
|
||||
value: "<<salutation>>",
|
||||
initialValue: "<<salutation>>",
|
||||
currentValue: "<<salutation>>",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "salutation",
|
||||
value: "Hello",
|
||||
initialValue: "Hello",
|
||||
currentValue: "Hello",
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
key: "greetText",
|
||||
value: "<<salutation>> <<fullName>>",
|
||||
initialValue: "<<salutation>> <<fullName>>",
|
||||
currentValue: "<<salutation>> <<fullName>>",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
parseRawKeyValueEntriesE,
|
||||
parseTemplateString,
|
||||
parseTemplateStringE,
|
||||
generateJWTToken,
|
||||
} from "@hoppscotch/data";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import * as A from "fp-ts/Array";
|
||||
|
|
@ -334,6 +335,50 @@ export async function getEffectiveRESTRequest(
|
|||
value: hawkHeader,
|
||||
description: "",
|
||||
});
|
||||
} else if (request.auth.authType === "jwt") {
|
||||
const { addTo } = request.auth;
|
||||
|
||||
// Generate JWT token
|
||||
const token = await generateJWTToken({
|
||||
algorithm: request.auth.algorithm || "HS256",
|
||||
secret: parseTemplateString(request.auth.secret, resolvedVariables),
|
||||
privateKey: parseTemplateString(
|
||||
request.auth.privateKey,
|
||||
resolvedVariables
|
||||
),
|
||||
payload: parseTemplateString(request.auth.payload, resolvedVariables),
|
||||
jwtHeaders: parseTemplateString(
|
||||
request.auth.jwtHeaders,
|
||||
resolvedVariables
|
||||
),
|
||||
isSecretBase64Encoded: request.auth.isSecretBase64Encoded,
|
||||
});
|
||||
|
||||
if (token) {
|
||||
if (addTo === "HEADERS") {
|
||||
const headerPrefix =
|
||||
parseTemplateString(request.auth.headerPrefix, resolvedVariables) ||
|
||||
"Bearer ";
|
||||
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `${headerPrefix}${token}`,
|
||||
description: "",
|
||||
});
|
||||
} else if (addTo === "QUERY_PARAMS") {
|
||||
const paramName =
|
||||
parseTemplateString(request.auth.paramName, resolvedVariables) ||
|
||||
"token";
|
||||
|
||||
effectiveFinalParams.push({
|
||||
active: true,
|
||||
key: paramName,
|
||||
value: token,
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,9 @@
|
|||
"username": "Username",
|
||||
"advance_config": "Advanced Configuration",
|
||||
"advance_config_description": "Hoppscotch automatically assigns default values to certain fields if no explicit value is provided",
|
||||
"algorithm": "Algorithm",
|
||||
"payload": "Payload",
|
||||
"secret": "Secret",
|
||||
"aws_signature": {
|
||||
"access_key": "Access Key",
|
||||
"secret_key": "Secret Key",
|
||||
|
|
@ -268,6 +271,16 @@
|
|||
"client_nonce": "Client Nonce",
|
||||
"opaque": "Opaque",
|
||||
"disable_retry": "Disable Retrying Request"
|
||||
},
|
||||
"jwt": {
|
||||
"params_name": "Params Name",
|
||||
"header_prefix": "Header Prefix",
|
||||
"placeholder_request_header": "Request header prefix",
|
||||
"placeholder_request_param": "Request params name",
|
||||
"secret_base64_encoded": "Secret Base64 Encoded",
|
||||
"headers": "JWT Headers",
|
||||
"private_key": "Private Key",
|
||||
"placeholder_headers": "JWT Headers"
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
"workbox-window": "7.1.0",
|
||||
"xml-formatter": "3.6.3",
|
||||
"yargs-parser": "21.1.1",
|
||||
"zod": "3.23.8"
|
||||
"zod": "3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"rollup-plugin-polyfill-node": "0.13.0",
|
||||
"sass": "1.79.5",
|
||||
"tailwindcss": "3.4.14",
|
||||
"typescript": "5.3.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-fonts": "1.1.1",
|
||||
"unplugin-icons": "0.19.3",
|
||||
"unplugin-vue-components": "0.27.4",
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ declare module 'vue' {
|
|||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||
HistoryPersonal: typeof import('./components/history/Personal.vue')['default']
|
||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||
HoppAccordion: typeof import('@hoppscotch/ui')['HoppAccordion']
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
|
|
@ -145,6 +146,7 @@ declare module 'vue' {
|
|||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||
HoppSmartTextarea: typeof import('@hoppscotch/ui')['HoppSmartTextarea']
|
||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||
|
|
@ -157,6 +159,7 @@ declare module 'vue' {
|
|||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||
HttpAuthorizationDigest: typeof import('./components/http/authorization/Digest.vue')['default']
|
||||
HttpAuthorizationHAWK: typeof import('./components/http/authorization/HAWK.vue')['default']
|
||||
HttpAuthorizationJWT: typeof import('./components/http/authorization/JWT.vue')['default']
|
||||
HttpAuthorizationNTLM: typeof import('./components/http/authorization/NTLM.vue')['default']
|
||||
HttpAuthorizationOAuth2: typeof import('./components/http/authorization/OAuth2.vue')['default']
|
||||
HttpBody: typeof import('./components/http/Body.vue')['default']
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@
|
|||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>{{ t("state.enabled") }}</HoppSmartCheckbox
|
||||
>
|
||||
{{ t("state.enabled") }}
|
||||
</HoppSmartCheckbox>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||
|
|
@ -157,6 +158,9 @@
|
|||
<div v-if="auth.authType === 'digest'">
|
||||
<HttpAuthorizationDigest v-model="auth" :envs="envs" />
|
||||
</div>
|
||||
<div v-if="auth.authType === 'jwt'">
|
||||
<HttpAuthorizationJWT v-model="auth" :envs="envs" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
|
|
@ -198,6 +202,7 @@ import {
|
|||
HoppRESTAuthDigest,
|
||||
HoppRESTAuthHAWK,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTAuthJWT,
|
||||
} from "@hoppscotch/data"
|
||||
|
||||
const t = useI18n()
|
||||
|
|
@ -294,6 +299,21 @@ const selectDigestAuthType = () => {
|
|||
} as HoppRESTAuth
|
||||
}
|
||||
|
||||
const selectJWTAuthType = () => {
|
||||
auth.value = {
|
||||
...auth.value,
|
||||
authType: "jwt",
|
||||
secret: "",
|
||||
algorithm: "HS256",
|
||||
payload: "{}",
|
||||
addTo: "HEADERS",
|
||||
isSecretBase64Encoded: false,
|
||||
headerPrefix: "Bearer ",
|
||||
paramName: "token",
|
||||
jwtHeaders: "{}",
|
||||
} as HoppRESTAuthJWT
|
||||
}
|
||||
|
||||
const authTypes: AuthType[] = [
|
||||
{
|
||||
key: "inherit",
|
||||
|
|
@ -336,6 +356,11 @@ const authTypes: AuthType[] = [
|
|||
label: "HAWK",
|
||||
handler: selectHAWKAuthType,
|
||||
},
|
||||
{
|
||||
key: "jwt",
|
||||
label: "JWT",
|
||||
handler: selectJWTAuthType,
|
||||
},
|
||||
]
|
||||
|
||||
const authType = pluckRef(auth, "authType")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,310 @@
|
|||
<template>
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
<label class="ml-4 text-secondaryLight">
|
||||
{{ t("authorization.digest.algorithm") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="auth.algorithm"
|
||||
class="ml-2 rounded-none pr-8"
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="alg in algorithms"
|
||||
:key="alg"
|
||||
:icon="auth.algorithm === alg ? IconCircleDot : IconCircle"
|
||||
:active="auth.algorithm === alg"
|
||||
:label="alg"
|
||||
@click="
|
||||
() => {
|
||||
auth.algorithm = alg
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Private Key field for RSA/ECDSA algorithms -->
|
||||
|
||||
<div
|
||||
v-if="isAsymmetricAlgorithm"
|
||||
class="ml-4 py-2 border-b border-dividerLight"
|
||||
>
|
||||
<label class="text-secondaryLight">
|
||||
{{ t("authorization.jwt.private_key") }}
|
||||
</label>
|
||||
<div ref="privateKeyEditor" class="mt-2 h-32"></div>
|
||||
</div>
|
||||
|
||||
<!-- Secret field for HMAC algorithms -->
|
||||
<div v-else>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="auth.secret"
|
||||
:auto-complete-env="true"
|
||||
:placeholder="t('authorization.secret')"
|
||||
:envs="envs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-2 flex items-center">
|
||||
<HoppSmartCheckbox
|
||||
:on="auth.isSecretBase64Encoded"
|
||||
@change="auth.isSecretBase64Encoded = !auth.isSecretBase64Encoded"
|
||||
>
|
||||
{{ t("authorization.jwt.secret_base64_encoded") }}
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 py-2 border-b border-dividerLight">
|
||||
<label class="text-secondaryLight">
|
||||
{{ t("authorization.payload") }}
|
||||
</label>
|
||||
<div ref="payloadEditor" class="mt-2 h-32"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<!-- label as advanced config here -->
|
||||
<div class="p-4 flex flex-col space-y-1">
|
||||
<label>
|
||||
{{ t("authorization.advance_config") }}
|
||||
</label>
|
||||
<p class="text-secondaryLight">
|
||||
{{ t("authorization.advance_config_description") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
<label class="ml-4 text-secondaryLight">
|
||||
{{ t("authorization.pass_key_by") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions?.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="passBy"
|
||||
class="ml-2 rounded-none pr-8"
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="addToTarget in addToTargets"
|
||||
:key="addToTarget.id"
|
||||
:label="addToTarget.label"
|
||||
:icon="
|
||||
auth.addTo === addToTarget.id ? IconCircleDot : IconCircle
|
||||
"
|
||||
:active="auth.addTo === addToTarget.id"
|
||||
@click="
|
||||
() => {
|
||||
auth.addTo = addToTarget.id
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- passby conditional prefix or name -->
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-if="auth.addTo === 'HEADERS'"
|
||||
v-model="auth.headerPrefix"
|
||||
:auto-complete-env="true"
|
||||
:placeholder="t('authorization.jwt.placeholder_request_header')"
|
||||
:envs="envs"
|
||||
/>
|
||||
|
||||
<SmartEnvInput
|
||||
v-else
|
||||
v-model="auth.paramName"
|
||||
:auto-complete-env="true"
|
||||
:placeholder="t('authorization.jwt.placeholder_request_param')"
|
||||
:envs="envs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 py-2 border-b border-dividerLight">
|
||||
<label class="text-secondaryLight">
|
||||
{{ t("authorization.jwt.headers") }}
|
||||
</label>
|
||||
<div ref="headersEditor" class="mt-2 h-32"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTAuthJWT } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { AggregateEnvironment } from "~/newstore/environments"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
|
||||
const t = useI18n()
|
||||
const authTippyActions = ref<any | null>(null)
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppRESTAuthJWT
|
||||
envs?: AggregateEnvironment[]
|
||||
}>(),
|
||||
{
|
||||
envs: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTAuthJWT): void
|
||||
}>()
|
||||
|
||||
const auth = useVModel(props, "modelValue", emit)
|
||||
|
||||
// Template refs for CodeMirror editors
|
||||
const payloadEditor = ref<any | null>(null)
|
||||
const headersEditor = ref<any | null>(null)
|
||||
const privateKeyEditor = ref<any | null>(null)
|
||||
|
||||
const payload = computed({
|
||||
get: () => auth.value.payload,
|
||||
set: (value) => {
|
||||
auth.value = {
|
||||
...auth.value,
|
||||
payload: value,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const jwtHeaders = computed({
|
||||
get: () => auth.value.jwtHeaders,
|
||||
set: (value) => {
|
||||
auth.value = {
|
||||
...auth.value,
|
||||
jwtHeaders: value,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const privateKey = computed({
|
||||
get: () => auth.value.privateKey,
|
||||
set: (value) => {
|
||||
auth.value = {
|
||||
...auth.value,
|
||||
privateKey: value,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize CodeMirror for payload editor
|
||||
useCodemirror(
|
||||
payloadEditor,
|
||||
payload,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/json",
|
||||
readOnly: false,
|
||||
lineWrapping: true,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: true,
|
||||
})
|
||||
)
|
||||
|
||||
// Initialize CodeMirror for headers editor
|
||||
useCodemirror(
|
||||
headersEditor,
|
||||
jwtHeaders,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/json",
|
||||
readOnly: false,
|
||||
lineWrapping: true,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: true,
|
||||
})
|
||||
)
|
||||
|
||||
useCodemirror(
|
||||
privateKeyEditor,
|
||||
privateKey,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: false,
|
||||
lineWrapping: true,
|
||||
placeholder: `-----BEGIN PRIVATE KEY-----
|
||||
Your private key here
|
||||
-----END PRIVATE KEY-----`,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: true,
|
||||
})
|
||||
)
|
||||
|
||||
const algorithms: HoppRESTAuthJWT["algorithm"][] = ["HS256", "HS384", "HS512"]
|
||||
|
||||
const addToTargets = [
|
||||
{
|
||||
id: "HEADERS" as const,
|
||||
label: "Headers",
|
||||
},
|
||||
{
|
||||
id: "QUERY_PARAMS" as const,
|
||||
label: "Query Params",
|
||||
},
|
||||
]
|
||||
|
||||
const passBy = computed(() => {
|
||||
return (
|
||||
addToTargets.find((target) => target.id === auth.value.addTo)?.label ||
|
||||
t("state.none")
|
||||
)
|
||||
})
|
||||
|
||||
const isAsymmetricAlgorithm = computed(() => {
|
||||
return (
|
||||
auth.value.algorithm.startsWith("RS") ||
|
||||
auth.value.algorithm.startsWith("ES") ||
|
||||
auth.value.algorithm.startsWith("PS")
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
|
@ -29,13 +29,12 @@ import { toFormData } from "../functional/formData"
|
|||
import { tupleWithSameKeysToRecord } from "../functional/record"
|
||||
import { isJSONContentType } from "./contenttypes"
|
||||
import { stripComments } from "../editor/linting/jsonc"
|
||||
|
||||
import {
|
||||
DigestAuthParams,
|
||||
fetchInitialDigestAuthInfo,
|
||||
generateDigestAuthHeader,
|
||||
} from "../auth/digest"
|
||||
import { calculateHawkHeader } from "@hoppscotch/data"
|
||||
import { calculateHawkHeader, generateJWTToken } from "@hoppscotch/data"
|
||||
|
||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
/**
|
||||
|
|
@ -297,6 +296,35 @@ export const getComputedAuthHeaders = async (
|
|||
value: hawkHeader,
|
||||
description: "",
|
||||
})
|
||||
} else if (
|
||||
request.auth.authType === "jwt" &&
|
||||
request.auth.addTo === "HEADERS"
|
||||
) {
|
||||
const token = await generateJWTToken({
|
||||
algorithm: request.auth.algorithm || "HS256",
|
||||
secret: parseTemplateString(request.auth.secret, envVars, false),
|
||||
privateKey: parseTemplateString(request.auth.privateKey, envVars, false),
|
||||
payload: parseTemplateString(request.auth.payload, envVars, false),
|
||||
jwtHeaders: parseTemplateString(request.auth.jwtHeaders, envVars, false),
|
||||
isSecretBase64Encoded: request.auth.isSecretBase64Encoded,
|
||||
})
|
||||
|
||||
if (token) {
|
||||
// Get prefix (defaults to "Bearer " if not specified)
|
||||
const headerPrefix = parseTemplateString(
|
||||
request.auth.headerPrefix,
|
||||
envVars,
|
||||
false,
|
||||
showKeyIfSecret
|
||||
)
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `${headerPrefix}${token}`,
|
||||
description: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
|
|
@ -432,7 +460,8 @@ export const getComputedParams = async (
|
|||
if (
|
||||
req.auth.authType !== "api-key" &&
|
||||
req.auth.authType !== "oauth-2" &&
|
||||
req.auth.authType !== "aws-signature"
|
||||
req.auth.authType !== "aws-signature" &&
|
||||
req.auth.authType !== "jwt"
|
||||
)
|
||||
return []
|
||||
|
||||
|
|
@ -503,6 +532,36 @@ export const getComputedParams = async (
|
|||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (req.auth.authType === "jwt") {
|
||||
const token = await generateJWTToken({
|
||||
algorithm: req.auth.algorithm || "HS256",
|
||||
secret: parseTemplateString(req.auth.secret, envVars, false),
|
||||
privateKey: parseTemplateString(req.auth.privateKey, envVars, false),
|
||||
payload: parseTemplateString(req.auth.payload, envVars, false),
|
||||
jwtHeaders: parseTemplateString(req.auth.jwtHeaders, envVars, false),
|
||||
isSecretBase64Encoded: req.auth.isSecretBase64Encoded,
|
||||
})
|
||||
|
||||
if (token) {
|
||||
// Get param name (defaults to "token" if not specified)
|
||||
const paramName = parseTemplateString(req.auth.paramName, envVars)
|
||||
|
||||
return [
|
||||
{
|
||||
source: "auth",
|
||||
param: {
|
||||
active: true,
|
||||
key: paramName,
|
||||
value: token,
|
||||
description: "",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
|
|||
|
||||
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: "Echo",
|
||||
requests: [
|
||||
{
|
||||
|
|
@ -57,7 +57,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||
|
||||
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
|
||||
{
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: "Echo",
|
||||
requests: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ export class PersistenceService extends Service {
|
|||
const translatedData = result.data.map(translateToNewRESTCollection)
|
||||
setRESTCollections(translatedData)
|
||||
} else {
|
||||
console.error(`Failed with `, result.error, data)
|
||||
this.showErrorToast(STORE_KEYS.REST_COLLECTIONS)
|
||||
await Store.set(
|
||||
STORE_NAMESPACE,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"devDependencies": {
|
||||
"@types/lodash": "4.17.10",
|
||||
"@types/uuid": "10.0.0",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "5.4.9"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -45,8 +45,9 @@
|
|||
"io-ts": "2.2.21",
|
||||
"lodash": "4.17.21",
|
||||
"parser-ts": "0.7.0",
|
||||
"jose": "6.0.11",
|
||||
"uuid": "10.0.0",
|
||||
"verzod": "0.2.4",
|
||||
"zod": "3.23.8"
|
||||
"zod": "3.25.32"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import V4_VERSION from "./v/4"
|
|||
import V5_VERSION from "./v/5"
|
||||
import V6_VERSION from "./v/6"
|
||||
import V7_VERSION from "./v/7"
|
||||
import V8_VERSION from "./v/8"
|
||||
|
||||
import { z } from "zod"
|
||||
import { translateToNewRequest } from "../rest"
|
||||
|
|
@ -18,7 +19,7 @@ const versionedObject = z.object({
|
|||
})
|
||||
|
||||
export const HoppCollection = createVersionedEntity({
|
||||
latestVersion: 7,
|
||||
latestVersion: 8,
|
||||
versionMap: {
|
||||
1: V1_VERSION,
|
||||
2: V2_VERSION,
|
||||
|
|
@ -27,6 +28,7 @@ export const HoppCollection = createVersionedEntity({
|
|||
5: V5_VERSION,
|
||||
6: V6_VERSION,
|
||||
7: V7_VERSION,
|
||||
8: V8_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
const versionCheck = versionedObject.safeParse(data)
|
||||
|
|
@ -42,7 +44,7 @@ export const HoppCollection = createVersionedEntity({
|
|||
|
||||
export type HoppCollection = InferredEntity<typeof HoppCollection>
|
||||
|
||||
export const CollectionSchemaVersion = 7
|
||||
export const CollectionSchemaVersion = 8
|
||||
|
||||
/**
|
||||
* Generates a Collection object. This ignores the version number object
|
||||
|
|
|
|||
37
packages/hoppscotch-data/src/collection/v/8.ts
Normal file
37
packages/hoppscotch-data/src/collection/v/8.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { defineVersion } from "verzod"
|
||||
import { z } from "zod"
|
||||
|
||||
import { HoppGQLAuth } from "../../graphql/v/8"
|
||||
import { HoppRESTAuth } from "../../rest/v/13"
|
||||
|
||||
import { V7_SCHEMA, v7_baseCollectionSchema } from "./7"
|
||||
|
||||
export const v8_baseCollectionSchema = v7_baseCollectionSchema.extend({
|
||||
v: z.literal(8),
|
||||
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
|
||||
})
|
||||
|
||||
type Input = z.input<typeof v8_baseCollectionSchema> & {
|
||||
folders: Input[]
|
||||
}
|
||||
|
||||
type Output = z.output<typeof v8_baseCollectionSchema> & {
|
||||
folders: Output[]
|
||||
}
|
||||
|
||||
export const V8_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> =
|
||||
v8_baseCollectionSchema.extend({
|
||||
folders: z.lazy(() => z.array(V8_SCHEMA)),
|
||||
})
|
||||
|
||||
export default defineVersion({
|
||||
initial: false,
|
||||
schema: V8_SCHEMA,
|
||||
// @ts-expect-error
|
||||
up(old: z.infer<typeof V7_SCHEMA>) {
|
||||
return {
|
||||
...old,
|
||||
v: 8 as const,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -8,3 +8,4 @@ export * from "./predefinedVariables"
|
|||
export * from "./utils/collection"
|
||||
export * from "./utils/hawk"
|
||||
export * from "./utils/akamai-eg"
|
||||
export * from "./utils/jwt"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import V8_VERSION from "./v/8"
|
|||
import V9_VERSION from "./v/9"
|
||||
import V10_VERSION, { HoppRESTReqBody } from "./v/10"
|
||||
import V11_VERSION, { HoppRESTRequestResponses } from "./v/11"
|
||||
import V12_VERSION, { HoppRESTAuth } from "./v/12"
|
||||
import V12_VERSION from "./v/12"
|
||||
import V13_VERSION, { HoppRESTAuth } from "./v/13"
|
||||
|
||||
export * from "./content-types"
|
||||
|
||||
|
|
@ -51,17 +52,15 @@ export {
|
|||
HoppRESTResponseOriginalRequest,
|
||||
HoppRESTRequestResponse,
|
||||
HoppRESTRequestResponses,
|
||||
HoppRESTAuthOAuth2,
|
||||
ClientCredentialsGrantTypeParams,
|
||||
} from "./v/11"
|
||||
|
||||
export { HoppRESTReqBody } from "./v/10"
|
||||
|
||||
export { HoppRESTAuthOAuth2, ClientCredentialsGrantTypeParams } from "./v/11"
|
||||
export { HoppRESTAuthHAWK, HoppRESTAuthAkamaiEdgeGrid } from "./v/12"
|
||||
|
||||
export {
|
||||
HoppRESTAuthHAWK,
|
||||
HoppRESTAuthAkamaiEdgeGrid,
|
||||
HoppRESTAuth,
|
||||
} from "./v/12"
|
||||
export { HoppRESTAuth, HoppRESTAuthJWT } from "./v/13"
|
||||
|
||||
const versionedObject = z.object({
|
||||
// v is a stringified number
|
||||
|
|
@ -69,7 +68,7 @@ const versionedObject = z.object({
|
|||
})
|
||||
|
||||
export const HoppRESTRequest = createVersionedEntity({
|
||||
latestVersion: 12,
|
||||
latestVersion: 13,
|
||||
versionMap: {
|
||||
0: V0_VERSION,
|
||||
1: V1_VERSION,
|
||||
|
|
@ -84,6 +83,7 @@ export const HoppRESTRequest = createVersionedEntity({
|
|||
10: V10_VERSION,
|
||||
11: V11_VERSION,
|
||||
12: V12_VERSION,
|
||||
13: V13_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
// For V1 onwards we have the v string storing the number
|
||||
|
|
@ -126,7 +126,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
|
|||
responses: lodashIsEqualEq,
|
||||
})
|
||||
|
||||
export const RESTReqSchemaVersion = "12"
|
||||
export const RESTReqSchemaVersion = "13"
|
||||
|
||||
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
||||
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
||||
|
|
|
|||
83
packages/hoppscotch-data/src/rest/v/13.ts
Normal file
83
packages/hoppscotch-data/src/rest/v/13.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthInherit,
|
||||
HoppRESTAuthNone,
|
||||
} from "./1"
|
||||
import { HoppRESTAuthAPIKey } from "./4"
|
||||
import { HoppRESTAuthAWSSignature } from "./7"
|
||||
import { HoppRESTAuthDigest } from "./8"
|
||||
|
||||
import { HoppRESTAuthHAWK, HoppRESTAuthAkamaiEdgeGrid, V12_SCHEMA } from "./12"
|
||||
|
||||
import { z } from "zod"
|
||||
import { defineVersion } from "verzod"
|
||||
import { HoppRESTAuthOAuth2 } from "./11"
|
||||
|
||||
export const HoppRESTAuthJWT = z.object({
|
||||
authType: z.literal("jwt"),
|
||||
secret: z.string().catch(""),
|
||||
privateKey: z.string().catch(""), // For RSA/ECDSA algorithms
|
||||
algorithm: z
|
||||
.enum([
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
])
|
||||
.catch("HS256"),
|
||||
payload: z.string().catch("{}"),
|
||||
addTo: z.enum(["HEADERS", "QUERY_PARAMS"]).catch("HEADERS"),
|
||||
isSecretBase64Encoded: z.boolean().catch(false),
|
||||
headerPrefix: z.string().catch("Bearer "),
|
||||
paramName: z.string().catch("token"),
|
||||
jwtHeaders: z.string().catch("{}"),
|
||||
})
|
||||
|
||||
export type HoppRESTAuthJWT = z.infer<typeof HoppRESTAuthJWT>
|
||||
|
||||
export const HoppRESTAuth = z
|
||||
.discriminatedUnion("authType", [
|
||||
HoppRESTAuthNone,
|
||||
HoppRESTAuthInherit,
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthOAuth2,
|
||||
HoppRESTAuthAPIKey,
|
||||
HoppRESTAuthAWSSignature,
|
||||
HoppRESTAuthDigest,
|
||||
HoppRESTAuthHAWK,
|
||||
HoppRESTAuthAkamaiEdgeGrid,
|
||||
HoppRESTAuthJWT,
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
authActive: z.boolean(),
|
||||
})
|
||||
)
|
||||
|
||||
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
|
||||
|
||||
export const V13_SCHEMA = V12_SCHEMA.extend({
|
||||
v: z.literal("13"),
|
||||
auth: HoppRESTAuth,
|
||||
})
|
||||
|
||||
export default defineVersion({
|
||||
schema: V13_SCHEMA,
|
||||
initial: false,
|
||||
up(old: any) {
|
||||
return {
|
||||
...old,
|
||||
v: "13" as const,
|
||||
}
|
||||
},
|
||||
})
|
||||
99
packages/hoppscotch-data/src/utils/jwt.ts
Normal file
99
packages/hoppscotch-data/src/utils/jwt.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import * as jose from "jose"
|
||||
|
||||
export interface JWTTokenParams {
|
||||
algorithm: string
|
||||
secret: string
|
||||
privateKey: string
|
||||
payload: string
|
||||
jwtHeaders: string
|
||||
isSecretBase64Encoded: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JWT token using the provided parameters
|
||||
* @param params JWT token generation parameters with pre-parsed values
|
||||
* @returns Promise<string | null> - The generated JWT token or null if generation fails
|
||||
*/
|
||||
export async function generateJWTToken(
|
||||
params: JWTTokenParams
|
||||
): Promise<string | null> {
|
||||
const {
|
||||
algorithm,
|
||||
secret,
|
||||
privateKey,
|
||||
payload,
|
||||
jwtHeaders,
|
||||
isSecretBase64Encoded,
|
||||
} = params
|
||||
|
||||
// Parse the payload and headers from JSON strings
|
||||
let parsedPayload = {}
|
||||
let parsedHeaders = {}
|
||||
|
||||
// Safely parse payload JSON
|
||||
try {
|
||||
const payloadString = payload?.trim() || "{}"
|
||||
if (payloadString === "") {
|
||||
parsedPayload = {}
|
||||
} else {
|
||||
parsedPayload = JSON.parse(payloadString)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JWT payload JSON:", e)
|
||||
console.error("Payload value:", payload)
|
||||
return null
|
||||
}
|
||||
|
||||
// Safely parse headers JSON
|
||||
try {
|
||||
const headersString = jwtHeaders?.trim() || "{}"
|
||||
if (headersString === "") {
|
||||
parsedHeaders = {}
|
||||
} else {
|
||||
parsedHeaders = JSON.parse(headersString)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JWT headers JSON:", e)
|
||||
console.error("Headers value:", jwtHeaders)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
let cryptoKey: Uint8Array
|
||||
|
||||
// Use private key for RSA/ECDSA algorithms, secret for HMAC algorithms
|
||||
if (
|
||||
algorithm.startsWith("RS") ||
|
||||
algorithm.startsWith("ES") ||
|
||||
algorithm.startsWith("PS")
|
||||
) {
|
||||
// RSA or ECDSA algorithms - use private key
|
||||
if (!privateKey) {
|
||||
console.error("Private key is required for RSA/ECDSA algorithms")
|
||||
return null
|
||||
}
|
||||
cryptoKey = new TextEncoder().encode(privateKey)
|
||||
} else {
|
||||
// HMAC algorithms - use secret
|
||||
if (!secret) {
|
||||
console.error("Secret is required for HMAC algorithms")
|
||||
return null
|
||||
}
|
||||
cryptoKey = isSecretBase64Encoded
|
||||
? Uint8Array.from(Buffer.from(secret, "base64"))
|
||||
: new TextEncoder().encode(secret)
|
||||
}
|
||||
|
||||
const token = await new jose.SignJWT(parsedPayload)
|
||||
.setProtectedHeader({
|
||||
alg: algorithm,
|
||||
...parsedHeaders,
|
||||
})
|
||||
.sign(cryptoKey)
|
||||
|
||||
return token
|
||||
} catch (e) {
|
||||
console.error("Error generating JWT token:", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ function exportedCollectionToHoppCollection(
|
|||
|
||||
return {
|
||||
id: restCollection.id,
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -196,7 +196,7 @@ function exportedCollectionToHoppCollection(
|
|||
|
||||
return {
|
||||
id: gqlCollection.id,
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -374,7 +374,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
})
|
||||
|
|
@ -382,7 +382,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
})
|
||||
|
|
@ -607,7 +607,7 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
|
|
@ -1037,7 +1037,7 @@ function transformDuplicatedCollections(
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"verzod": "0.2.4",
|
||||
"vue": "3.5.12",
|
||||
"workbox-window": "7.1.0",
|
||||
"zod": "3.23.8"
|
||||
"zod": "3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "5.0.3",
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
"postcss": "8.4.47",
|
||||
"prettier-plugin-tailwindcss": "0.6.8",
|
||||
"tailwindcss": "3.4.13",
|
||||
"typescript": "5.3.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-fonts": "1.1.1",
|
||||
"unplugin-icons": "0.19.3",
|
||||
"unplugin-vue-components": "0.27.4",
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
|
|||
return {
|
||||
id: restCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection(
|
|||
return {
|
||||
id: gqlCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
|
|
@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
|
|
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
|
|
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
|
|||
folders,
|
||||
requests,
|
||||
_ref_id,
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ function exportedCollectionToHoppCollection(
|
|||
return {
|
||||
id: restCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: restCollection.name,
|
||||
folders: restCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -204,7 +204,7 @@ function exportedCollectionToHoppCollection(
|
|||
return {
|
||||
id: gqlCollection.id,
|
||||
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
|
||||
v: 7,
|
||||
v: 8,
|
||||
name: gqlCollection.name,
|
||||
folders: gqlCollection.folders.map((folder) =>
|
||||
exportedCollectionToHoppCollection(folder, collectionType)
|
||||
|
|
@ -383,7 +383,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
|
|
@ -392,7 +392,7 @@ function setupUserCollectionCreatedSubscription() {
|
|||
name: res.right.userCollectionCreated.title,
|
||||
folders: [],
|
||||
requests: [],
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id: data._ref_id,
|
||||
auth: data.auth,
|
||||
headers: addDescriptionField(data.headers),
|
||||
|
|
@ -620,7 +620,7 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
name,
|
||||
folders,
|
||||
requests,
|
||||
v: 7,
|
||||
v: 8,
|
||||
_ref_id,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
|
|
@ -1054,7 +1054,7 @@ function transformDuplicatedCollections(
|
|||
folders,
|
||||
requests,
|
||||
_ref_id,
|
||||
v: 7,
|
||||
v: 8,
|
||||
auth,
|
||||
headers: addDescriptionField(headers),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
"npm-run-all": "4.1.5",
|
||||
"sass": "1.80.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-fonts": "1.1.1",
|
||||
"vite": "5.4.9",
|
||||
"vite-plugin-pages": "0.32.3",
|
||||
|
|
|
|||
1153
pnpm-lock.yaml
1153
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue