184 lines
4.6 KiB
TypeScript
184 lines
4.6 KiB
TypeScript
import {
|
|
OauthAuthService,
|
|
createFlowConfig,
|
|
decodeResponseAsJSON,
|
|
} from "../oauth.service"
|
|
import { z } from "zod"
|
|
import { getService } from "~/modules/dioc"
|
|
import * as E from "fp-ts/Either"
|
|
import { InterceptorService } from "~/services/interceptor.service"
|
|
import { useToast } from "~/composables/toast"
|
|
import { ClientCredentialsGrantTypeParams } from "@hoppscotch/data"
|
|
|
|
const interceptorService = getService(InterceptorService)
|
|
|
|
const ClientCredentialsFlowParamsSchema = ClientCredentialsGrantTypeParams.pick(
|
|
{
|
|
authEndpoint: true,
|
|
clientID: true,
|
|
clientSecret: true,
|
|
scopes: true,
|
|
}
|
|
).refine(
|
|
(params) => {
|
|
return (
|
|
params.authEndpoint.length >= 1 &&
|
|
params.clientID.length >= 1 &&
|
|
params.clientSecret.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
|
|
>
|
|
|
|
export const getDefaultClientCredentialsFlowParams =
|
|
(): ClientCredentialsFlowParams => ({
|
|
authEndpoint: "",
|
|
clientID: "",
|
|
clientSecret: "",
|
|
scopes: undefined,
|
|
})
|
|
|
|
const initClientCredentialsOAuthFlow = async ({
|
|
clientID,
|
|
clientSecret,
|
|
scopes,
|
|
authEndpoint,
|
|
}: ClientCredentialsFlowParams) => {
|
|
const toast = useToast()
|
|
|
|
const formData = new URLSearchParams()
|
|
formData.append("grant_type", "client_credentials")
|
|
formData.append("client_id", clientID)
|
|
formData.append("client_secret", clientSecret)
|
|
|
|
if (scopes) {
|
|
formData.append("scope", scopes)
|
|
}
|
|
|
|
const { response } = interceptorService.runRequest({
|
|
url: authEndpoint,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Accept: "application/json",
|
|
},
|
|
data: formData.toString(),
|
|
})
|
|
|
|
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 withAccessTokenSchema = z.object({
|
|
access_token: z.string(),
|
|
})
|
|
|
|
const parsedTokenResponse = withAccessTokenSchema.safeParse(
|
|
responsePayload.right
|
|
)
|
|
|
|
if (!parsedTokenResponse.success) {
|
|
toast.error("AUTH_TOKEN_REQUEST_INVALID_RESPONSE")
|
|
}
|
|
|
|
return parsedTokenResponse.success
|
|
? E.right(parsedTokenResponse.data)
|
|
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
|
|
}
|
|
|
|
const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
|
|
// parse the query string
|
|
const params = new URLSearchParams(window.location.search)
|
|
|
|
const code = params.get("code")
|
|
const state = params.get("state")
|
|
const error = params.get("error")
|
|
|
|
if (error) {
|
|
return E.left("AUTH_SERVER_RETURNED_ERROR")
|
|
}
|
|
|
|
if (!code) {
|
|
return E.left("AUTH_TOKEN_REQUEST_FAILED")
|
|
}
|
|
|
|
const expectedSchema = z.object({
|
|
state: z.string(),
|
|
tokenEndpoint: z.string(),
|
|
clientSecret: z.string(),
|
|
clientID: z.string(),
|
|
})
|
|
|
|
const decodedLocalConfig = expectedSchema.safeParse(JSON.parse(localConfig))
|
|
|
|
if (!decodedLocalConfig.success) {
|
|
return E.left("INVALID_LOCAL_CONFIG")
|
|
}
|
|
|
|
// check if the state matches
|
|
if (decodedLocalConfig.data.state !== state) {
|
|
return E.left("INVALID_STATE")
|
|
}
|
|
|
|
// exchange the code for a token
|
|
const formData = new URLSearchParams()
|
|
formData.append("code", code)
|
|
formData.append("client_id", decodedLocalConfig.data.clientID)
|
|
formData.append("client_secret", decodedLocalConfig.data.clientSecret)
|
|
formData.append("redirect_uri", OauthAuthService.redirectURI)
|
|
|
|
const { response } = interceptorService.runRequest({
|
|
url: decodedLocalConfig.data.tokenEndpoint,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Accept: "application/json",
|
|
},
|
|
data: formData.toString(),
|
|
})
|
|
|
|
const res = await response
|
|
|
|
if (E.isLeft(res)) {
|
|
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
|
|
}
|
|
|
|
const responsePayload = new TextDecoder("utf-8")
|
|
.decode(res.right.data as any)
|
|
.replaceAll("\x00", "")
|
|
|
|
const withAccessTokenSchema = z.object({
|
|
access_token: z.string(),
|
|
})
|
|
|
|
const parsedTokenResponse = withAccessTokenSchema.safeParse(
|
|
JSON.parse(responsePayload)
|
|
)
|
|
|
|
return parsedTokenResponse.success
|
|
? E.right(parsedTokenResponse.data)
|
|
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
|
|
}
|
|
|
|
export default createFlowConfig(
|
|
"CLIENT_CREDENTIALS" as const,
|
|
ClientCredentialsFlowParamsSchema,
|
|
initClientCredentialsOAuthFlow,
|
|
handleRedirectForAuthCodeOauthFlow
|
|
)
|