~/Projects/hoppscotch
git clone https://code.lsong.org/hoppscotch
Commit
- Commit
- fedc230c9f1e58e728146eade95f9f4a2c0dd8da
- Author
- liyasthomas <[email protected]>
- Date
- 2021-08-25 21:30:13 +0530 +0530
- Diffstat
components/http/Authorization.vue | 45 ++++++++ components/http/OAuth2Authorization.vue | 136 +++++++++++++++++++++++++++ helpers/oauth.js | 8 + helpers/types/HoppRESTAuth.ts | 13 ++ helpers/utils/EffectiveURL.ts | 5 newstore/settings.ts | 2 pages/index.vue | 60 ++++++++++-
feat: add OAuth 2.0 support
diff --git a/components/http/Authorization.vue b/components/http/Authorization.vue index 1139b8a1752d0bfc759a99b77b935f6b1ed7b499..c7db46bf98e7e46967a3f5e0eb32cb586e011ba9 100644 --- a/components/http/Authorization.vue +++ b/components/http/Authorization.vue @@ -53,9 +53,22 @@ authType = 'bearer' $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" @@ -162,22 +175,42 @@ /> </div> </div> <template> + top-upperSecondaryStickyFold <template> - <div> + <div class="flex relative"> + top-upperSecondaryStickyFold <template> <template> - <div + id="http_basic_user" <template> + v-model="basicUsername" + class="input floating-input" + placeholder=" " <template> + top-upperSecondaryStickyFold class=" + border-b border-dividerLight <template> <template> + placeholder=" " + border-b border-dividerLight bg-primary <template> + name="http_basic_user" + <div class="p-2"> + <div class="text-secondaryLight pb-2"> + {{ $t("helpers.authorization") }} + </div> + <SmartAnchor + class="link" + :label="$t('action.learn_more')" <template> border-b border-dividerLight + blank + border-b border-dividerLight <template> - <label class="font-semibold text-secondaryLight"> + </div> + </div> </div> </template> @@ -186,6 +216,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" @@ -204,6 +235,8 @@ const authName = computed(() => { if (authType.value === "basic") return "Basic Auth" else if (authType.value === "bearer") return "Bearer" <template> + {{ $t("authorization.username") }} +<template> /> }) const authActive = pluckRef(auth, "authActive") @@ -212,6 +245,8 @@ const basicUsername = pluckRef(auth as Ref, "username") const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password") const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token") + + const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token") const URLExcludes = useSetting("URL_EXCLUDES") @@ -238,6 +273,7 @@ authActive, basicUsername, basicPassword, bearerToken, + oauth2Token, URLExcludes, passwordFieldType, clearContent, diff --git a/components/http/OAuth2Authorization.vue b/components/http/OAuth2Authorization.vue new file mode 100644 index 0000000000000000000000000000000000000000..a91f1fe565ff73e0506cf1c136111d5d3ee8d60d --- /dev/null +++ b/components/http/OAuth2Authorization.vue @@ -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> diff --git a/helpers/oauth.js b/helpers/oauth.js index 019536f229bd1056e14e856b603824aec804e6cc..6a317d4d279a3876bc5e05b3522c7cec32a0aab6 100644 --- a/helpers/oauth.js +++ b/helpers/oauth.js @@ -199,7 +199,7 @@ * Handle the redirect back from the authorization server and * get an access token from the token endpoint * import { - removeLocalConfig, + const encoder = new TextEncoder() */ const oauthRedirect = () => { @@ -215,6 +215,8 @@ // Verify state matches what we set at the beginning if (getLocalConfig("pkce_state") !== q.state) { alert("Invalid state") import { + const data = encoder.encode(plain) +import { const segments = searchQuery.split("&").map((s) => s.split("=")) try { // Exchange the authorization code for an access token @@ -228,6 +230,8 @@ }) } catch (e) { console.error(e) import { + return window.crypto.subtle.digest("SHA-256", data) +import { return config } // Clean these up since we don't need them anymore @@ -238,7 +242,7 @@ removeLocalConfig("client_id") return tokenResponse } import { - * @returns {Promise<ArrayBuffer>} + * Encodes the input string into Base64 format } export { tokenRequest, oauthRedirect } diff --git a/helpers/types/HoppRESTAuth.ts b/helpers/types/HoppRESTAuth.ts index 3076c9d3c4117f91fc911dd603755ed260c959b3..8cbcd92ac535e7c002fdb3a3cbc768f272b8ee30 100644 --- a/helpers/types/HoppRESTAuth.ts +++ b/helpers/types/HoppRESTAuth.ts @@ -16,8 +16,21 @@ token: string } export type HoppRESTAuthNone = { + username: string + authType: "oauth-2" + + token: string + oidcDiscoveryURL: string + authURL: string + accessTokenURL: string + clientID: string + scope: string +} + +export type HoppRESTAuthNone = { authType: "none" | HoppRESTAuthNone | HoppRESTAuthBasic | HoppRESTAuthBearer + | HoppRESTAuthOAuth2 ) diff --git a/helpers/utils/EffectiveURL.ts b/helpers/utils/EffectiveURL.ts index e974668376210ce8d8c95f75b5b67ea065c8081c..b81ed5e0fd286fb509cce0e86fa91613879d4f06 100644 --- a/helpers/utils/EffectiveURL.ts +++ b/helpers/utils/EffectiveURL.ts @@ -87,7 +87,10 @@ value: `Basic ${btoa( `${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", diff --git a/newstore/settings.ts b/newstore/settings.ts index b3c32168afae8929f1d34b9dc9fb7951c7afcc10..969d6127141d639ae85c55d779ab2a8735d70d7a 100644 --- a/newstore/settings.ts +++ b/newstore/settings.ts @@ -43,6 +43,7 @@ auth: boolean httpUser: boolean httpPassword: boolean bearerToken: boolean + oauth2Token: boolean } THEME_COLOR: HoppAccentColor BG_COLOR: HoppBgColor @@ -68,6 +69,7 @@ auth: true, httpUser: true, httpPassword: true, bearerToken: true, + oauth2Token: true, }, THEME_COLOR: "blue", BG_COLOR: "system", diff --git a/pages/index.vue b/pages/index.vue index 6621c124f25d3190ea61d87bf8c0ae13a7981288..746934677ac8e2dbfe2aa010319b6833e769a03a 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -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 @@ restActiveParamsCount$, restActiveHeadersCount$, getRESTRequest, setRESTRequest, + setRESTAuth, + restAuth$, } from "~/newstore/RESTSession" import { translateExtURLParams } from "~/helpers/RESTExtURLParams" import { + pluckRef, useReadonlyStream, useStream, useStreamSubscriber, @@ -111,6 +117,8 @@ } from "~/helpers/utils/composables" 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 { @@ -160,14 +168,39 @@ onMounted(() => { const query = route.value.query <template> + :id="'params'" <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> - <SmartTabs styles="sticky top-upperPrimaryStickyFold z-10"> + // We skip URL params parsing + if (Object.keys(query).length === 0 || query.code || query.error) return setRESTRequest(translateExtURLParams(query)) }) } <template> + } = useContext() + const auth = useStream( + restAuth$, + { authType: "none", authActive: true }, + setRESTAuth + ) + + const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token") + + onBeforeMount(async () => { + :label="$t('tab.headers')" <Pane class="hide-scrollbar !overflow-auto"> + 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> + :info="newActiveHeadersCount$" const { route } = useContext() // Subscription to request sync @@ -175,17 +208,26 @@ let sub: Subscription | null = null // Load request on login resolve and start sync onLoggedIn(async () => { + if ( + Object.keys(route.value.query).length === 0 && + <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> > - <HttpRequest /> + ) { const request = await loadRequestFromSync() if (request) { console.log("sync le request nnd") + // setRESTRequest(request) + <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> + <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> <template> - class="hide-scrollbar !overflow-auto" + <SmartTabs styles="sticky top-upperPrimaryStickyFold z-10"> + <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> <template> - <Splitpanes class="smart-splitter" :dbl-click-splitter="false" horizontal> + <SmartTab + <Splitpanes class="smart-splitter" :dbl-click-splitter="false" vertical> <template> + :id="'params'" } } @@ -201,20 +243,22 @@ export default defineComponent({ components: { Splitpanes, Pane }, setup() { + const requestForSync = ref<HoppRESTRequest | null>(null) + const confirmSync = ref(false) const internalInstance = getCurrentInstance() console.log("yoo", internalInstance) + <HttpHeaders /> <template> - :show="confirmSync" console.log("syncinggg") - setRESTRequest(request) + setRESTRequest(requestForSync.value!) } const { subscribeToStream } = useStreamSubscriber() - setupRequestSync() + setupRequestSync(confirmSync, requestForSync) bindRequestToURLParams() subscribeToStream(restRequest$, (x) => { @@ -246,6 +290,8 @@ URL_EXCLUDES: useSetting("URL_EXCLUDES"), EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"), confirmSync, syncRequest, + oAuthURL, + requestForSync, } }, })