feat: ability to refresh tokens for oauth flows (#4302)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Akash K
2024-08-29 13:27:31 +05:30
committed by GitHub
parent 2ed7221182
commit 181ad098e0
20 changed files with 488 additions and 68 deletions

View File

@@ -255,7 +255,7 @@ onMounted(() => {
return
}
const { context, source, token }: PersistedOAuthConfig =
const { context, source, token, refresh_token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "REST") {
@@ -279,6 +279,10 @@ onMounted(() => {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
if (refresh_token && grantTypeInfo.grantType === "AUTHORIZATION_CODE") {
grantTypeInfo.refreshToken = refresh_token
}
}
editingProperties.value = unsavedCollectionProperties

View File

@@ -428,7 +428,7 @@ onMounted(() => {
return
}
const { context, source, token }: PersistedOAuthConfig =
const { context, source, token, refresh_token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "GraphQL") {
@@ -452,6 +452,10 @@ onMounted(() => {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
if (refresh_token && grantTypeInfo.grantType === "AUTHORIZATION_CODE") {
grantTypeInfo.refreshToken = refresh_token
}
}
editingProperties.value = unsavedCollectionProperties

View File

@@ -165,12 +165,18 @@
</span>
</div>
<div class="p-2">
<div class="p-2 gap-1 flex">
<HoppButtonSecondary
filled
:label="`${t('authorization.generate_token')}`"
@click="generateOAuthToken()"
/>
<HoppButtonSecondary
v-if="runTokenRefresh"
filled
:label="`${t('authorization.refresh_token')}`"
@click="refreshOauthToken()"
/>
</div>
</div>
</template>
@@ -192,6 +198,7 @@ import { getCombinedEnvVariables } from "~/helpers/preRequest"
import { AggregateEnvironment } from "~/newstore/environments"
import authCode, {
AuthCodeOauthFlowParams,
AuthCodeOauthRefreshParams,
getDefaultAuthCodeOauthFlowParams,
} from "~/services/oauth/flows/authCode"
import clientCredentials, {
@@ -356,6 +363,48 @@ const supportedGrantTypes = [
}
)
const refreshToken = async () => {
const grantTypeInfo = auth.value.grantTypeInfo
if (!("refreshToken" in grantTypeInfo)) {
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
}
const refreshToken = grantTypeInfo.refreshToken
if (!refreshToken) {
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
}
const params: AuthCodeOauthRefreshParams = {
clientID: clientID.value,
clientSecret: clientSecret.value,
tokenEndpoint: tokenEndpoint.value,
refreshToken,
}
const unwrappedParams = replaceTemplateStringsInObjectValues(params)
const refreshTokenFunc = authCode.refreshToken
if (!refreshTokenFunc) {
return E.left("REFRESH_TOKEN_FUNCTION_NOT_DEFINED" as const)
}
const res = await refreshTokenFunc(unwrappedParams)
if (E.isLeft(res)) {
return E.left("OAUTH_REFRESH_TOKEN_FAILED" as const)
}
setAccessTokenInActiveContext(
res.right.access_token,
res.right.refresh_token
)
return E.right(undefined)
}
const runAction = () => {
const params: AuthCodeOauthFlowParams = {
authEndpoint: authEndpoint.value,
@@ -456,6 +505,7 @@ const supportedGrantTypes = [
return {
runAction,
refreshToken,
elements,
}
}),
@@ -854,13 +904,28 @@ const selectedGrantType = computed(() => {
)
})
const setAccessTokenInActiveContext = (accessToken?: string) => {
const setAccessTokenInActiveContext = (
accessToken?: string,
refreshToken?: string
) => {
if (props.isCollectionProperty && accessToken) {
auth.value.grantTypeInfo = {
...auth.value.grantTypeInfo,
token: accessToken,
}
// set the refresh token if provided
// we also make sure the grantTypes supporting refreshTokens are
if (
refreshToken &&
auth.value.grantTypeInfo.grantType === "AUTHORIZATION_CODE"
) {
auth.value.grantTypeInfo = {
...auth.value.grantTypeInfo,
refreshToken,
}
}
return
}
@@ -874,6 +939,16 @@ const setAccessTokenInActiveContext = (accessToken?: string) => {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
accessToken
}
if (
refreshToken &&
tabService.currentActiveTab.value.document.request.auth.authType ===
"oauth-2"
) {
// @ts-expect-error - todo: narrow the grantType to only supporting refresh tokens
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken =
refreshToken
}
}
const changeSelectedGrantType = (
@@ -905,10 +980,53 @@ const runAction = computed(() => {
return selectedGrantType.value?.formElements.value?.runAction
})
const runTokenRefresh = computed(() => {
// the only grant type that supports refresh tokens is the authCode grant type
if (selectedGrantType.value?.id === "authCode") {
return selectedGrantType.value?.formElements.value?.refreshToken
}
return null
})
const currentOAuthGrantTypeFormElements = computed(() => {
return selectedGrantType.value?.formElements.value?.elements.value
})
const refreshOauthToken = async () => {
if (!runTokenRefresh.value) {
return
}
const res = await runTokenRefresh.value()
if (E.isLeft(res)) {
const errorMessages = {
NO_REFRESH_TOKEN_PRESENT: t(
"authorization.oauth.no_refresh_token_present"
),
REFRESH_TOKEN_FUNCTION_NOT_DEFINED: t(
"authorization.oauth.refresh_token_request_failed"
),
OAUTH_REFRESH_TOKEN_FAILED: t(
"authorization.oauth.refresh_token_request_failed"
),
}
const isKnownError = res.left in errorMessages
if (!isKnownError) {
toast.error(t("authorization.oauth.refresh_token_failed"))
return
}
toast.error(errorMessages[res.left])
return
}
toast.success(t("authorization.oauth.token_refreshed_successfully"))
}
const generateOAuthToken = async () => {
if (
grantTypesInvolvingRedirect.includes(auth.value.grantTypeInfo.grantType)

View File

@@ -97,6 +97,11 @@ onMounted(async () => {
...persistedOAuthConfig,
token: tokenInfo.right.access_token,
}
if (tokenInfo.right.refresh_token) {
authConfig.refresh_token = tokenInfo.right.refresh_token
}
persistenceService.setLocalConfig(
"oauth_temp_config",
JSON.stringify(authConfig)
@@ -118,6 +123,14 @@ onMounted(async () => {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
tokenInfo.right.access_token
if (
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo
.grantType === "AUTHORIZATION_CODE"
) {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken =
tokenInfo.right.refresh_token
}
toast.success(t("authorization.oauth.token_fetched_successfully"))
}

View File

@@ -46,6 +46,13 @@ export type AuthCodeOauthFlowParams = z.infer<
typeof AuthCodeOauthFlowParamsSchema
>
export type AuthCodeOauthRefreshParams = {
tokenEndpoint: string
clientID: string
clientSecret?: string
refreshToken: string
}
export const getDefaultAuthCodeOauthFlowParams =
(): AuthCodeOauthFlowParams => ({
authEndpoint: "",
@@ -233,6 +240,7 @@ const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
const withAccessTokenSchema = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
@@ -284,9 +292,60 @@ const encodeArrayBufferAsUrlEncodedBase64 = (buffer: ArrayBuffer) => {
return hashBase64URL
}
const refreshToken = async ({
tokenEndpoint,
clientID,
refreshToken,
clientSecret,
}: AuthCodeOauthRefreshParams) => {
const formData = new URLSearchParams()
formData.append("grant_type", "refresh_token")
formData.append("refresh_token", refreshToken)
formData.append("client_id", clientID)
if (clientSecret) {
formData.append("client_secret", clientSecret)
}
const { response } = interceptorService.runRequest({
url: tokenEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = decodeResponseAsJSON(res.right)
if (E.isLeft(responsePayload)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenAndRefreshTokenSchema = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
})
const parsedTokenResponse = withAccessTokenAndRefreshTokenSchema.safeParse(
responsePayload.right
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
export default createFlowConfig(
"AUTHORIZATION_CODE" as const,
AuthCodeOauthFlowParamsSchema,
initAuthCodeOauthFlow,
handleRedirectForAuthCodeOauthFlow
handleRedirectForAuthCodeOauthFlow,
refreshToken
)

View File

@@ -22,6 +22,7 @@ export type PersistedOAuthConfig = {
state: string
}
token?: string
refresh_token?: string
}
const persistenceService = getService(PersistenceService)
@@ -66,6 +67,7 @@ export function createFlowConfig<
Flow extends string,
AuthParams extends Record<string, unknown>,
InitFuncReturnObject extends Record<string, unknown>,
RefreshTokenParams extends Record<string, unknown>,
>(
flow: Flow,
params: ZodType<AuthParams>,
@@ -81,8 +83,14 @@ export function createFlowConfig<
string,
{
access_token: string
refresh_token?: string
}
>
>,
refreshToken?: (
params: RefreshTokenParams
) => Promise<
E.Either<string, { access_token: string; refresh_token?: string }>
>
) {
return {
@@ -90,6 +98,7 @@ export function createFlowConfig<
params,
init,
onRedirectReceived,
refreshToken,
}
}