feat(common): support advanced parameters in OAuth grant types (#5287)
Adds support for advanced parameters in `implicit`, `password`, and `client_credentials` grant types.
This commit is contained in:
parent
6f942a7c30
commit
1df781ec0a
9 changed files with 1677 additions and 1397 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,301 @@
|
|||
import { HoppGQLAuthOAuth2, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
|
||||
import { Ref, ref, watch } from "vue"
|
||||
|
||||
export type AuthRequestParam = {
|
||||
id: number
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
sendIn?: "headers" | "url" | "body"
|
||||
}
|
||||
|
||||
export type TokenRequestParam = {
|
||||
id: number
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
sendIn: "headers" | "url" | "body"
|
||||
}
|
||||
|
||||
let paramsIdCounter = 1000
|
||||
|
||||
export const useOAuth2AdvancedParams = (
|
||||
auth: Ref<HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2>
|
||||
) => {
|
||||
// Auth Request Parameters
|
||||
const workingAuthRequestParams = ref<AuthRequestParam[]>([
|
||||
{ id: paramsIdCounter++, key: "", value: "", active: true },
|
||||
])
|
||||
|
||||
// Token Request Parameters
|
||||
const workingTokenRequestParams = ref<TokenRequestParam[]>([
|
||||
{
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Refresh Request Parameters
|
||||
const workingRefreshRequestParams = ref<TokenRequestParam[]>([
|
||||
{
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Watch for changes in working auth request params
|
||||
watch(
|
||||
workingAuthRequestParams,
|
||||
(newParams: AuthRequestParam[]) => {
|
||||
// Auto-add empty row when the last row is filled
|
||||
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
|
||||
workingAuthRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Update auth.value.grantTypeInfo with non-empty params
|
||||
const nonEmptyParams = newParams.filter(
|
||||
(p: AuthRequestParam) => p.key !== "" || p.value !== ""
|
||||
)
|
||||
|
||||
if ("authRequestParams" in auth.value.grantTypeInfo) {
|
||||
auth.value.grantTypeInfo.authRequestParams = nonEmptyParams.map(
|
||||
(param) => ({
|
||||
id: param.id,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
active: param.active,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Watch for changes in working token request params
|
||||
watch(
|
||||
workingTokenRequestParams,
|
||||
(newParams: TokenRequestParam[]) => {
|
||||
// Auto-add empty row when the last row is filled
|
||||
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
|
||||
workingTokenRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Update auth.value.grantTypeInfo with non-empty params
|
||||
const nonEmptyParams = newParams.filter(
|
||||
(p: TokenRequestParam) => p.key !== "" || p.value !== ""
|
||||
)
|
||||
|
||||
if ("tokenRequestParams" in auth.value.grantTypeInfo) {
|
||||
auth.value.grantTypeInfo.tokenRequestParams = nonEmptyParams.map(
|
||||
(param) => ({
|
||||
id: param.id,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
sendIn: param.sendIn,
|
||||
active: param.active,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Watch for changes in working refresh request params
|
||||
watch(
|
||||
workingRefreshRequestParams,
|
||||
(newParams: TokenRequestParam[]) => {
|
||||
// Auto-add empty row when the last row is filled
|
||||
if (newParams.length > 0 && newParams[newParams.length - 1].key !== "") {
|
||||
workingRefreshRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Update auth.value.grantTypeInfo with non-empty params
|
||||
const nonEmptyParams = newParams.filter(
|
||||
(p: TokenRequestParam) => p.key !== "" || p.value !== ""
|
||||
)
|
||||
|
||||
if ("refreshRequestParams" in auth.value.grantTypeInfo) {
|
||||
auth.value.grantTypeInfo.refreshRequestParams = nonEmptyParams.map(
|
||||
(param) => ({
|
||||
id: param.id,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
sendIn: param.sendIn,
|
||||
active: param.active,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Functions for auth request params management
|
||||
const addAuthRequestParam = () => {
|
||||
workingAuthRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateAuthRequestParam = (index: number, payload: AuthRequestParam) => {
|
||||
workingAuthRequestParams.value[index] = payload
|
||||
}
|
||||
|
||||
const deleteAuthRequestParam = (index: number) => {
|
||||
if (workingAuthRequestParams.value.length > 1) {
|
||||
workingAuthRequestParams.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for token request params management
|
||||
const addTokenRequestParam = () => {
|
||||
workingTokenRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateTokenRequestParam = (
|
||||
index: number,
|
||||
payload: TokenRequestParam
|
||||
) => {
|
||||
workingTokenRequestParams.value[index] = payload
|
||||
}
|
||||
|
||||
const deleteTokenRequestParam = (index: number) => {
|
||||
if (workingTokenRequestParams.value.length > 1) {
|
||||
workingTokenRequestParams.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for refresh request params management
|
||||
const addRefreshRequestParam = () => {
|
||||
workingRefreshRequestParams.value.push({
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateRefreshRequestParam = (
|
||||
index: number,
|
||||
payload: TokenRequestParam
|
||||
) => {
|
||||
workingRefreshRequestParams.value[index] = payload
|
||||
}
|
||||
|
||||
const deleteRefreshRequestParam = (index: number) => {
|
||||
if (workingRefreshRequestParams.value.length > 1) {
|
||||
workingRefreshRequestParams.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize advanced parameters from the auth object when component mounts
|
||||
const initializeParams = () => {
|
||||
if (
|
||||
"authRequestParams" in auth.value.grantTypeInfo &&
|
||||
auth.value.grantTypeInfo.authRequestParams &&
|
||||
auth.value.grantTypeInfo.authRequestParams.length > 0
|
||||
) {
|
||||
workingAuthRequestParams.value =
|
||||
auth.value.grantTypeInfo.authRequestParams.map((param) => ({
|
||||
id: param.id || paramsIdCounter++,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
active: param.active,
|
||||
}))
|
||||
}
|
||||
|
||||
if (
|
||||
"tokenRequestParams" in auth.value.grantTypeInfo &&
|
||||
auth.value.grantTypeInfo.tokenRequestParams &&
|
||||
auth.value.grantTypeInfo.tokenRequestParams.length > 0
|
||||
) {
|
||||
workingTokenRequestParams.value = [
|
||||
...auth.value.grantTypeInfo.tokenRequestParams.map((param) => ({
|
||||
id: param.id || paramsIdCounter++,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
sendIn: param.sendIn || "body",
|
||||
active: param.active,
|
||||
})),
|
||||
{
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (
|
||||
"refreshRequestParams" in auth.value.grantTypeInfo &&
|
||||
auth.value.grantTypeInfo.refreshRequestParams &&
|
||||
auth.value.grantTypeInfo.refreshRequestParams.length > 0
|
||||
) {
|
||||
workingRefreshRequestParams.value = [
|
||||
...auth.value.grantTypeInfo.refreshRequestParams.map((param) => ({
|
||||
id: param.id || paramsIdCounter++,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
sendIn: param.sendIn || "body",
|
||||
active: param.active,
|
||||
})),
|
||||
{
|
||||
id: paramsIdCounter++,
|
||||
key: "",
|
||||
value: "",
|
||||
sendIn: "body",
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
workingAuthRequestParams,
|
||||
workingTokenRequestParams,
|
||||
workingRefreshRequestParams,
|
||||
addAuthRequestParam,
|
||||
updateAuthRequestParam,
|
||||
deleteAuthRequestParam,
|
||||
addTokenRequestParam,
|
||||
updateTokenRequestParam,
|
||||
deleteTokenRequestParam,
|
||||
addRefreshRequestParam,
|
||||
updateRefreshRequestParam,
|
||||
deleteRefreshRequestParam,
|
||||
initializeParams,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,824 @@
|
|||
import { HoppGQLAuthOAuth2, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { Ref, computed } from "vue"
|
||||
import { z } from "zod"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { refWithCallbackOnChange } from "~/composables/ref"
|
||||
import {
|
||||
replaceTemplateString,
|
||||
replaceTemplateStringsInObjectValues,
|
||||
} from "~/helpers/auth"
|
||||
import authCode, {
|
||||
AuthCodeOauthFlowParams,
|
||||
AuthCodeOauthRefreshParams,
|
||||
getDefaultAuthCodeOauthFlowParams,
|
||||
} from "~/services/oauth/flows/authCode"
|
||||
import clientCredentials, {
|
||||
ClientCredentialsFlowParams,
|
||||
getDefaultClientCredentialsFlowParams,
|
||||
} from "~/services/oauth/flows/clientCredentials"
|
||||
import implicit, {
|
||||
ImplicitOauthFlowParams,
|
||||
getDefaultImplicitOauthFlowParams,
|
||||
} from "~/services/oauth/flows/implicit"
|
||||
import passwordFlow, {
|
||||
PasswordFlowParams,
|
||||
getDefaultPasswordFlowParams,
|
||||
} from "~/services/oauth/flows/password"
|
||||
import { AuthRequestParam, TokenRequestParam } from "./useOAuth2AdvancedParams"
|
||||
|
||||
export type GrantTypes = z.infer<
|
||||
typeof HoppRESTAuthOAuth2
|
||||
>["grantTypeInfo"]["grantType"]
|
||||
|
||||
export const useOAuth2GrantTypes = (
|
||||
auth: Ref<HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2>,
|
||||
setAccessTokenInActiveContext: (
|
||||
accessToken?: string,
|
||||
refreshToken?: string
|
||||
) => void,
|
||||
workingAuthRequestParams: Ref<AuthRequestParam[]>,
|
||||
workingTokenRequestParams: Ref<TokenRequestParam[]>,
|
||||
workingRefreshRequestParams: Ref<TokenRequestParam[]>,
|
||||
pkceTippyActions: Ref<HTMLElement | null>,
|
||||
clientAuthenticationTippyActions: Ref<HTMLElement | null>
|
||||
) => {
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// Helper function to prepare request parameters
|
||||
const prepareRequestParams = (
|
||||
params: Ref<AuthRequestParam[] | TokenRequestParam[]>
|
||||
) => {
|
||||
return params.value
|
||||
.filter((p) => p.active && p.key && p.value)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
key: replaceTemplateString(p.key),
|
||||
value: replaceTemplateString(p.value),
|
||||
active: p.active,
|
||||
sendIn: p.sendIn || "body",
|
||||
}))
|
||||
}
|
||||
|
||||
const preparedAuthRequestParams = computed(() => {
|
||||
return prepareRequestParams(workingAuthRequestParams)
|
||||
})
|
||||
|
||||
const preparedTokenRequestParams = computed(() => {
|
||||
return prepareRequestParams(workingTokenRequestParams)
|
||||
})
|
||||
|
||||
const preparedRefreshRequestParams = computed(() => {
|
||||
return prepareRequestParams(workingRefreshRequestParams)
|
||||
})
|
||||
|
||||
const grantTypeMap: Record<
|
||||
GrantTypes,
|
||||
"authCode" | "clientCredentials" | "password" | "implicit"
|
||||
> = {
|
||||
AUTHORIZATION_CODE: "authCode",
|
||||
CLIENT_CREDENTIALS: "clientCredentials",
|
||||
IMPLICIT: "implicit",
|
||||
PASSWORD: "password",
|
||||
} as const
|
||||
|
||||
const grantTypeDefaultPayload = {
|
||||
AUTHORIZATION_CODE: getDefaultAuthCodeOauthFlowParams,
|
||||
CLIENT_CREDENTIALS: getDefaultClientCredentialsFlowParams,
|
||||
IMPLICIT: getDefaultImplicitOauthFlowParams,
|
||||
PASSWORD: getDefaultPasswordFlowParams,
|
||||
} as const
|
||||
|
||||
const supportedGrantTypes = [
|
||||
{
|
||||
id: "authCode" as const,
|
||||
label: t("authorization.oauth.label_auth_code"),
|
||||
formElements: computed(() => {
|
||||
if (!(auth.value.grantTypeInfo.grantType === "AUTHORIZATION_CODE")) {
|
||||
return
|
||||
}
|
||||
|
||||
const grantType = auth.value.grantTypeInfo
|
||||
|
||||
const authEndpoint = refWithCallbackOnChange(
|
||||
grantType?.authEndpoint,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
authEndpoint: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const tokenEndpoint = refWithCallbackOnChange(
|
||||
grantType?.tokenEndpoint,
|
||||
(value) => {
|
||||
if (!("tokenEndpoint" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
tokenEndpoint: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientID = refWithCallbackOnChange(
|
||||
grantType?.clientID,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientID: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientSecret = refWithCallbackOnChange(
|
||||
grantType?.clientSecret,
|
||||
(value) => {
|
||||
if (!("clientSecret" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientSecret: value ?? "",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const scopes = refWithCallbackOnChange(
|
||||
grantType?.scopes ? grantType.scopes : undefined,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
scopes: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const isPKCE = refWithCallbackOnChange(
|
||||
auth.value.grantTypeInfo.isPKCE,
|
||||
(value) => {
|
||||
if (!("isPKCE" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
isPKCE: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const codeChallenge: Ref<{
|
||||
id: "plain" | "S256"
|
||||
label: string
|
||||
} | null> = refWithCallbackOnChange(
|
||||
auth.value.grantTypeInfo.codeVerifierMethod
|
||||
? {
|
||||
id: auth.value.grantTypeInfo.codeVerifierMethod,
|
||||
label:
|
||||
auth.value.grantTypeInfo.codeVerifierMethod === "plain"
|
||||
? "Plain"
|
||||
: "SHA-256",
|
||||
}
|
||||
: null,
|
||||
(value) => {
|
||||
if (!("codeVerifierMethod" in auth.value.grantTypeInfo) || !value) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
codeVerifierMethod: value.id,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const refreshToken = async () => {
|
||||
const grantTypeInfo = auth.value.grantTypeInfo
|
||||
|
||||
if (!("refreshToken" in grantTypeInfo)) {
|
||||
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
|
||||
}
|
||||
|
||||
const refreshToken = grantTypeInfo.refreshToken
|
||||
|
||||
if (!refreshToken) {
|
||||
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
|
||||
}
|
||||
|
||||
const params: AuthCodeOauthRefreshParams = {
|
||||
clientID: clientID.value,
|
||||
clientSecret: clientSecret.value,
|
||||
tokenEndpoint: tokenEndpoint.value,
|
||||
refreshToken,
|
||||
}
|
||||
|
||||
const unwrappedParams = replaceTemplateStringsInObjectValues(params)
|
||||
|
||||
const refreshTokenFunc = authCode.refreshToken
|
||||
|
||||
if (!refreshTokenFunc) {
|
||||
return E.left("REFRESH_TOKEN_FUNCTION_NOT_DEFINED" as const)
|
||||
}
|
||||
|
||||
const res = await refreshTokenFunc(unwrappedParams)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("OAUTH_REFRESH_TOKEN_FAILED" as const)
|
||||
}
|
||||
|
||||
setAccessTokenInActiveContext(
|
||||
res.right.access_token,
|
||||
res.right.refresh_token
|
||||
)
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
const runAction = async () => {
|
||||
const params: AuthCodeOauthFlowParams = {
|
||||
authEndpoint: authEndpoint.value,
|
||||
tokenEndpoint: tokenEndpoint.value,
|
||||
clientID: clientID.value,
|
||||
clientSecret: clientSecret.value,
|
||||
scopes: scopes.value,
|
||||
isPKCE: isPKCE.value,
|
||||
codeVerifierMethod: codeChallenge.value?.id,
|
||||
authRequestParams: preparedAuthRequestParams.value,
|
||||
tokenRequestParams: preparedTokenRequestParams.value,
|
||||
refreshRequestParams: preparedRefreshRequestParams.value,
|
||||
}
|
||||
|
||||
const unwrappedParams = replaceTemplateStringsInObjectValues(params)
|
||||
|
||||
const parsedArgs = authCode.params.safeParse(unwrappedParams)
|
||||
|
||||
if (!parsedArgs.success) {
|
||||
return E.left("VALIDATION_FAILED" as const)
|
||||
}
|
||||
|
||||
const res = await authCode.init(parsedArgs.data)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return res
|
||||
}
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
const pkceElements = computed(() => {
|
||||
const checkbox = {
|
||||
id: "isPKCE",
|
||||
label: t("authorization.oauth.label_use_pkce"),
|
||||
type: "checkbox" as const,
|
||||
ref: isPKCE,
|
||||
onChange: (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
isPKCE.value = target.checked
|
||||
},
|
||||
}
|
||||
|
||||
return isPKCE.value
|
||||
? [
|
||||
checkbox,
|
||||
{
|
||||
id: "codeChallenge",
|
||||
label: t("authorization.oauth.label_code_challenge"),
|
||||
type: "dropdown" as const,
|
||||
ref: codeChallenge,
|
||||
tippyRefName: "pkceTippyActions",
|
||||
tippyRef: pkceTippyActions,
|
||||
options: [
|
||||
{
|
||||
id: "plain" as const,
|
||||
label: "Plain",
|
||||
},
|
||||
{
|
||||
id: "S256" as const,
|
||||
label: "SHA-256",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [checkbox]
|
||||
})
|
||||
|
||||
const elements = computed(() => {
|
||||
return [
|
||||
...pkceElements.value,
|
||||
{
|
||||
id: "authEndpoint",
|
||||
label: t("authorization.oauth.label_authorization_endpoint"),
|
||||
type: "text" as const,
|
||||
ref: authEndpoint,
|
||||
},
|
||||
{
|
||||
id: "tokenEndpoint",
|
||||
label: t("authorization.oauth.label_token_endpoint"),
|
||||
type: "text" as const,
|
||||
ref: tokenEndpoint,
|
||||
},
|
||||
{
|
||||
id: "clientId",
|
||||
label: t("authorization.oauth.label_client_id"),
|
||||
type: "text" as const,
|
||||
ref: clientID,
|
||||
},
|
||||
{
|
||||
id: "clientSecret",
|
||||
label: t("authorization.oauth.label_client_secret"),
|
||||
type: "text" as const,
|
||||
ref: clientSecret,
|
||||
},
|
||||
{
|
||||
id: "scopes",
|
||||
label: t("authorization.oauth.label_scopes"),
|
||||
type: "text" as const,
|
||||
ref: scopes,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
runAction,
|
||||
refreshToken,
|
||||
elements,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "clientCredentials" as const,
|
||||
label: t("authorization.oauth.label_client_credentials"),
|
||||
formElements: computed(() => {
|
||||
if (!(auth.value.grantTypeInfo.grantType === "CLIENT_CREDENTIALS")) {
|
||||
return
|
||||
}
|
||||
|
||||
const grantTypeInfo = auth.value.grantTypeInfo
|
||||
|
||||
const authEndpoint = refWithCallbackOnChange(
|
||||
grantTypeInfo?.authEndpoint,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
authEndpoint: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientID = refWithCallbackOnChange(
|
||||
grantTypeInfo?.clientID,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientID: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientSecret = refWithCallbackOnChange(
|
||||
grantTypeInfo?.clientSecret,
|
||||
(value) => {
|
||||
if (!("clientSecret" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientSecret: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const scopes = refWithCallbackOnChange(
|
||||
grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
scopes: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientAuthentication = refWithCallbackOnChange(
|
||||
grantTypeInfo.clientAuthentication
|
||||
? grantTypeInfo.clientAuthentication === "AS_BASIC_AUTH_HEADERS"
|
||||
? {
|
||||
id: "AS_BASIC_AUTH_HEADERS" as const,
|
||||
label: t("authorization.oauth.label_send_as_basic_auth"),
|
||||
}
|
||||
: {
|
||||
id: "IN_BODY" as const,
|
||||
label: t("authorization.oauth.label_send_in_body"),
|
||||
}
|
||||
: {
|
||||
id: "IN_BODY" as const,
|
||||
label: t("authorization.oauth.label_send_in_body"),
|
||||
},
|
||||
(value) => {
|
||||
if (!("clientAuthentication" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientAuthentication: value.id,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const runAction = async () => {
|
||||
const values: ClientCredentialsFlowParams =
|
||||
replaceTemplateStringsInObjectValues({
|
||||
authEndpoint: authEndpoint.value,
|
||||
clientID: clientID.value,
|
||||
clientSecret: clientSecret.value,
|
||||
scopes: scopes.value,
|
||||
clientAuthentication: clientAuthentication.value.id,
|
||||
tokenRequestParams: preparedTokenRequestParams.value,
|
||||
refreshRequestParams: preparedRefreshRequestParams.value,
|
||||
})
|
||||
|
||||
const parsedArgs = clientCredentials.params.safeParse(values)
|
||||
|
||||
if (!parsedArgs.success) {
|
||||
return E.left("VALIDATION_FAILED" as const)
|
||||
}
|
||||
|
||||
const res = await clientCredentials.init(parsedArgs.data)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("OAUTH_TOKEN_FETCH_FAILED" as const)
|
||||
}
|
||||
|
||||
setAccessTokenInActiveContext(res.right?.access_token)
|
||||
|
||||
toast.success(t("authorization.oauth.token_fetched_successfully"))
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
const elements = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: "authEndpoint",
|
||||
label: t("authorization.oauth.label_authorization_endpoint"),
|
||||
type: "text" as const,
|
||||
ref: authEndpoint,
|
||||
},
|
||||
{
|
||||
id: "clientId",
|
||||
label: t("authorization.oauth.label_client_id"),
|
||||
type: "text" as const,
|
||||
ref: clientID,
|
||||
},
|
||||
{
|
||||
id: "clientSecret",
|
||||
label: t("authorization.oauth.label_client_secret"),
|
||||
type: "text" as const,
|
||||
ref: clientSecret,
|
||||
},
|
||||
{
|
||||
id: "scopes",
|
||||
label: t("authorization.oauth.label_scopes"),
|
||||
type: "text" as const,
|
||||
ref: scopes,
|
||||
},
|
||||
{
|
||||
id: "clientAuthentication",
|
||||
label: t("authorization.oauth.label_send_as"),
|
||||
type: "dropdown" as const,
|
||||
ref: clientAuthentication,
|
||||
tippyRefName: "clientAuthenticationTippyActions",
|
||||
tippyRef: clientAuthenticationTippyActions,
|
||||
options: [
|
||||
{
|
||||
id: "IN_BODY" as const,
|
||||
label: t("authorization.oauth.label_send_in_body"),
|
||||
},
|
||||
{
|
||||
id: "AS_BASIC_AUTH_HEADERS" as const,
|
||||
label: t("authorization.oauth.label_send_as_basic_auth"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
runAction,
|
||||
elements,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "password" as const,
|
||||
label: "Password",
|
||||
formElements: computed(() => {
|
||||
if (!(auth.value.grantTypeInfo.grantType === "PASSWORD")) {
|
||||
return
|
||||
}
|
||||
|
||||
const grantTypeInfo = auth.value.grantTypeInfo
|
||||
|
||||
const authEndpoint = refWithCallbackOnChange(
|
||||
grantTypeInfo?.authEndpoint,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
authEndpoint: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientID = refWithCallbackOnChange(
|
||||
grantTypeInfo?.clientID,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientID: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientSecret = refWithCallbackOnChange(
|
||||
grantTypeInfo?.clientSecret,
|
||||
(value) => {
|
||||
if (!("clientSecret" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientSecret: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const scopes = refWithCallbackOnChange(
|
||||
grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo.scopes = value
|
||||
}
|
||||
)
|
||||
|
||||
const username = refWithCallbackOnChange(
|
||||
grantTypeInfo?.username,
|
||||
(value) => {
|
||||
if (!("username" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
username: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const password = refWithCallbackOnChange(
|
||||
grantTypeInfo?.password,
|
||||
(value) => {
|
||||
if (!("password" in auth.value.grantTypeInfo)) {
|
||||
return
|
||||
}
|
||||
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
password: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const runAction = async () => {
|
||||
const values: PasswordFlowParams =
|
||||
replaceTemplateStringsInObjectValues({
|
||||
authEndpoint: authEndpoint.value,
|
||||
clientID: clientID.value,
|
||||
clientSecret: clientSecret.value,
|
||||
scopes: scopes.value,
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
tokenRequestParams: preparedTokenRequestParams.value,
|
||||
refreshRequestParams: preparedRefreshRequestParams.value,
|
||||
})
|
||||
|
||||
const parsedArgs = passwordFlow.params.safeParse(values)
|
||||
|
||||
if (!parsedArgs.success) {
|
||||
return E.left("VALIDATION_FAILED" as const)
|
||||
}
|
||||
|
||||
const res = await passwordFlow.init(parsedArgs.data)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("OAUTH_TOKEN_FETCH_FAILED" as const)
|
||||
}
|
||||
|
||||
setAccessTokenInActiveContext(res.right?.access_token)
|
||||
|
||||
toast.success(t("authorization.oauth.token_fetched_successfully"))
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
const elements = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: "authEndpoint",
|
||||
label: t("authorization.oauth.label_authorization_endpoint"),
|
||||
type: "text" as const,
|
||||
ref: authEndpoint,
|
||||
},
|
||||
{
|
||||
id: "clientId",
|
||||
label: t("authorization.oauth.label_client_id"),
|
||||
type: "text" as const,
|
||||
ref: clientID,
|
||||
},
|
||||
{
|
||||
id: "clientSecret",
|
||||
label: t("authorization.oauth.label_client_secret"),
|
||||
type: "text" as const,
|
||||
ref: clientSecret,
|
||||
},
|
||||
{
|
||||
id: "username",
|
||||
label: t("authorization.oauth.label_username"),
|
||||
type: "text" as const,
|
||||
ref: username,
|
||||
},
|
||||
{
|
||||
id: "password",
|
||||
label: t("authorization.oauth.label_password"),
|
||||
type: "text" as const,
|
||||
ref: password,
|
||||
},
|
||||
{
|
||||
id: "scopes",
|
||||
label: t("authorization.oauth.label_scopes"),
|
||||
type: "text" as const,
|
||||
ref: scopes,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
runAction,
|
||||
elements,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "implicit" as const,
|
||||
label: t("authorization.oauth.label_implicit"),
|
||||
formElements: computed(() => {
|
||||
const grantTypeInfo = auth.value.grantTypeInfo
|
||||
|
||||
const authEndpoint = refWithCallbackOnChange(
|
||||
grantTypeInfo?.authEndpoint,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
authEndpoint: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const clientID = refWithCallbackOnChange(
|
||||
grantTypeInfo?.clientID,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
clientID: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const scopes = refWithCallbackOnChange(
|
||||
grantTypeInfo?.scopes ? grantTypeInfo.scopes : undefined,
|
||||
(value) => {
|
||||
auth.value.grantTypeInfo = {
|
||||
...auth.value.grantTypeInfo,
|
||||
scopes: value,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const runAction = () => {
|
||||
const values: ImplicitOauthFlowParams =
|
||||
replaceTemplateStringsInObjectValues({
|
||||
authEndpoint: authEndpoint.value,
|
||||
clientID: clientID.value,
|
||||
scopes: scopes.value,
|
||||
authRequestParams: preparedAuthRequestParams.value,
|
||||
refreshRequestParams: preparedRefreshRequestParams.value,
|
||||
})
|
||||
|
||||
const unwrappedValues = replaceTemplateStringsInObjectValues(values)
|
||||
|
||||
const parsedArgs = implicit.params.safeParse(unwrappedValues)
|
||||
|
||||
if (!parsedArgs.success) {
|
||||
return E.left("VALIDATION_FAILED" as const)
|
||||
}
|
||||
|
||||
implicit.init(parsedArgs.data)
|
||||
|
||||
return E.right(undefined)
|
||||
}
|
||||
|
||||
const elements = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: "authEndpoint",
|
||||
label: t("authorization.oauth.label_authorization_endpoint"),
|
||||
type: "text" as const,
|
||||
ref: authEndpoint,
|
||||
},
|
||||
{
|
||||
id: "clientId",
|
||||
label: t("authorization.oauth.label_client_id"),
|
||||
type: "text" as const,
|
||||
ref: clientID,
|
||||
},
|
||||
{
|
||||
id: "scopes",
|
||||
label: t("authorization.oauth.label_scopes"),
|
||||
type: "text" as const,
|
||||
ref: scopes,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
runAction,
|
||||
elements,
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
const selectedGrantTypeID = computed(() => {
|
||||
const currentGrantType = auth.value.grantTypeInfo.grantType
|
||||
return grantTypeMap[currentGrantType]
|
||||
})
|
||||
|
||||
const selectedGrantType = computed(() => {
|
||||
return supportedGrantTypes.find(
|
||||
(grantType) => grantType.id === selectedGrantTypeID.value
|
||||
)
|
||||
})
|
||||
|
||||
const changeSelectedGrantType = (
|
||||
grantType: (typeof supportedGrantTypes)[number]["id"]
|
||||
) => {
|
||||
const keys = Object.keys(grantTypeMap) as GrantTypes[]
|
||||
|
||||
const grantTypeToSet = keys.find((key) => grantTypeMap[key] === grantType)
|
||||
|
||||
if (grantTypeToSet) {
|
||||
auth.value.grantTypeInfo.grantType = grantTypeToSet
|
||||
|
||||
const getDefaultPayload = grantTypeDefaultPayload[grantTypeToSet]
|
||||
|
||||
if (getDefaultPayload) {
|
||||
auth.value.grantTypeInfo = {
|
||||
...getDefaultPayload(),
|
||||
...auth.value.grantTypeInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runAction = computed(() => {
|
||||
return selectedGrantType.value?.formElements.value?.runAction
|
||||
})
|
||||
|
||||
const runTokenRefresh = computed(() => {
|
||||
if (selectedGrantType.value?.id === "authCode") {
|
||||
return selectedGrantType.value?.formElements.value?.refreshToken
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const currentOAuthGrantTypeFormElements = computed(() => {
|
||||
return selectedGrantType.value?.formElements.value?.elements.value
|
||||
})
|
||||
|
||||
return {
|
||||
supportedGrantTypes,
|
||||
selectedGrantTypeID,
|
||||
selectedGrantType,
|
||||
changeSelectedGrantType,
|
||||
runAction,
|
||||
runTokenRefresh,
|
||||
currentOAuthGrantTypeFormElements,
|
||||
}
|
||||
}
|
||||
|
|
@ -43,17 +43,9 @@ export const commonOAuth2RefreshParams = [
|
|||
"resource",
|
||||
]
|
||||
|
||||
export const sendInOptions = [
|
||||
{
|
||||
label: "Request Body",
|
||||
value: "body",
|
||||
},
|
||||
{
|
||||
label: "Request URL",
|
||||
value: "url",
|
||||
},
|
||||
{
|
||||
label: "Request Headers",
|
||||
value: "headers",
|
||||
},
|
||||
]
|
||||
export const sendInOptions = ["body", "url", "headers"] as const
|
||||
export const sendInOptionsLabels = {
|
||||
body: "Request Body",
|
||||
url: "Request URL",
|
||||
headers: "Request Headers",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getService } from "~/modules/dioc"
|
|||
import * as E from "fp-ts/Either"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import { content } from "@hoppscotch/kernel"
|
||||
import { refreshToken, OAuth2ParamSchema } from "../utils"
|
||||
|
||||
const persistenceService = getService(PersistenceService)
|
||||
const interceptorService = getService(KernelInterceptorService)
|
||||
|
|
@ -25,31 +26,12 @@ const AuthCodeOauthFlowParamsSchema = z
|
|||
isPKCE: z.boolean(),
|
||||
codeVerifierMethod: z.enum(["plain", "S256"]).optional(),
|
||||
authRequestParams: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
active: z.boolean(),
|
||||
})
|
||||
),
|
||||
refreshRequestParams: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
active: z.boolean(),
|
||||
sendIn: z.enum(["headers", "url", "body"]).optional(),
|
||||
})
|
||||
),
|
||||
tokenRequestParams: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
active: z.boolean(),
|
||||
sendIn: z.enum(["headers", "url", "body"]).optional(),
|
||||
OAuth2ParamSchema.omit({
|
||||
sendIn: true,
|
||||
})
|
||||
),
|
||||
refreshRequestParams: z.array(OAuth2ParamSchema),
|
||||
tokenRequestParams: z.array(OAuth2ParamSchema),
|
||||
})
|
||||
.refine(
|
||||
(params) => {
|
||||
|
|
@ -354,57 +336,6 @@ const encodeArrayBufferAsUrlEncodedBase64 = (buffer: ArrayBuffer) => {
|
|||
return hashBase64URL
|
||||
}
|
||||
|
||||
const refreshToken = async ({
|
||||
tokenEndpoint,
|
||||
clientID,
|
||||
refreshToken,
|
||||
clientSecret,
|
||||
}: AuthCodeOauthRefreshParams) => {
|
||||
const { response } = interceptorService.execute({
|
||||
id: Date.now(),
|
||||
url: tokenEndpoint,
|
||||
method: "POST",
|
||||
version: "HTTP/1.1",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
content: content.urlencoded({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientID,
|
||||
...(clientSecret && {
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const res = await response
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
|
||||
}
|
||||
|
||||
const responsePayload = decodeResponseAsJSON(res.right)
|
||||
|
||||
if (E.isLeft(responsePayload)) {
|
||||
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
|
||||
}
|
||||
|
||||
const withAccessTokenAndRefreshTokenSchema = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string().optional(),
|
||||
})
|
||||
|
||||
const parsedTokenResponse = withAccessTokenAndRefreshTokenSchema.safeParse(
|
||||
responsePayload.right
|
||||
)
|
||||
|
||||
return parsedTokenResponse.success
|
||||
? E.right(parsedTokenResponse.data)
|
||||
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
|
||||
}
|
||||
|
||||
export default createFlowConfig(
|
||||
"AUTHORIZATION_CODE" as const,
|
||||
AuthCodeOauthFlowParamsSchema,
|
||||
|
|
|
|||
|
|
@ -8,33 +8,35 @@ import { getService } from "~/modules/dioc"
|
|||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { ClientCredentialsGrantTypeParams } from "@hoppscotch/data"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import { RelayRequest, content } from "@hoppscotch/kernel"
|
||||
import { parseBytesToJSON } from "~/helpers/functional/json"
|
||||
import { refreshToken, OAuth2ParamSchema } from "../utils"
|
||||
|
||||
const interceptorService = getService(KernelInterceptorService)
|
||||
|
||||
const ClientCredentialsFlowParamsSchema = ClientCredentialsGrantTypeParams.pick(
|
||||
{
|
||||
authEndpoint: true,
|
||||
clientID: true,
|
||||
clientSecret: true,
|
||||
scopes: true,
|
||||
clientAuthentication: true,
|
||||
}
|
||||
).refine(
|
||||
(params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
(!params.scopes || params.scopes.length >= 1)
|
||||
)
|
||||
},
|
||||
{
|
||||
message: "Minimum length requirement not met for one or more parameters",
|
||||
}
|
||||
)
|
||||
const ClientCredentialsFlowParamsSchema = z
|
||||
.object({
|
||||
authEndpoint: z.string(),
|
||||
clientID: z.string(),
|
||||
clientSecret: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
clientAuthentication: z.enum(["AS_BASIC_AUTH_HEADERS", "IN_BODY"]),
|
||||
tokenRequestParams: z.array(OAuth2ParamSchema),
|
||||
refreshRequestParams: z.array(OAuth2ParamSchema),
|
||||
})
|
||||
.refine(
|
||||
(params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
(!params.scopes || params.scopes.length >= 1)
|
||||
)
|
||||
},
|
||||
{
|
||||
message: "Minimum length requirement not met for one or more parameters",
|
||||
}
|
||||
)
|
||||
|
||||
export type ClientCredentialsFlowParams = z.infer<
|
||||
typeof ClientCredentialsFlowParamsSchema
|
||||
|
|
@ -47,6 +49,8 @@ export const getDefaultClientCredentialsFlowParams =
|
|||
clientSecret: "",
|
||||
scopes: undefined,
|
||||
clientAuthentication: "IN_BODY",
|
||||
tokenRequestParams: [],
|
||||
refreshRequestParams: [],
|
||||
})
|
||||
|
||||
const initClientCredentialsOAuthFlow = async (
|
||||
|
|
@ -169,56 +173,115 @@ export default createFlowConfig(
|
|||
"CLIENT_CREDENTIALS" as const,
|
||||
ClientCredentialsFlowParamsSchema,
|
||||
initClientCredentialsOAuthFlow,
|
||||
handleRedirectForAuthCodeOauthFlow
|
||||
handleRedirectForAuthCodeOauthFlow,
|
||||
refreshToken
|
||||
)
|
||||
|
||||
const getPayloadForViaBasicAuthHeader = (
|
||||
payload: Omit<ClientCredentialsFlowParams, "clientAuthentication">
|
||||
): RelayRequest => {
|
||||
const { clientID, clientSecret, scopes, authEndpoint } = payload
|
||||
|
||||
const getPayloadForViaBasicAuthHeader = ({
|
||||
clientID,
|
||||
clientSecret,
|
||||
scopes,
|
||||
authEndpoint,
|
||||
tokenRequestParams,
|
||||
}: ClientCredentialsFlowParams): RelayRequest => {
|
||||
// RFC 6749 Section 2.3.1 states that the client ID and secret should be URL encoded.
|
||||
const encodedClientID = encodeBasicAuthComponent(clientID)
|
||||
const encodedClientSecret = encodeBasicAuthComponent(clientSecret || "")
|
||||
const basicAuthToken = btoa(`${encodedClientID}:${encodedClientSecret}`)
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "client_credentials",
|
||||
...(scopes && { scope: scopes }),
|
||||
}
|
||||
|
||||
const urlParams: Record<string, string> = {}
|
||||
|
||||
// Process additional token request parameters
|
||||
if (tokenRequestParams) {
|
||||
tokenRequestParams
|
||||
.filter((param) => param.active && param.key && param.value)
|
||||
.forEach((param) => {
|
||||
if (param.sendIn === "headers") {
|
||||
headers[param.key] = param.value
|
||||
} else if (param.sendIn === "url") {
|
||||
urlParams[param.key] = param.value
|
||||
} else {
|
||||
// Default to body
|
||||
bodyParams[param.key] = param.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = new URL(authEndpoint)
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
|
||||
return {
|
||||
id: Date.now(),
|
||||
url: authEndpoint,
|
||||
url: url.toString(),
|
||||
method: "POST",
|
||||
version: "HTTP/1.1",
|
||||
headers: {
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
content: content.urlencoded({
|
||||
grant_type: "client_credentials",
|
||||
...(scopes && { scope: scopes }),
|
||||
}),
|
||||
headers,
|
||||
content: content.urlencoded(bodyParams),
|
||||
}
|
||||
}
|
||||
|
||||
const getPayloadForViaBody = (
|
||||
payload: Omit<ClientCredentialsFlowParams, "clientAuthentication">
|
||||
): RelayRequest => {
|
||||
const { clientID, clientSecret, scopes, authEndpoint } = payload
|
||||
const getPayloadForViaBody = ({
|
||||
clientID,
|
||||
clientSecret,
|
||||
scopes,
|
||||
authEndpoint,
|
||||
tokenRequestParams,
|
||||
}: ClientCredentialsFlowParams): RelayRequest => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientID,
|
||||
...(clientSecret && { client_secret: clientSecret }),
|
||||
...(scopes && { scope: scopes }),
|
||||
}
|
||||
|
||||
const urlParams: Record<string, string> = {}
|
||||
|
||||
// Process additional token request parameters
|
||||
if (tokenRequestParams) {
|
||||
tokenRequestParams
|
||||
.filter((param) => param.active && param.key && param.value)
|
||||
.forEach((param) => {
|
||||
if (param.sendIn === "headers") {
|
||||
headers[param.key] = param.value
|
||||
} else if (param.sendIn === "url") {
|
||||
urlParams[param.key] = param.value
|
||||
} else {
|
||||
// Default to body
|
||||
bodyParams[param.key] = param.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = new URL(authEndpoint)
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
|
||||
return {
|
||||
id: Date.now(),
|
||||
url: authEndpoint,
|
||||
url: url.toString(),
|
||||
method: "POST",
|
||||
version: "HTTP/1.1",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
content: content.urlencoded({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientID,
|
||||
...(clientSecret && { client_secret: clientSecret }),
|
||||
...(scopes && { scope: scopes }),
|
||||
}),
|
||||
headers,
|
||||
content: content.urlencoded(bodyParams),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,21 +8,29 @@ import {
|
|||
import { z } from "zod"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ImplicitOauthFlowParams } from "@hoppscotch/data"
|
||||
import { OAuth2ParamSchema } from "../utils"
|
||||
|
||||
const persistenceService = getService(PersistenceService)
|
||||
|
||||
const ImplicitOauthFlowParamsSchema = ImplicitOauthFlowParams.pick({
|
||||
authEndpoint: true,
|
||||
clientID: true,
|
||||
scopes: true,
|
||||
}).refine((params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
(params.scopes === undefined || params.scopes.length >= 1)
|
||||
)
|
||||
})
|
||||
const ImplicitOauthFlowParamsSchema = z
|
||||
.object({
|
||||
authEndpoint: z.string(),
|
||||
clientID: z.string(),
|
||||
scopes: z.string().optional(),
|
||||
authRequestParams: z.array(
|
||||
OAuth2ParamSchema.omit({
|
||||
sendIn: true,
|
||||
})
|
||||
),
|
||||
refreshRequestParams: z.array(OAuth2ParamSchema),
|
||||
})
|
||||
.refine((params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
(params.scopes === undefined || params.scopes.length >= 1)
|
||||
)
|
||||
})
|
||||
|
||||
export type ImplicitOauthFlowParams = z.infer<
|
||||
typeof ImplicitOauthFlowParamsSchema
|
||||
|
|
@ -33,12 +41,15 @@ export const getDefaultImplicitOauthFlowParams =
|
|||
authEndpoint: "",
|
||||
clientID: "",
|
||||
scopes: undefined,
|
||||
authRequestParams: [],
|
||||
refreshRequestParams: [],
|
||||
})
|
||||
|
||||
const initImplicitOauthFlow = async ({
|
||||
clientID,
|
||||
scopes,
|
||||
authEndpoint,
|
||||
authRequestParams,
|
||||
}: ImplicitOauthFlowParams) => {
|
||||
const state = generateRandomString()
|
||||
|
||||
|
|
@ -59,6 +70,7 @@ const initImplicitOauthFlow = async ({
|
|||
authEndpoint,
|
||||
scopes,
|
||||
state,
|
||||
authRequestParams,
|
||||
},
|
||||
grant_type: "IMPLICIT",
|
||||
})
|
||||
|
|
@ -79,6 +91,15 @@ const initImplicitOauthFlow = async ({
|
|||
|
||||
if (scopes) url.searchParams.set("scope", scopes)
|
||||
|
||||
// Process additional auth request parameters
|
||||
if (authRequestParams) {
|
||||
authRequestParams
|
||||
.filter((param) => param.active && param.key && param.value)
|
||||
.forEach((param) => {
|
||||
url.searchParams.set(param.key, param.value)
|
||||
})
|
||||
}
|
||||
|
||||
// Redirect to the authorization server
|
||||
window.location.assign(url.toString())
|
||||
|
||||
|
|
|
|||
|
|
@ -9,33 +9,37 @@ import { z } from "zod"
|
|||
import { getService } from "~/modules/dioc"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { PasswordGrantTypeParams } from "@hoppscotch/data"
|
||||
import { content } from "@hoppscotch/kernel"
|
||||
import { parseBytesToJSON } from "~/helpers/functional/json"
|
||||
import { OAuth2ParamSchema, refreshToken } from "../utils"
|
||||
|
||||
const interceptorService = getService(KernelInterceptorService)
|
||||
|
||||
const PasswordFlowParamsSchema = PasswordGrantTypeParams.pick({
|
||||
authEndpoint: true,
|
||||
clientID: true,
|
||||
clientSecret: true,
|
||||
scopes: true,
|
||||
username: true,
|
||||
password: true,
|
||||
}).refine(
|
||||
(params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
params.username.length >= 1 &&
|
||||
params.password.length >= 1 &&
|
||||
(!params.scopes || params.scopes.length >= 1)
|
||||
)
|
||||
},
|
||||
{
|
||||
message: "Minimum length requirement not met for one or more parameters",
|
||||
}
|
||||
)
|
||||
const PasswordFlowParamsSchema = z
|
||||
.object({
|
||||
authEndpoint: z.string(),
|
||||
clientID: z.string(),
|
||||
clientSecret: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
tokenRequestParams: z.array(OAuth2ParamSchema),
|
||||
refreshRequestParams: z.array(OAuth2ParamSchema),
|
||||
})
|
||||
.refine(
|
||||
(params) => {
|
||||
return (
|
||||
params.authEndpoint.length >= 1 &&
|
||||
params.clientID.length >= 1 &&
|
||||
params.username.length >= 1 &&
|
||||
params.password.length >= 1 &&
|
||||
(!params.scopes || params.scopes.length >= 1)
|
||||
)
|
||||
},
|
||||
{
|
||||
message: "Minimum length requirement not met for one or more parameters",
|
||||
}
|
||||
)
|
||||
|
||||
export type PasswordFlowParams = z.infer<typeof PasswordFlowParamsSchema>
|
||||
|
||||
|
|
@ -46,6 +50,8 @@ export const getDefaultPasswordFlowParams = (): PasswordFlowParams => ({
|
|||
scopes: undefined,
|
||||
username: "",
|
||||
password: "",
|
||||
tokenRequestParams: [],
|
||||
refreshRequestParams: [],
|
||||
})
|
||||
|
||||
const initPasswordOauthFlow = async ({
|
||||
|
|
@ -55,30 +61,58 @@ const initPasswordOauthFlow = async ({
|
|||
clientSecret,
|
||||
scopes,
|
||||
authEndpoint,
|
||||
tokenRequestParams,
|
||||
}: PasswordFlowParams) => {
|
||||
const toast = useToast()
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "password",
|
||||
client_id: clientID,
|
||||
username,
|
||||
password,
|
||||
...(clientSecret && {
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
...(scopes && {
|
||||
scope: scopes,
|
||||
}),
|
||||
}
|
||||
|
||||
const urlParams: Record<string, string> = {}
|
||||
|
||||
// Process additional token request parameters
|
||||
if (tokenRequestParams) {
|
||||
tokenRequestParams
|
||||
.filter((param) => param.active && param.key && param.value)
|
||||
.forEach((param) => {
|
||||
if (param.sendIn === "headers") {
|
||||
headers[param.key] = param.value
|
||||
} else if (param.sendIn === "url") {
|
||||
urlParams[param.key] = param.value
|
||||
} else {
|
||||
// Default to body
|
||||
bodyParams[param.key] = param.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = new URL(authEndpoint)
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
|
||||
const { response } = interceptorService.execute({
|
||||
id: Date.now(),
|
||||
url: authEndpoint,
|
||||
url: url.toString(),
|
||||
method: "POST",
|
||||
version: "HTTP/1.1",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
content: content.urlencoded({
|
||||
grant_type: "password",
|
||||
client_id: clientID,
|
||||
username,
|
||||
password,
|
||||
...(clientSecret && {
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
...(scopes && {
|
||||
scope: scopes,
|
||||
}),
|
||||
}),
|
||||
headers,
|
||||
content: content.urlencoded(bodyParams),
|
||||
})
|
||||
|
||||
const res = await response
|
||||
|
|
@ -191,5 +225,6 @@ export default createFlowConfig(
|
|||
"PASSWORD" as const,
|
||||
PasswordFlowParamsSchema,
|
||||
initPasswordOauthFlow,
|
||||
handleRedirectForAuthCodeOauthFlow
|
||||
handleRedirectForAuthCodeOauthFlow,
|
||||
refreshToken
|
||||
)
|
||||
|
|
|
|||
122
packages/hoppscotch-common/src/services/oauth/utils.ts
Normal file
122
packages/hoppscotch-common/src/services/oauth/utils.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as E from "fp-ts/Either"
|
||||
import { z } from "zod"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import { content } from "@hoppscotch/kernel"
|
||||
import { decodeResponseAsJSON } from "./oauth.service"
|
||||
|
||||
const interceptorService = getService(KernelInterceptorService)
|
||||
|
||||
// Type definition for refresh request parameters
|
||||
export type RefreshRequestParam = {
|
||||
id: number
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
sendIn?: "headers" | "url" | "body"
|
||||
}
|
||||
|
||||
// Unified refresh token parameters for all OAuth flows
|
||||
export type RefreshTokenParams = {
|
||||
tokenEndpoint: string
|
||||
clientID: string
|
||||
refreshToken: string
|
||||
clientSecret?: string
|
||||
refreshRequestParams?: Array<RefreshRequestParam>
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified refresh token function for all OAuth flows
|
||||
* Supports both basic flows (authCode) and advanced flows (password, clientCredentials)
|
||||
* with optional advanced parameters
|
||||
*/
|
||||
export const refreshToken = async ({
|
||||
tokenEndpoint,
|
||||
clientID,
|
||||
refreshToken,
|
||||
clientSecret,
|
||||
refreshRequestParams,
|
||||
}: RefreshTokenParams) => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
}
|
||||
|
||||
const bodyParams: Record<string, string> = {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientID,
|
||||
...(clientSecret && {
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
}
|
||||
|
||||
const urlParams: Record<string, string> = {}
|
||||
|
||||
// Process additional refresh request parameters (if provided)
|
||||
if (refreshRequestParams) {
|
||||
refreshRequestParams
|
||||
.filter((param) => param.active && param.key && param.value)
|
||||
.forEach((param) => {
|
||||
if (param.sendIn === "headers") {
|
||||
headers[param.key] = param.value
|
||||
} else if (param.sendIn === "url") {
|
||||
urlParams[param.key] = param.value
|
||||
} else {
|
||||
// Default to body
|
||||
bodyParams[param.key] = param.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = new URL(tokenEndpoint)
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
|
||||
const { response } = interceptorService.execute({
|
||||
id: Date.now(),
|
||||
url: url.toString(),
|
||||
method: "POST",
|
||||
version: "HTTP/1.1",
|
||||
headers,
|
||||
content: content.urlencoded(bodyParams),
|
||||
})
|
||||
|
||||
const res = await response
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
|
||||
}
|
||||
|
||||
const responsePayload = decodeResponseAsJSON(res.right)
|
||||
|
||||
if (E.isLeft(responsePayload)) {
|
||||
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
|
||||
}
|
||||
|
||||
const withAccessTokenAndRefreshTokenSchema = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string().optional(),
|
||||
})
|
||||
|
||||
const parsedTokenResponse = withAccessTokenAndRefreshTokenSchema.safeParse(
|
||||
responsePayload.right
|
||||
)
|
||||
|
||||
return parsedTokenResponse.success
|
||||
? E.right(parsedTokenResponse.data)
|
||||
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common OAuth2 parameter schema with all possible fields
|
||||
* Used as base for both auth requests and advanced token/refresh requests
|
||||
*/
|
||||
export const OAuth2ParamSchema = z.object({
|
||||
id: z.number(),
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
active: z.boolean(),
|
||||
sendIn: z.enum(["headers", "url", "body"]).optional(),
|
||||
})
|
||||
Loading…
Reference in a new issue