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