feat: add OAuth 2.0 support

This commit is contained in:
liyasthomas
2021-08-25 21:30:13 +05:30
parent 8796cec493
commit fedc230c9f
7 changed files with 250 additions and 18 deletions

View File

@@ -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,

View 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>

View File

@@ -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 }

View File

@@ -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
)

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
}
},
})