gnikyt / laravel-shopify

A full-featured Laravel package for aiding in Shopify App development
MIT License
1.24k stars 374 forks source link

Auth Flow Refactoring #744

Closed gnikyt closed 3 years ago

gnikyt commented 3 years ago

Dev Branch: [feature/cookieless]

Introduction

Shopify is now requiring apps to work with session tokens (JWT).

This package has changed several times due to requirement changes with either Shopify or browsers.

At this point, a restructure will be worked on as a priority for authentication flow and validation.

Off the top of my head as some planning, the following will be done and worked on:

Removal of cookies & JWT first-class

Still toying with this in my head, however, while the recent releases helps with cookie issues by handling ITP, its a messy solution and also breaks a good userflow when they see the warnings pop up to enable cookies.

Currently, I see abolishing all cookie and ITP related code and switch to JWT only.

This would involve the removal of ITP views, ITP middleware, cookie class, a good chunk of AuthShopify middleware, and more.

In place, we can promote AuthToken middleware to the forefront, and modify it to utilize Laravel's request context so place in the decoded JWT data into the request lifecycle.

This will allow for the JWT data to be available within the code and views, allowing us to then lookup the user based on the JWT data by request, instead of cookies.


A lot of work will be required to complete this, please be patient. If someone sees an issue with promoting JWT to the forefront, please let me know.

gnikyt commented 3 years ago

Open to input here as I'm stuck. The token stuff is fine for SPAs but if I am to remove dependence on cookies, then for non-SPAs, this would be an issue as the token won't be in the next request.

Currently the only thought I had was to have the token appended on URLs and added to forms with helpers. So all links and forms used will have the token. Something like:

{{ token_url('/orders/search') }}
{{ token_url(route('home')) }}

Open to suggestions if others think there's a better way.

ghost commented 3 years ago

Why not store the token in a database like laravel sanctum does?

gnikyt commented 3 years ago

Database is useless. We store the offline token there now, but that doesn't tell Laravel who it is. We can't access the correct information without knowing who.. and if going cookie-less route, this information is in the token.

For SPAs, this is no issue since you just issue the token in the headers with the AJAX requests.

For non-SPAs, this is an issue. Currently my only solution, which seems to be used by other Laravel projects utlizing tokens, is to check for it in the query param.

When I get some time this weekend, I'll be diving in to try all this out.

alexweissman commented 3 years ago

Why not just store the token in some form of client-side storage, and then have a client-side helper that injects it as an Authorization: Bearer header with each request? This would basically be a roll-your-own-cookie without the restrictions imposed on actual third-party cookies.

arvesolland commented 3 years ago

Related to this - is there a good step-by-step guide or docs around how to use this package with JWT for a Laravel SPA?

LHongy commented 3 years ago

What about use turbolink to make non-SPAs acting like SPAs? so it can use ajax requests which can issue token in the request header, is this a possible solution? @alexweissman I think for non-SPAs, the requests will always be non-ajax, non-ajax requests can't change the request header, right?

diemah77 commented 3 years ago

What about use turbolink to make non-SPAs acting like SPAs? so it can use ajax requests which can issue token in the request header, is this a possible solution? @alexweissman I think for non-SPAs, the requests will always be non-ajax, non-ajax requests can't change the request header, right?

In our Shopify apps we use InertiaJS to handle frontend with backend routing. I guess it might be a better solution to turbolinks.

gnikyt commented 3 years ago

I would rather avoid turbolinks. I don't see an issue with it as part of a post or query param.. it appears many other projects do this. With a blade helper function to automatically append it to the url or form, then it should be fine.

On Fri., Mar. 26, 2021, 08:15 diemah77, @.***> wrote:

What about use turbolink to make non-SPAs acting like SPAs? so it can use ajax requests which can issue token in the request header, is this a possible solution? @alexweissman https://github.com/alexweissman I think for non-SPAs, the requests will always be non-ajax, non-ajax requests can't change the request header, right?

In our Shopify apps we use InertiaJS to handle frontend with backend routing. I guess it might be a better solution to turbolinks.

— You are receiving this because you were assigned. Reply to this email directly, view it on GitHub https://github.com/osiset/laravel-shopify/issues/744#issuecomment-808113043, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASO4OTFEIP2X4YNNLQLOTTTFRQTHANCNFSM4ZLA3ICA .

alexweissman commented 3 years ago

Isn't it a rather significant security issue to expose access tokens in query string parameters? See https://owasp.org/www-community/vulnerabilities/Information_exposure_through_query_strings_in_url

Seems like authentication in non-SPA contexts has been a major blind spot in the rush to banish third-party cookies from browsers.

darrynten commented 3 years ago

Alex is correct, JWTs should not be passed in query params, they should only ever be in the Authorization header.

gnikyt commented 3 years ago

I'm open to suggestions on how to handle for non-SPAs then. I've been given a good hint that the end goal is to eventually force apps to completely remove dependence on cookies, good to plan for this now than later.

darrynten commented 3 years ago

For non-SPAs, this is an issue. Currently my only solution, which seems to be used by other Laravel projects utlizing tokens, is to check for it in the query param.

You should fetch a fresh token from the frontend app instance before posting, and Shopify will always give you a fresh, valid token so the backend never needs to know who the token is for, you just decode a valid token from Shopify and the URL inside will be valid.

If you mean wanting to know the token from a get request, there isn't really much more of an option than adding it to the query params.

Ideally non-SPA would add something like @hotwired/turbo (not turbolinks) and add a few lines of code to make it work with fetch, but if Turbo is not an option then query params are pretty much your only solution.

diemah77 commented 3 years ago

@osiset Are you planning to consume tokens issued by Shopify App Bridge or use a custom implementation like Laravel Sanctum?

darrynten commented 3 years ago

@diemah77 the ones issued by Shopify, which are already supported on the auth.token middleware.

darrynten commented 3 years ago

Related to this - is there a good step-by-step guide or docs around how to use this package with JWT for a Laravel SPA?

Just add the JWT you get to an Authorization header and send it to a auth.token route and it will work.

darrynten commented 3 years ago

The announcement from Shopify that apps must be using JWT in order to be considered for listing.

gnikyt commented 3 years ago

I've decided to go ahead and kill off ITP and cookie dependencies. Will be pushing tokens as the solution. For non-SPAs, since theres no much choice on plain GET/POST, then it'll have to do as a trade off. I'll leave it up to the developer to modify if they are not happy with it as a param.

I'll begin this work tomorrow on my day off.

gnikyt commented 3 years ago

I've started the work this morning on prototyping this.

So initially if no token, then the user will be taken to a route (still behind the auth.shopify middleware) which loads app bridge and gets a token.

Then, redirected to home route with the token and verified (auth.shopify + auth.token) to the home route where you can then use the token with your requests for SPAs or use a Blade helper for appending to URLs.

So far that's what I have planned..

diemah77 commented 3 years ago

So auth.shopify will then basically varify hmac?

gnikyt commented 3 years ago

It will handle everything it currently does yes.

mkmaker commented 3 years ago

Hi @osiset thanks for this awesome package.

Any estimate do you have in mind for this as I was planning to go live but Shopify now doesn't accept apps without session based auth? Also, any part of this I can help you with let me know and I will issue a PR.

gnikyt commented 3 years ago

No I don't. I am picking at it slowly.

darrynten commented 3 years ago

@osiset

So initially if no token, then the user will be taken to a route (still behind the auth.shopify middleware) which loads app bridge and gets a token

The user should get sent to a route that has no middleware at all, just a basic page that loads app bridge and grabs a token from Shopify

gnikyt commented 3 years ago

@darrynten That's what I have done :) so far so good.

gnikyt commented 3 years ago

I've been busy, but managed to grab a few hours today to work on this.

I've started and pushed some code to create a more simplified and combined middleware to verify hmac, handle tokens, and other loose ends.

I have a route for unauthenticated to init app bridge and get a token.

I have a base there just trying to test some physical scenarios out.

Will update when I have more.

gnikyt commented 3 years ago

Things are going well, I managed to grab a few more hours.

I've removed all ITP code/routes, old session helpers, and cookie code.

I've created a more combined and simplified middleware as mentioned before to handle the bulk of the checks.

Results

Next Steps:

Thanks everyone for patience.

ingalesachin7 commented 3 years ago

@osiset Let us know when this feature will be available to use.

gnikyt commented 3 years ago

New update

I was able to complete the work.

In the attached video I am doing a non-SPA.

https://user-images.githubusercontent.com/2420282/115582271-5be47580-a2a3-11eb-849f-6c79d03340a5.mp4

Next Steps

Rewrite a lot of the unit tests and begin work there.

davodavodavo3 commented 3 years ago

First of all, thank you for your great work Tyler. (I'm using Basic Shopify API)

Can you push this new change so we can check and maybe try to help?

gnikyt commented 3 years ago

You can pull from the feature/cookieless branch, but I don't have upgrade instructions yet completed.

On Sun, Apr 25, 2021 at 17:28, Scorpion @.***> wrote:

First of all, thank you for your great work Tyler. (I'm using Basic Shopify API)

Can you push this new change so we can check and maybe try to help?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/osiset/laravel-shopify/issues/744#issuecomment-826425548, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASO4OVYUXO2GXJOQ5AMIGTTKSXSRANCNFSM4ZLA3ICA.

davodavodavo3 commented 3 years ago

It's okay, no need for instruction. Developer is required to read the code. Unless this app will be bought/managed by Shopify and they can protect us of any vulnerabilities.

By the way I suggest adding some tipping link to your project. I believe many people are using your app and earn good money.

So I believe they can tip you a small amount of that earnings.

Thank you.

davodavodavo3 commented 3 years ago

@osiset Do you think calling invocable classes will be more beautiful using other technic instead of call_user_func()? Instead of $result = call_user_func( $this->installShopAction, ShopDomain::fromNative($request->get('shop')), $request->query('code') );

Maybe use

$result = (new installShop())->__invoke(ShopDomain::fromNative($request->get('shop')),, $request->query('code'));

OR

$installShopAction = new installShop(); $result = $installShopAction(ShopDomain::fromNative($request->get('shop')),, $request->query('code'));

If this package is for laravel 7 and UP we can use even this method

($this->installShopAction)(ShopDomain::fromNative($request->get('shop')),, $request->query('code'));

I think in this case we can save some little speed(ms) by declaring/calling the function native instead of calling user func to search for it.

ghost commented 3 years ago

Hi! Am I correct in assuming that the billing is not working at the moment? It uses the ShopSession class, which no longer exists

gnikyt commented 3 years ago

Yes

On Thu., Apr. 29, 2021, 15:05 Vitaly, @.***> wrote:

Hi! Am I correct in assuming that the billing is not working at the moment? It uses the ShopSession class, which no longer exists

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/osiset/laravel-shopify/issues/744#issuecomment-829455822, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASO4OVCSXSJIYCOTNRLN2LTLGKGXANCNFSM4ZLA3ICA .

ghost commented 3 years ago

sent a pull-request with billing


My code probably leaves a lot to be desired, Immediately I apologize. But I think it will be useful for those who already need an application with tokens, but lack of billing stopped them Or for those who want to or already use Turbolinks

gnikyt commented 3 years ago

Excellent!! Thats super helpful. I am taking the afternoon off work to continue working on the unit tests.

Turbolinks support is great.

I'll merge this before I continue.

Probably a bit pre-mature to use this yet in production however until I get all the tests in working order... there may be something overlooked (hopefully not though).

On Wed, May 5, 2021 at 06:32, Vitaly @.***> wrote:

sent a pull-request with billing

My code probably leaves a lot to be desired, Immediately I apologize. But I think it will be useful for those who already need an application with tokens, but lack of billing stopped them Or for those who want to or already use Turbolinks

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/osiset/laravel-shopify/issues/744#issuecomment-832689678, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASO4OS2DU2DGC7KPEH56CDTMFCFNANCNFSM4ZLA3ICA.

squatto commented 3 years ago

I'm working on integrating this into my app today. I'll contribute anything I can, if needed.

I was going to submit my app for review on Monday and then I found out about the token auth requirement, so I may end up submitting my app with the unreleased code. Trial by fire 🔥 😃

squatto commented 3 years ago

I just submitted three PRs:

775 is the most important because if you use route prefixes, the current VerifyShopify middleware won't skip authentication or billing routes.

The other 2 are just convenience and cleanup changes.

squatto commented 3 years ago

Or for those who want to or already use Turbolinks

@Enmaboya in the turbolinks:request-start event listener, why do you have it making a request and not just setting the authorization header? Will this cause the original Turbolinks request to also run, leading to 2 requests? Will it cause POST requests to run as GET, since you are calling xhr.open('GET', ...)?

From what I can gather from the Shopify docs here and here and the Turbolinks docs here, you should get the token (async) on page load, store it, and only set the request header in the turbolinks:request-start event listener (and allow the Turbolinks request to run). The Shopify docs also start an interval/timer to retrieve the token every 2 seconds, which I'm assuming is to ensure that the token doesn't expire.

Sorry for the barrage of questions 😬

Thanks!

davodavodavo3 commented 3 years ago

@osiset Does the authenticate.token router is failing in loop because of verify.shopify middleware?

squatto commented 3 years ago

@davodavodavo3 are you using the default package routes with no prefix or have you customized your routes? If you have set a route prefix, you are going to get stuck in a loop. I've submitted a fix (PR #775) but it hasn't been merged yet.

davodavodavo3 commented 3 years ago

Yeah, I used a prefix to separate the shopify app from laravel jetstream.

Great I see. By the way, what do you think about separating stores tables from users?

That approach is limiting the existing laravel projects

squatto commented 3 years ago

@davodavodavo3 it's hacky, but if you change this line:

https://github.com/osiset/laravel-shopify/blob/f0febc8c986fc2feffe0429a9476fac19f16cdad/src/ShopifyApp/Http/Middleware/VerifyShopify.php#L109

To this:

if (Str::contains($request->getRequestUri(), ['/authenticate', '/billing'])) {

You will no longer get stuck in a redirect loop. You'll be changing it in your vendor directory, which of course will be overwritten when you update, but it will at least get you moving forward again.

squatto commented 3 years ago

@davodavodavo3 I'm only running a single app with my codebase so I haven't run into any issues with users being the stores. I can certainly see the downsides to doing it that way, but as of right now I don't have any reason to move in that direction.

squatto commented 3 years ago

This really makes this a pain to deal with 🤦🏼‍♂️

image

gnikyt commented 3 years ago

Status Update

Thanks everyone for the help with the various PRs.

I had a few hours today to tackle the unit tests.

As of now, the tests are passing locally and externally on Actions with no failures, took a lot to get to this point finally.

Next Steps

  1. Review tests and coverage
  2. Re-implement per-user support somehow
  3. Do more physical tests

Thanks!

squatto commented 3 years ago

Thanks @osiset! I'm glad that I can contribute to your already-amazing package. I'll keep doing all that I can 👍🏻

gnikyt commented 3 years ago

This really makes this a pain to deal with 🤦🏼‍♂️

image

Yes, not an issue for SPAs as you can get a new token easily, but with non-SPAs its a killer. I am looking at adding a JS function for non-SPAs to fetch and update the token URL upon click/submit. I will be testing this tomorrow more than likely.

squatto commented 3 years ago

I am looking at adding a JS function for non-SPAs to fetch and update the token URL upon click/submit

I am implementing that into my app literally as we speak, in fact. It's the only way to make this remotely possible 🤦🏼‍♂️

Once I get the initial version of my app submitted and approved, I'm going to take a few days and convert everything to use Inertia.js. I hate having to do hacky things just to make something work like normal.

squatto commented 3 years ago

@osiset Are you able to submit POST forms? I get a 419 error no matter what I do for some reason. Regular forms that have always been there and that worked before switching to this branch. My first instinct was a collision with the CSRF token but the field names are different (_token vs token). Removing the Shopify session token field still results in a 419.

I'm digging into it right now, but wanted to check with you in case there's something obvious I missed 🤔