feat: add OAuth 2.0 support
This commit is contained in:
@@ -53,9 +53,22 @@
|
||||
$refs.authTypeOptions.tippy().hide()
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="OAuth 2.0"
|
||||
@click.native="
|
||||
authType = 'oauth-2'
|
||||
$refs.authTypeOptions.tippy().hide()
|
||||
"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<!-- <SmartToggle
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</SmartToggle> -->
|
||||
<SmartToggle
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@@ -161,12 +174,30 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <SmartToggle
|
||||
:on="!URL_EXCLUDES.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</SmartToggle> -->
|
||||
<div v-if="authType === 'oauth-2'" class="space-y-2 p-2">
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="oauth2_token"
|
||||
v-model="oauth2Token"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="oauth2_token"
|
||||
/>
|
||||
<label for="oauth2_token"> Token </label>
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
<div class="p-2">
|
||||
<div class="text-secondaryLight pb-2">
|
||||
{{ $t("helpers.authorization") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="$t('action.learn_more')"
|
||||
to="https://docs.hoppscotch.io/"
|
||||
blank
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -175,6 +206,7 @@ import { computed, defineComponent, Ref, ref } from "@nuxtjs/composition-api"
|
||||
import {
|
||||
HoppRESTAuthBasic,
|
||||
HoppRESTAuthBearer,
|
||||
HoppRESTAuthOAuth2,
|
||||
} from "~/helpers/types/HoppRESTAuth"
|
||||
import { pluckRef, useStream } from "~/helpers/utils/composables"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
@@ -192,6 +224,7 @@ export default defineComponent({
|
||||
const authName = computed(() => {
|
||||
if (authType.value === "basic") return "Basic Auth"
|
||||
else if (authType.value === "bearer") return "Bearer"
|
||||
else if (authType.value === "oauth-2") return "OAuth 2.0"
|
||||
else return "None"
|
||||
})
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
@@ -201,6 +234,8 @@ export default defineComponent({
|
||||
|
||||
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
|
||||
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
|
||||
const URLExcludes = useSetting("URL_EXCLUDES")
|
||||
|
||||
const passwordFieldType = ref("password")
|
||||
@@ -226,6 +261,7 @@ export default defineComponent({
|
||||
basicUsername,
|
||||
basicPassword,
|
||||
bearerToken,
|
||||
oauth2Token,
|
||||
URLExcludes,
|
||||
passwordFieldType,
|
||||
clearContent,
|
||||
|
||||
136
components/http/OAuth2Authorization.vue
Normal file
136
components/http/OAuth2Authorization.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="oidcDiscoveryURL"
|
||||
v-model="oidcDiscoveryURL"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="oidcDiscoveryURL"
|
||||
/>
|
||||
<label for="oidcDiscoveryURL">oidcDiscoveryURL </label>
|
||||
</div>
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="authURL"
|
||||
v-model="authURL"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="authURL"
|
||||
/>
|
||||
<label for="authURL">authURL </label>
|
||||
</div>
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="accessTokenURL"
|
||||
v-model="accessTokenURL"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="accessTokenURL"
|
||||
/>
|
||||
<label for="accessTokenURL">accessTokenURL </label>
|
||||
</div>
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="clientID"
|
||||
v-model="clientID"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="clientID"
|
||||
/>
|
||||
<label for="clientID">clientID </label>
|
||||
</div>
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="scope"
|
||||
v-model="scope"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
name="scope"
|
||||
/>
|
||||
<label for="scope">scope </label>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonPrimary
|
||||
label="Get request"
|
||||
@click.native="handleAccessTokenRequest()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { pluckRef, useStream } from "~/helpers/utils/composables"
|
||||
import { HoppRESTAuthOAuth2 } from "~/helpers/types/HoppRESTAuth"
|
||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
||||
import { tokenRequest } from "~/helpers/oauth"
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const $t = i18n.t.bind(i18n)
|
||||
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
|
||||
const oidcDiscoveryURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"oidcDiscoveryURL"
|
||||
)
|
||||
|
||||
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
|
||||
|
||||
const accessTokenURL = pluckRef(
|
||||
auth as Ref<HoppRESTAuthOAuth2>,
|
||||
"accessTokenURL"
|
||||
)
|
||||
|
||||
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
|
||||
|
||||
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
|
||||
|
||||
const handleAccessTokenRequest = async () => {
|
||||
if (
|
||||
oidcDiscoveryURL.value === "" &&
|
||||
(authURL.value === "" || accessTokenURL.value === "")
|
||||
) {
|
||||
$toast.error($t("complete_config_urls"), {
|
||||
icon: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const tokenReqParams = {
|
||||
grantType: "code",
|
||||
oidcDiscoveryUrl: oidcDiscoveryURL.value,
|
||||
authUrl: authURL.value,
|
||||
accessTokenUrl: accessTokenURL.value,
|
||||
clientId: clientID.value,
|
||||
scope: scope.value,
|
||||
}
|
||||
await tokenRequest(tokenReqParams)
|
||||
} catch (e) {
|
||||
$toast.error(e, {
|
||||
icon: "code",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oidcDiscoveryURL,
|
||||
authURL,
|
||||
accessTokenURL,
|
||||
clientID,
|
||||
scope,
|
||||
handleAccessTokenRequest,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -198,7 +198,7 @@ const tokenRequest = async ({
|
||||
* Handle the redirect back from the authorization server and
|
||||
* get an access token from the token endpoint
|
||||
*
|
||||
* @returns {Object}
|
||||
* @returns {Promise<any | void>}
|
||||
*/
|
||||
|
||||
const oauthRedirect = () => {
|
||||
@@ -213,6 +213,7 @@ const oauthRedirect = () => {
|
||||
// Verify state matches what we set at the beginning
|
||||
if (getLocalConfig("pkce_state") !== q.state) {
|
||||
alert("Invalid state")
|
||||
Promise.reject(tokenResponse)
|
||||
} else {
|
||||
try {
|
||||
// Exchange the authorization code for an access token
|
||||
@@ -225,6 +226,7 @@ const oauthRedirect = () => {
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Promise.reject(tokenResponse)
|
||||
}
|
||||
}
|
||||
// Clean these up since we don't need them anymore
|
||||
@@ -234,7 +236,7 @@ const oauthRedirect = () => {
|
||||
removeLocalConfig("client_id")
|
||||
return tokenResponse
|
||||
}
|
||||
return tokenResponse
|
||||
return Promise.reject(tokenResponse)
|
||||
}
|
||||
|
||||
export { tokenRequest, oauthRedirect }
|
||||
|
||||
@@ -15,8 +15,20 @@ export type HoppRESTAuthBearer = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type HoppRESTAuthOAuth2 = {
|
||||
authType: "oauth-2"
|
||||
|
||||
token: string
|
||||
oidcDiscoveryURL: string
|
||||
authURL: string
|
||||
accessTokenURL: string
|
||||
clientID: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
export type HoppRESTAuth = { authActive: boolean } & (
|
||||
| HoppRESTAuthNone
|
||||
| HoppRESTAuthBasic
|
||||
| HoppRESTAuthBearer
|
||||
| HoppRESTAuthOAuth2
|
||||
)
|
||||
|
||||
@@ -87,7 +87,10 @@ export function getEffectiveRESTRequest(
|
||||
`${request.auth.username}:${request.auth.password}`
|
||||
)}`,
|
||||
})
|
||||
} else if (request.auth.authType === "bearer") {
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
) {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
|
||||
@@ -43,6 +43,7 @@ export type SettingsType = {
|
||||
httpUser: boolean
|
||||
httpPassword: boolean
|
||||
bearerToken: boolean
|
||||
oauth2Token: boolean
|
||||
}
|
||||
THEME_COLOR: HoppAccentColor
|
||||
BG_COLOR: HoppBgColor
|
||||
@@ -68,6 +69,7 @@ export const defaultSettings: SettingsType = {
|
||||
httpUser: true,
|
||||
httpPassword: true,
|
||||
bearerToken: true,
|
||||
oauth2Token: true,
|
||||
},
|
||||
THEME_COLOR: "blue",
|
||||
BG_COLOR: "system",
|
||||
|
||||
@@ -84,8 +84,10 @@ import {
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
Ref,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
@@ -94,6 +96,7 @@ import { Splitpanes, Pane } from "splitpanes"
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
import { map } from "rxjs/operators"
|
||||
import { Subscription } from "rxjs"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import {
|
||||
restRequest$,
|
||||
@@ -101,9 +104,12 @@ import {
|
||||
restActiveHeadersCount$,
|
||||
getRESTRequest,
|
||||
setRESTRequest,
|
||||
setRESTAuth,
|
||||
restAuth$,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||
import {
|
||||
pluckRef,
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
useStreamSubscriber,
|
||||
@@ -111,6 +117,8 @@ import {
|
||||
import { loadRequestFromSync, startRequestSync } from "~/helpers/fb/request"
|
||||
import { onLoggedIn } from "~/helpers/fb/auth"
|
||||
import { HoppRESTRequest } from "~/helpers/types/HoppRESTRequest"
|
||||
import { oauthRedirect } from "~/helpers/oauth"
|
||||
import { HoppRESTAuthOAuth2 } from "~/helpers/types/HoppRESTAuth"
|
||||
|
||||
function bindRequestToURLParams() {
|
||||
const {
|
||||
@@ -159,12 +167,36 @@ function bindRequestToURLParams() {
|
||||
onMounted(() => {
|
||||
const query = route.value.query
|
||||
|
||||
if (Object.keys(query).length === 0) return
|
||||
// If query params are empty, or contains code or error param (these are from Oauth Redirect)
|
||||
// We skip URL params parsing
|
||||
if (Object.keys(query).length === 0 || query.code || query.error) return
|
||||
setRESTRequest(translateExtURLParams(query))
|
||||
})
|
||||
}
|
||||
|
||||
function setupRequestSync() {
|
||||
function oAuthURL() {
|
||||
const auth = useStream(
|
||||
restAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setRESTAuth
|
||||
)
|
||||
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const tokenInfo = await oauthRedirect()
|
||||
if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||
if (typeof tokenInfo === "object") {
|
||||
oauth2Token.value = tokenInfo.access_token
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setupRequestSync(
|
||||
confirmSync: Ref<boolean>,
|
||||
requestForSync: Ref<HoppRESTRequest | null>
|
||||
) {
|
||||
const { route } = useContext()
|
||||
|
||||
// Subscription to request sync
|
||||
@@ -172,13 +204,18 @@ function setupRequestSync() {
|
||||
|
||||
// Load request on login resolve and start sync
|
||||
onLoggedIn(async () => {
|
||||
if (Object.keys(route.value.query).length === 0) {
|
||||
if (
|
||||
Object.keys(route.value.query).length === 0 &&
|
||||
!(route.value.query.code || route.value.query.error)
|
||||
) {
|
||||
const request = await loadRequestFromSync()
|
||||
if (request) {
|
||||
console.log("sync le request nnd")
|
||||
|
||||
setRESTRequest(request)
|
||||
// confirmSync.value = true
|
||||
// setRESTRequest(request)
|
||||
if (!isEqual(request, getRESTRequest())) {
|
||||
requestForSync.value = request
|
||||
confirmSync.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,19 +231,21 @@ function setupRequestSync() {
|
||||
export default defineComponent({
|
||||
components: { Splitpanes, Pane },
|
||||
setup() {
|
||||
const requestForSync = ref<HoppRESTRequest | null>(null)
|
||||
|
||||
const confirmSync = ref(false)
|
||||
|
||||
const internalInstance = getCurrentInstance()
|
||||
console.log("yoo", internalInstance)
|
||||
|
||||
const syncRequest = (request: HoppRESTRequest) => {
|
||||
const syncRequest = () => {
|
||||
console.log("syncinggg")
|
||||
setRESTRequest(request)
|
||||
setRESTRequest(requestForSync.value!)
|
||||
}
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
setupRequestSync()
|
||||
setupRequestSync(confirmSync, requestForSync)
|
||||
bindRequestToURLParams()
|
||||
|
||||
subscribeToStream(restRequest$, (x) => {
|
||||
@@ -238,6 +277,8 @@ export default defineComponent({
|
||||
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
|
||||
confirmSync,
|
||||
syncRequest,
|
||||
oAuthURL,
|
||||
requestForSync,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user