chore: Oauth temporary ux improvements (#3792)
This commit is contained in:
@@ -3,14 +3,25 @@
|
|||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="oidcDiscoveryURL"
|
v-model="oidcDiscoveryURL"
|
||||||
|
:styles="
|
||||||
|
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
||||||
|
"
|
||||||
placeholder="OpenID Connect Discovery URL"
|
placeholder="OpenID Connect Discovery URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" />
|
<SmartEnvInput
|
||||||
|
v-model="authURL"
|
||||||
|
placeholder="Authorization URL"
|
||||||
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
|
></SmartEnvInput>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" />
|
<SmartEnvInput
|
||||||
|
v-model="accessTokenURL"
|
||||||
|
placeholder="Access Token URL"
|
||||||
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||||
@@ -44,6 +55,7 @@ import { useToast } from "@composables/toast"
|
|||||||
import { tokenRequest } from "~/helpers/oauth"
|
import { tokenRequest } from "~/helpers/oauth"
|
||||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -66,10 +78,16 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
||||||
|
const hasOIDCURL = computed(() => {
|
||||||
|
return oidcDiscoveryURL.value
|
||||||
|
})
|
||||||
|
|
||||||
const authURL = pluckRef(auth, "authURL")
|
const authURL = pluckRef(auth, "authURL")
|
||||||
|
|
||||||
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
||||||
|
const hasAccessTokenOrAuthURL = computed(() => {
|
||||||
|
return accessTokenURL.value || authURL.value
|
||||||
|
})
|
||||||
|
|
||||||
const clientID = pluckRef(auth, "clientID")
|
const clientID = pluckRef(auth, "clientID")
|
||||||
|
|
||||||
@@ -88,13 +106,11 @@ function translateTokenRequestError(error: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAccessTokenRequest = async () => {
|
const handleAccessTokenRequest = async () => {
|
||||||
if (
|
if (!oidcDiscoveryURL.value && !(authURL.value || accessTokenURL.value)) {
|
||||||
oidcDiscoveryURL.value === "" &&
|
|
||||||
(authURL.value === "" || accessTokenURL.value === "")
|
|
||||||
) {
|
|
||||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const envs = getCombinedEnvVariables()
|
const envs = getCombinedEnvVariables()
|
||||||
const envVars = [...envs.selected, ...envs.global]
|
const envVars = [...envs.selected, ...envs.global]
|
||||||
|
|
||||||
|
|||||||
@@ -3,41 +3,20 @@ import { PersistenceService } from "~/services/persistence"
|
|||||||
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
|
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
|
||||||
|
|
||||||
|
import { AxiosRequestConfig } from "axios"
|
||||||
|
import { until } from "@vueuse/core"
|
||||||
|
|
||||||
const redirectUri = `${window.location.origin}/oauth`
|
const redirectUri = `${window.location.origin}/oauth`
|
||||||
|
|
||||||
|
const interceptorService = getService(InterceptorService)
|
||||||
const persistenceService = getService(PersistenceService)
|
const persistenceService = getService(PersistenceService)
|
||||||
|
const extensionService = getService(ExtensionInterceptorService)
|
||||||
|
|
||||||
// GENERAL HELPER FUNCTIONS
|
// GENERAL HELPER FUNCTIONS
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a POST request and parse the response as JSON
|
|
||||||
*
|
|
||||||
* @param {String} url - The resource
|
|
||||||
* @param {Object} params - Configuration options
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const sendPostRequest = async (url: string, params: Record<string, string>) => {
|
|
||||||
const body = Object.keys(params)
|
|
||||||
.map((key) => `${key}=${params[key]}`)
|
|
||||||
.join("&")
|
|
||||||
const options = {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
||||||
},
|
|
||||||
body,
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, options)
|
|
||||||
const data = await response.json()
|
|
||||||
return E.right(data)
|
|
||||||
} catch (e) {
|
|
||||||
return E.left("AUTH_TOKEN_REQUEST_FAILED")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a query string into an object
|
* Parse a query string into an object
|
||||||
*
|
*
|
||||||
@@ -71,9 +50,16 @@ const getTokenConfiguration = async (endpoint: string) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, options)
|
const res = await runRequestThroughInterceptor({
|
||||||
const config = await response.json()
|
url: endpoint,
|
||||||
return E.right(config)
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
return E.left("OIDC_DISCOVERY_FAILED")
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(JSON.parse(res.right))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return E.left("OIDC_DISCOVERY_FAILED")
|
return E.left("OIDC_DISCOVERY_FAILED")
|
||||||
}
|
}
|
||||||
@@ -166,8 +152,7 @@ const tokenRequest = async ({
|
|||||||
clientSecret,
|
clientSecret,
|
||||||
scope,
|
scope,
|
||||||
}: TokenRequestParams) => {
|
}: TokenRequestParams) => {
|
||||||
// Check oauth configuration
|
if (oidcDiscoveryUrl) {
|
||||||
if (oidcDiscoveryUrl !== "") {
|
|
||||||
const res = await getTokenConfiguration(oidcDiscoveryUrl)
|
const res = await getTokenConfiguration(oidcDiscoveryUrl)
|
||||||
|
|
||||||
const OIDCConfigurationSchema = z.object({
|
const OIDCConfigurationSchema = z.object({
|
||||||
@@ -269,17 +254,19 @@ const handleOAuthRedirect = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exchange the authorization code for an access token
|
// Exchange the authorization code for an access token
|
||||||
const tokenResponse: E.Either<string, any> = await sendPostRequest(
|
const tokenResponse = await runRequestThroughInterceptor({
|
||||||
tokenEndpoint,
|
url: tokenEndpoint,
|
||||||
{
|
data: JSON.stringify({
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: queryParams.code,
|
code: queryParams.code,
|
||||||
client_id: clientID,
|
client_id: clientID,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
code_verifier: codeVerifier,
|
code_verifier: codeVerifier,
|
||||||
}
|
}),
|
||||||
)
|
method: "POST",
|
||||||
|
headers: {},
|
||||||
|
})
|
||||||
|
|
||||||
// Clean these up since we don't need them anymore
|
// Clean these up since we don't need them anymore
|
||||||
clearPKCEState()
|
clearPKCEState()
|
||||||
@@ -293,7 +280,7 @@ const handleOAuthRedirect = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const parsedTokenResponse = withAccessTokenSchema.safeParse(
|
const parsedTokenResponse = withAccessTokenSchema.safeParse(
|
||||||
tokenResponse.right
|
JSON.parse(tokenResponse.right)
|
||||||
)
|
)
|
||||||
|
|
||||||
return parsedTokenResponse.success
|
return parsedTokenResponse.success
|
||||||
@@ -309,4 +296,20 @@ const clearPKCEState = () => {
|
|||||||
persistenceService.removeLocalConfig("client_secret")
|
persistenceService.removeLocalConfig("client_secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runRequestThroughInterceptor(config: AxiosRequestConfig) {
|
||||||
|
const res = await interceptorService.runRequest(config).response
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
return E.left("REQUEST_FAILED")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert ArrayBuffer to string
|
||||||
|
if (!(res.right.data instanceof ArrayBuffer)) {
|
||||||
|
return E.left("REQUEST_FAILED")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new TextDecoder().decode(res.right.data).replace(/\0+$/, "")
|
||||||
|
return E.right(data)
|
||||||
|
}
|
||||||
|
|
||||||
export { tokenRequest, handleOAuthRedirect }
|
export { tokenRequest, handleOAuthRedirect }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent"
|
|||||||
import SettingsExtension from "~/components/settings/Extension.vue"
|
import SettingsExtension from "~/components/settings/Extension.vue"
|
||||||
import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue"
|
import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue"
|
||||||
import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue"
|
import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue"
|
||||||
|
import { until } from "@vueuse/core"
|
||||||
|
|
||||||
export const defineSubscribableObject = <T extends object>(obj: T) => {
|
export const defineSubscribableObject = <T extends object>(obj: T) => {
|
||||||
const proxyObject = {
|
const proxyObject = {
|
||||||
@@ -206,6 +207,14 @@ export class ExtensionInterceptorService
|
|||||||
private async runRequestOnExtension(
|
private async runRequestOnExtension(
|
||||||
req: AxiosRequestConfig
|
req: AxiosRequestConfig
|
||||||
): RequestRunResult["response"] {
|
): RequestRunResult["response"] {
|
||||||
|
// wait for the extension to resolve
|
||||||
|
await until(this.extensionStatus).toMatch(
|
||||||
|
(status) => status !== "waiting",
|
||||||
|
{
|
||||||
|
timeout: 1000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const extensionHook = window.__POSTWOMAN_EXTENSION_HOOK__
|
const extensionHook = window.__POSTWOMAN_EXTENSION_HOOK__
|
||||||
if (!extensionHook) {
|
if (!extensionHook) {
|
||||||
return E.left(<InterceptorError>{
|
return E.left(<InterceptorError>{
|
||||||
|
|||||||
Reference in New Issue
Block a user