#Shopify OAuth Flow
1 messages · Page 1 of 1 (latest)
@chilly dome
If no response in a reasonable time, ping @Member.
To close, type !solve or byte solve.
Please include an MCVE so that we can reproduce your issue locally.
The issue is that dependency for the callback route currently expects the OAuth client instantiated upfront whereas for Shopify, the shop name (provided dynamically by the user in the authorize route) is required to initialize the client correctly. Attached some of my code that is using the oauth plugin from the fullstack-inertia project (which looks like the same thing you sent above).
app.config
shopify_oauth_client = ShopifyOAuth2(
client_id=settings.app.SHOPIFY_OAUTH2_CLIENT_ID,
client_secret=settings.app.SHOPIFY_OAUTH2_CLIENT_SECRET,
shop="test-store",
scopes=["read_all_orders", "read_customers", "read_orders", "read_products"],
)
controller
class MyOAuthController(Controller):
"""OAuth management."""
@post(operation_id="OAuthAuthorize", path=urls.SHOPIFY_OAUTH_AUTHORIZE, name="shopify-oauth.authorize")
async def shopify_authorize(self, request: Request, shop: str) -> Redirect:
redirect_uri = request.url_for("shopify-oauth.callback")
print(f"{redirect_uri=}")
# Create client with shop
auth_url = await shopify_oauth_client.get_authorization_url(redirect_uri)
print(f"{auth_url=}")
return Redirect(path=auth_url, status_code=302)
@get(
operation_id="OAuthCallback",
path=urls.SHOPIFY_OAUTH_CALLBACK,
name="shopify-oauth.callback",
# client is already passed without the shop name (hardcoded in app.config.app)
dependencies={"access_token_state": Provide(OAuth2AuthorizeCallback(shopify_oauth_client, route_name="shopify-oauth.callback"))},
)
async def shopify_callback(self, access_token_state: AccessTokenState) -> Response:
print(f"{access_token_state=}")
token, state = access_token_state
print(f"{token=}")
print(f"{state= }")
return Response("OK")
In the example above, everything is working, but the shop is hardcoded. I want the flow to accept the shop from the client. My question is what's the recommended way to dynamically instantiate dependencies based on parameters received in earlier routes?
something like this:
def provide_shopify_oauth_client(request: Request, shop: str = Parameter(query="shop", required=True)) -> ShopifyOAuthClient:
return ShopifyOAuthClient(
client_id=settings.app.SHOPIFY_OAUTH2_CLIENT_ID,
client_secret=settings.app.SHOPIFY_OAUTH2_CLIENT_SECRET,
shop=shop,
scopes=["read_all_orders", "read_customers", "read_orders", "read_products"],
)
then you'd use it like so
class MyOAuthController(Controller):
"""OAuth management."""
dependencies = {'shopify_oauth_client': Provide(provide_shopify_oauth_client)}
@post(operation_id="OAuthAuthorize", path=urls.SHOPIFY_OAUTH_AUTHORIZE, name="shopify-oauth.authorize")
async def shopify_authorize(self, request: Request, shopify_oauth_client: ShopifyOAuthClient) -> Redirect:
redirect_uri = request.url_for("shopify-oauth.callback")
print(f"{redirect_uri=}")
# Create client with shop
auth_url = await shopify_oauth_client.get_authorization_url(redirect_uri)
print(f"{auth_url=}")
return Redirect(path=auth_url, status_code=302)
Trying to get the callback working following the same structure and getting a ValidationError: Expected array, got OAuth2AuthorizeCallback - at $.access_token_state .
class MyOAuthController(Controller):
"""OAuth management."""
dependencies = {
"shopify_oauth_client": Provide(provide_shopify_oauth_client),
}
@post(operation_id="OAuthAuthorize", path=urls.SHOPIFY_OAUTH_AUTHORIZE, name="shopify-oauth.authorize")
async def shopify_authorize(self, request: Request, shopify_oauth_client: ShopifyOAuth2) -> Redirect:
"""Redirect to Shopify OAuth authorize page."""
redirect_uri = request.url_for("shopify-oauth.callback")
print(f"{redirect_uri=}")
auth_url = await shopify_oauth_client.get_authorization_url(redirect_uri)
print(f"{auth_url=}")
return Redirect(path=auth_url, status_code=302)
@get(
operation_id="OAuthCallback",
path=urls.SHOPIFY_OAUTH_CALLBACK,
name="shopify-oauth.callback",
dependencies={"access_token_state": Provide(provide_shopify_oauth_callback)},
)
async def shopify_callback(self, request: Request, access_token_state: AccessTokenState) -> Response:
print(f"{access_token_state=}")
token, state = access_token_state
print(f"{token=}")
print(f"{state=}")
return Response("OK")
These are the 2 dependency provider functions. The second one returns the OAuth2AuthorizeCallback, which is what the example was returning in the guards. Not sure exactly why its throwing an error this way.
async def provide_shopify_oauth_client(request: Request, shop: str = Parameter(query="shop", required=True)) -> ShopifyOAuth2:
return ShopifyOAuth2(
client_id=settings.app.SHOPIFY_OAUTH2_CLIENT_ID,
client_secret=settings.app.SHOPIFY_OAUTH2_CLIENT_SECRET,
shop=shop,
scopes=["read_all_orders", "read_customers", "read_orders", "read_products"],
)
async def provide_shopify_oauth_callback(request: Request, shop: str = Parameter(query="shop", required=True)) -> OAuth2AuthorizeCallback:
shopify_oauth_client = ShopifyOAuth2(
client_id=settings.app.SHOPIFY_OAUTH2_CLIENT_ID,
client_secret=settings.app.SHOPIFY_OAUTH2_CLIENT_SECRET,
shop=shop,
scopes=["read_all_orders", "read_customers", "read_orders", "read_products"],
)
return OAuth2AuthorizeCallback(shopify_oauth_client, route_name="shopify-oauth.callback")
Got it working by modifying the callback provider and returning the result, rather than the instance itself.
async def provide_shopify_oauth_callback(
request: Request,
shop: str = Parameter(query="shop", required=True),
code: str | None = Parameter(query="code", required=False),
code_verifier: str | None = Parameter(query="code_verifier", required=False),
callback_state: str | None = Parameter(query="state", required=False),
error: str | None = Parameter(query="error", required=False),
) -> AccessTokenState:
print(f"Received shop parameter: {shop}")
shop = shop.split('.myshopify.com')[0]
print(f"{shop = }")
shopify_oauth_client = ShopifyOAuth2(
client_id=settings.app.SHOPIFY_OAUTH2_CLIENT_ID,
client_secret=settings.app.SHOPIFY_OAUTH2_CLIENT_SECRET,
shop=shop,
scopes=["read_all_orders", "read_customers", "read_orders", "read_products"],
)
callback = OAuth2AuthorizeCallback(shopify_oauth_client, route_name="shopify-oauth.callback")
return await callback(request, code, code_verifier, callback_state, error)