Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT REQUEST] Add an example of a route using proxy authenticated proxy route #295

Closed
blanklob opened this issue Aug 8, 2023 · 43 comments

Comments

@blanklob
Copy link
Contributor

blanklob commented Aug 8, 2023

Would be really appreciated to provide an example route (or documentation for developers) for integrating a proxy route app/proxy that can be queried from an online store channel.

-- Idea --

A good example would include a simple authenticated fetch request to update customer data from a theme app extension or a metafield ressource on the customer that is created by the app from an app block.

@sam-masscreations
Copy link

+1

@byrichardpowell
Copy link
Contributor

Thanks for opening this issue.

We think this is something we missed but we want to make it as easy as possible. We think we'll add a new API to authenticate requests from a storefront context.

To help us design this, please can you confirm that you are referring to App proxy requests from a storefront: https://shopify.dev/docs/apps/online-store/app-proxies

If you can, we'll make this a priority.

@blanklob
Copy link
Contributor Author

Yes that will be so cool.

And yes Im talking about those app proxies.

@pkyek1
Copy link

pkyek1 commented Aug 18, 2023

any update ??

@byrichardpowell
Copy link
Contributor

Hi @pkyek1 I am hoping to prioritize it after the unauthenticated admin client ships, which seems to be affecting more people right now.

@ericnkatz
Copy link

Excited to see this making progress! Thanks @byrichardpowell!

@huykon
Copy link

huykon commented Aug 22, 2023

I also need this, thanks @byrichardpowell

@simone-boa-ideas
Copy link

That would be really cool to have :) I'm also trying to figure out how to make a proxy route with remix
Thank you @byrichardpowell

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Aug 22, 2023

Hey everyone 👋

Please can you confirm the following API suits your needs:

// app/routes/**/*.jsx
import {authenticate} from "~/shopify.server"

export async function loader({ request }) {
  const {admin, session} = await authenticate.public.storefrontAppProxy(request);

  admin.graphql
  admin.rest
  admin.rest.resources 

  // ... etc
}

If possible, I would also be grateful for a sentence or two that describes you use case.

Thanks in advance 🙏

@sam-masscreations
Copy link

@byrichardpowell Thanks for getting back so quickly! Really appreciate your fast turn around of the Remix changes!

My use case I need to create discount codes using a third party loyalty points system.
I have an app block which will get available points. I need the request to get the current logged in customer to make sure points are available in the third party DB. If available, then i will create a Discount code and send back to the front end

@huykon
Copy link

huykon commented Aug 23, 2023

Hi @byrichardpowell Thank for you & team for the fast development.

My use case is I want client call to this request will required pass this authentication. Then we have a param or function returned to we can call an Admin API or Storefront API (Rest or Graphql). At this moment of my project, I want to call storefront api product by variant id param.

Thanks for advance!

@byrichardpowell
Copy link
Contributor

Thanks @sam-masscreations . That makes sense.

@huykon Are you able to achieve what you need using the admin API? We think we should return a storefront API client, but that might take us a little longer, because it's a new concept for the package.

@huykon
Copy link

huykon commented Aug 23, 2023

@byrichardpowell yes I think to grap admin API, our package is supporting now, I want to get storefront API at my app proxy remix loader route so do you have idea to get it?

@byrichardpowell
Copy link
Contributor

Hey @huykon,

Right now @shopify/shopify-app-remix doesn't support the storefront API, but I think you can get a storefront client using @shopify/shopify-api-js, which the Remix package is built upon. Docs here: https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Storefront.md

I'm fairly sure we'll be adding the storefront client to the Remix package, but hopefully the docs above can be helpful in the meantime.

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Sep 1, 2023

Hey everyone 👋

I think we are going to tackle this in two releases.

Release 1
Allows you to authenticate requests:

import {authenticate} from "~/shopify.server"

export async function loader({ request }) {
  // Throws Response if request is not valid
  await authenticate.public.appProxy(request);

  const {searchParams} = new URL(request)
  const customerId = searchParams.get("logged_in_customer_id")

  // Do whatever you need here
}

This satisfies the use case where you do not need Shopify's API's, as described by @sam-masscreations:

... create discount codes using a third party loyalty points system. I have an app block which will get available points. I need the request to get the current logged in customer to make sure points are available in the third party DB. If available, then i will create a Discount code and send back to the front end

This will ship when this PR ships (hopefully next week).

Release 2
Return a storefront client. We are envisioning something like:

// app/shopify.server.js
const shopify = shopifyApp({
    // Specify the token to use for storefront access tokens
    // privateAppStorefrontAccessToken will also work, but will marked as deprecated.
    privateStorefrontAccessToken: "12abc"
    // etc.
})
// app/routes/**/*.js
import {authenticate} from "~shopify.server"
import { json } from "@remix-run/node";

export async function loader() {
    const {storefront} = authenticate.public.appProxy(request)
    const response = storefront.graphql("...QUERY", {
        variables: {
            "my": "variable"
        }
    })
    const data = await response.json();

    return json(data)
}

We are still figuring out the best DX here, but would love your input if anything stands out.

What about Admin API Access?
We think we should not return an admin API client here. App Proxy requests are requests from public URL's so we think the storefront API is a much safer fit.

Would love to hear your thoughts and any counterpoints here.

@blanklob
Copy link
Contributor Author

blanklob commented Sep 1, 2023

Hey, thanks for considering this.

Online Store Example

I'm wondering what an example of the theme app extension would look like from a liquid snippet for instance.

Admin Access

App proxies are often used to update specific customer data or metadata (metafields/metaobjects). How is this possible with the current approach using the SF API?

Thanks!

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Sep 5, 2023

Thanks @blanklob

Online Store Example

I'm wondering what an example of the theme app extension would look like from a liquid snippet for instance.

Do you mean how Remix would respond with liquid? If so, I assume something like:

import {authenticate} from "~shopify.server"

export async function loader() {
    authenticate.public.appProxy(request)

    return new Response("Liquid code goes here", {status: 200})
}

Is that what you mean?

Admin Access

App proxies are often used to update specific customer data or metadata (metafields/metaobjects). How is this possible with the current approach using the SF API?

Good point. I'm going to chat with a few people internally about this. Technically you could do this:

export const loader = async ({ request }) => {
 await authenticate.public.appProxy(request);

  const { searchParams } = new URL(request.url);
  const shop = searchParams.get("shop");
  const {admin} = await unauthenticated.admin(shop);

  admin.graphql()

  // ...etc
};

But it may be better if we return the admin context directly from authenticate.public.appProxy if mutating metafields is a common use case. I'm going to chat to a few people internally to confirm.

@blanklob
Copy link
Contributor Author

blanklob commented Sep 6, 2023

Awesome thanks!

I wan't talking about returning Liquid I was talking how would the fetch() request would look like from the the online store theme?

@itszoose
Copy link

itszoose commented Sep 7, 2023

@byrichardpowell Thanks for being active on this.

I am not able to use the .appProxy function on the latest template release:
authenticate.public.appProxy(request);

I can only use .public or .admin or .webhook functions, but no .appProxy in sight, Am I missing something?

Note: I am new to Remix and overall Shopify App development so any info you can provide is highly appreciated.

@byrichardpowell
Copy link
Contributor

Hey @itszoose

The .appProxy method hasn't been released yet. I'm targeting the next release though.

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Sep 7, 2023

Update
Sorry this is taking a little longer than I'd like. I needed to chat to some domain experts first. Here is what we've landed on for the first release:

import {authenticate} from "~/shopify.server"

export async function loader({request}) {
  const {admin, session, liquid} = authenticate.public.appProxy(request)

  // If there is a session for the shop you can access admin API's
  // admin.graphql()
  // admin.rest.get()
  // admin.rest.resources
  // If there is no session, these properties will be undefined

  // The signature of liquid function matches Remix's json function:
  // https://remix.run/docs/en/main/utils/json
  // Except it takes a 3rd argument `{layout: false}`
  // If passed Shopify will render the liquid Response outside of themes layout. 
  return liquid("Hello {{shop.name}}")
}

This means we will return the admin API clients if we can.

@blanklob
Copy link
Contributor Author

blanklob commented Sep 7, 2023

This will really be cool if shipped can't wait!

How is the online store fetch will look like. I am assuming something like this?

export async function getSomething() {
  const response = await fetch('/app/proxy', {
      method: 'GET',
      headers: ??
  });
  const data = await response.text()
  return data;
}

@byrichardpowell
Copy link
Contributor

@blanklob Yep, exactly. Worth noting that your Remix app could respond with JSON. So if you want it access your app proxy from a fetch request you don't have to return liquid.

@itszoose
Copy link

itszoose commented Sep 7, 2023

@byrichardpowell Does that mean that in the meantime we have no way of authenticating the proxy requests?

Also if you could shed some light on what the authenticate.public() method does, and whether I can trust it to block requests that aren't coming from the store's front end? Thank you.

@blanklob
Copy link
Contributor Author

blanklob commented Sep 7, 2023

@byrichardpowell how is the request is authenticated from an Online Store call is there any headers that we should be aware of?

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Sep 7, 2023

@itszoose

Does that mean that in the meantime we have no way of authenticating the proxy requests?

Unfortunately. Really sorry about that. We should release a new version of @shopify/shopify-app-remix early next week.

Also if you could shed some light on what the authenticate.public() method does, and whether I can trust it to block requests that aren't coming from the store's front end? Thank you.

authenticate.public() is really for authenticating checkout extension requests. It won't work for app proxy requests. We made a mistake here because that's not clear in the name. Eventually we'll deprecate authenticate.public() in favor of authenticate.public.checkout(), but for the time being authenticate.public() will continue to work.

@byrichardpowell
Copy link
Contributor

@blanklob You can read about how to auth an app proxy request here: https://shopify.dev/docs/apps/online-store/app-proxies

The WIP implementation of this is:

  1. In the Remix Package: https://github.com/Shopify/shopify-app-js/blob/42f4b2c5555c83a3e0315939b9c23ac7821aeeaf/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.ts#L22-L40
  2. Which calls this: https://github.com/Shopify/shopify-api-js/blob/main/lib/utils/hmac-validator.ts#L47

As far as I can see, I don't think there are any special headers involved in authentication. It's all URL Params which are added by Shopify.

@itszoose
Copy link

itszoose commented Sep 8, 2023

@byrichardpowell Does that mean that I can use the .public method for authenticating non-proxy requests originating from the front end, or is it strictly for the checkout extension requests and can't authenticate requests originating from elsewhere on the store?

@byrichardpowell
Copy link
Contributor

@itszoose It's strictly for checkout requests. Imagine it's called authenticate.public.checkout not authenticate.public

@byrichardpowell
Copy link
Contributor

byrichardpowell commented Sep 11, 2023

Hey everyone 👋

We've released version 1.2.1 of @shopify/shopify-app-remix (Changelog). This version contains a new API for authenticating app proxy requests.

Please upgrade @shopify/shopify-app-remix to 1.2.1

Returning Liquid Responses

// app/routes/**\/.ts
import {authenticate} from '~/shopify.server';

export async function loader({request}) {
  const {liquid} = authenticate.public.appProxy(request);

  return liquid('Hello {{shop.name}}');
}

Using the admin GraphQL API

// app/routes/**\/.ts
import {authenticate} from '~/shopify.server';

export async function loader({request}) {
  const {liquid, admin} = authenticate.public.appProxy(request);

  const response = await admin.graphql('QUERY');
  const json = await response.json();

  return json(json);
}

WIP: Storefront API

This release does not contain the storefront API, but that is next on my list. I'm still figuring this out, but it may look something like this:

// app/routes/**\/.ts
import {authenticate} from '~/shopify.server';

export async function loader({request}) {
  const {storefront} = authenticate.public.appProxy(request);

  const response = await storefront.graphql("QUERY GOES HERE")
  const json = await response.json();

  return json(json);
}

You won't be able to use this storefront API until a later release

@blanklob
Copy link
Contributor Author

Awesome thanks!

@LHongy
Copy link

LHongy commented Sep 12, 2023

Hello @byrichardpowell,
Could you show me an example of checkout extension requests? Or guide me to a documentation?
I was looking at this doc, still not sure how it should work.
https://shopify.dev/docs/api/checkout-ui-extensions/2023-07/configuration#network-access

So if the extension make an api call to my app: {appDomain}/api/{resource} , should the api use public auth, like this?

* export async function loader({ request }: LoaderArgs) {
         *   const {sessionToken} = authenticate.public(request);
         *
         *   return json(await getWidgets(sessionToken));
         * }

@byrichardpowell
Copy link
Contributor

Hey @LHongy

That looks correct to me, incase it helps here is a tutorial that includes authenticating a checkout request: https://shopify.dev/docs/apps/checkout/product-offers/post-purchase/getting-started#define-the-offer-and-sign-changeset-endpoints

@LHongy
Copy link

LHongy commented Sep 13, 2023

@byrichardpowell
Does this one also work?
Use authenticate.public on a request from checkout-ui-extension

image
https://shopify.dev/docs/api/checkout-ui-extensions/2023-07/apis/standardapi

@LHongy
Copy link

LHongy commented Sep 14, 2023

Thanks a lot!

@pkyek1
Copy link

pkyek1 commented Sep 17, 2023

@byrichardpowell @blanklob @byrichardpowell

this update is really great

just want to know about admin api post request

the usecase is i am trying to create checkout with admin api

the code
const { admin } = await authenticate.public.appProxy(request);

const checkresponse = await admin?.rest.post({ path:'/checkouts.json',data: {
      line_items:[
        {
          "variant_id": 35344523624,
          "quantity": 5
        }
      ],
      note:"new nitr",
      note_attributes:{
        " wello": "App"
    }
 } 

});

it create checkout but in response line_items is empty array and note is null and note_attribute is also {}

may be there is syntax error or the post request is not working

can anyone help me in this

@pkyek1
Copy link

pkyek1 commented Sep 25, 2023

@byrichardpowell @huykon @huykon

any update ??

@mikesosa
Copy link

mikesosa commented Sep 26, 2023

any updates on this? how to handle the CORS? the other methods like autehnticate.public exposed a cors fucntion to return

@byrichardpowell
Copy link
Contributor

Hey everyone 👋

We've released version 1.3.0 of @shopify/shopify-app-remix. This contains 2 ways to access the storefront API:

App Proxy

import {json} from '@remix-run/node';
import {authenticate} from '~/shopify.server';

export async function loader({request}) {
  const {storefront} = await authenticate.public.appProxy(request);
  const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}');

  return json(await response.json());
}

Unauthenticated Storefront

import {json} from '@remix-run/node';
import {unauthenticated} from '~/shopify.server';
import {customAuthenticateRequest} from '~/helpers';

export async function loader({request}) {
  await customAuthenticateRequest(request);

  const {storefront} = await unauthenticated.storefront(
    'my-shop.myshopify.com',
  );
  const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}');

  return json(await response.json());
}

There is a lot of discussion in this PR, but the original post is about App Proxy. To solve this we've released:

  1. A new authenticate.public.appProxy(request)
  2. A storefront API client (see above).

Given this solves the discussions around App Proxy I'm going to close this issue.


@pkyek1 since this is a question about the GraphQL API please could you open a separate issue or ask in our partner Slack: https://join.slack.com/t/shopifypartners/shared_invite/zt-sdr2quab-mGkzkttZ2hnVm0~8noSyvw

@mikesosa in my experiments CORS wasn't an issue since App Proxy is a server to server request. You should be able to configure your app proxy, and then use authenticate.public.appProxy() without worrying about CORS.

@joelvh
Copy link

joelvh commented Oct 2, 2023

@blanklob or anyone else run into signature validation errors with actions? I've opened Shopify/shopify-app-js#455 with details of what I've run into. Thanks for sharing your thoughts.

@joeainsworth
Copy link

joeainsworth commented Jan 31, 2024

Can anybody help?

I'm trying to return a liquid response from a remix app proxy route using the app template.

import { authenticate } from "../shopify.server";

export async function loader({ request }) {
  const { liquid } = authenticate.public.appProxy(request);

  return liquid("Hello {{shop.name}}");
}

But I'm getting the following error:

10:06:35 │ remix │ TypeError: liquid is not a function

How do I include the liquid function?

@nboliver-ventureweb
Copy link

@joeainsworth Looks like you may be missing an await. I think there may have been an error in the docs previously.

  const { admin, session, liquid } = await authenticate.public.appProxy(
    request,
  );

Unfortunately I found that client side JS doesn't work on app proxy pages, have an issue open here: #436 - hopefully it gets addressed at some point and there's a way to build in client-side JS.

@joeainsworth
Copy link

joeainsworth commented Jan 31, 2024

It seems most all the examples here do not work. I cannot interact with Admin or Storefront API. Even using copy & paste.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests