#Shopify OAuth Flow

1 messages · Page 1 of 1 (latest)

chilly dome
#

How can I modify the OAuth flow to dynamically accept a shop id parameter for Shopify? I'm looking at the litestar-fullstack-inertia project and there it hardcodes the OAuth clients, but Shopify requires a shop id (passed in from client side) to initiate the OAuth flow. How would I go about adapting it?

lusty larkBOT
#
Notes for Shopify OAuth Flow
At your assistance

@chilly dome

No Response?

If no response in a reasonable time, ping @Member.

Closing

To close, type !solve or byte solve.

MCVE

Please include an MCVE so that we can reproduce your issue locally.

naive junco
#

#learning message

chilly dome
#

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?

naive junco
#

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)
chilly dome
#

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")
chilly dome
#

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)