feat: ability to refresh tokens for oauth flows (#4302)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user