@@ -114,6 +114,48 @@ async def create_authorization_url(
114
114
115
115
return str (authorization_url )
116
116
117
+ async def exchange_short_lived_token (self , short_lived_token : str ) -> dict [str , Any ]:
118
+ """
119
+ Exchange short-lived access token for long-lived access token.
120
+ This is specific to Instagram's API requirements.
121
+
122
+ Args:
123
+ short_lived_token: The short-lived access token from the initial OAuth flow
124
+
125
+ Returns:
126
+ Token response dictionary with long-lived access token
127
+ """
128
+ if self .app_name != "INSTAGRAM" :
129
+ raise OAuth2Error ("Token exchange is only supported for Instagram" )
130
+
131
+ exchange_token_url = self .custom_data .get (
132
+ "exchange_token_url" , "https://graph.instagram.com/access_token"
133
+ )
134
+
135
+ try :
136
+ response = await self .oauth2_client .get (
137
+ exchange_token_url ,
138
+ params = {
139
+ "grant_type" : "ig_exchange_token" ,
140
+ "client_secret" : self .client_secret ,
141
+ "access_token" : short_lived_token ,
142
+ },
143
+ timeout = 30.0 ,
144
+ )
145
+ response .raise_for_status ()
146
+
147
+ token_data = cast (dict [str , Any ], response .json ())
148
+ logger .info (
149
+ f"Successfully exchanged short-lived token for long-lived token, app_name={ self .app_name } "
150
+ )
151
+ return token_data
152
+
153
+ except Exception as e :
154
+ logger .error (
155
+ f"Failed to exchange short-lived token, app_name={ self .app_name } , error={ e } "
156
+ )
157
+ raise OAuth2Error ("Failed to exchange short-lived token for long-lived token" ) from e
158
+
117
159
# TODO: some app may not support "code_verifier"?
118
160
async def fetch_token (
119
161
self ,
@@ -143,6 +185,24 @@ async def fetch_token(
143
185
scope = self .scope ,
144
186
),
145
187
)
188
+ # handle Instagram's special case - exchange short-lived token for long-lived token
189
+ if self .app_name == "INSTAGRAM" :
190
+ if "access_token" in token :
191
+ short_lived_token = token ["access_token" ]
192
+ logger .info (
193
+ f"Exchanging short-lived token for long-lived token, app_name={ self .app_name } "
194
+ )
195
+ long_lived_token_response = await self .exchange_short_lived_token (
196
+ short_lived_token
197
+ )
198
+ # Update data with long-lived token response: add expires_in and token_type, update access_token
199
+ token .update (long_lived_token_response )
200
+ else :
201
+ logger .error (
202
+ f"Missing access_token in Instagram OAuth response, app={ self .app_name } "
203
+ )
204
+ raise OAuth2Error ("Missing access_token in Instagram OAuth response" )
205
+ # return the token response with long-lived access token
146
206
return token
147
207
except Exception as e :
148
208
logger .error (f"Failed to fetch access token, app_name={ self .app_name } , error={ e } " )
@@ -164,48 +224,6 @@ async def refresh_token(
164
224
logger .error (f"Failed to refresh access token, app_name={ self .app_name } , error={ e } " )
165
225
raise OAuth2Error ("Failed to refresh access token" ) from e
166
226
167
- async def exchange_short_lived_token (self , short_lived_token : str ) -> dict [str , Any ]:
168
- """
169
- Exchange short-lived access token for long-lived access token.
170
- This is specific to Instagram's API requirements.
171
-
172
- Args:
173
- short_lived_token: The short-lived access token from the initial OAuth flow
174
-
175
- Returns:
176
- Token response dictionary with long-lived access token
177
- """
178
- if self .app_name != "INSTAGRAM" :
179
- raise OAuth2Error ("Token exchange is only supported for Instagram" )
180
-
181
- exchange_token_url = self .custom_data .get (
182
- "exchange_token_url" , "https://graph.instagram.com/access_token"
183
- )
184
-
185
- try :
186
- response = await self .oauth2_client .get (
187
- exchange_token_url ,
188
- params = {
189
- "grant_type" : "ig_exchange_token" ,
190
- "client_secret" : self .client_secret ,
191
- "access_token" : short_lived_token ,
192
- },
193
- timeout = 30.0 ,
194
- )
195
- response .raise_for_status ()
196
-
197
- token_data = cast (dict [str , Any ], response .json ())
198
- logger .info (
199
- f"Successfully exchanged short-lived token for long-lived token, app_name={ self .app_name } "
200
- )
201
- return token_data
202
-
203
- except Exception as e :
204
- logger .error (
205
- f"Failed to exchange short-lived token, app_name={ self .app_name } , error={ e } "
206
- )
207
- raise OAuth2Error ("Failed to exchange short-lived token for long-lived token" ) from e
208
-
209
227
async def parse_fetch_token_response (self , token : dict ) -> OAuth2SchemeCredentials :
210
228
"""
211
229
Parse OAuth2SchemeCredentials from token response with app-specific handling.
@@ -226,22 +244,6 @@ async def parse_fetch_token_response(self, token: dict) -> OAuth2SchemeCredentia
226
244
logger .error (f"Missing authed_user in Slack OAuth response, app={ self .app_name } " )
227
245
raise OAuth2Error ("Missing access_token in Slack OAuth response" )
228
246
229
- # handle Instagram's special case - exchange short-lived token for long-lived token
230
- if self .app_name == "INSTAGRAM" :
231
- if "access_token" in data :
232
- short_lived_token = data ["access_token" ]
233
- logger .info (
234
- f"Exchanging short-lived token for long-lived token, app_name={ self .app_name } "
235
- )
236
- long_lived_token_response = await self .exchange_short_lived_token (short_lived_token )
237
- # Update data with long-lived token response: add expires_in and token_type, update access_token
238
- data .update (long_lived_token_response )
239
- else :
240
- logger .error (
241
- f"Missing access_token in Instagram OAuth response, app={ self .app_name } "
242
- )
243
- raise OAuth2Error ("Missing access_token in Instagram OAuth response" )
244
-
245
247
if "access_token" not in data :
246
248
logger .error (f"Missing access_token in OAuth response, app={ self .app_name } " )
247
249
raise OAuth2Error ("Missing access_token in OAuth response" )
0 commit comments