Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

JWT Support #601

Merged
merged 41 commits into from
Oct 15, 2020
Merged

JWT Support #601

merged 41 commits into from
Oct 15, 2020

Conversation

darrynten
Copy link
Contributor

@darrynten darrynten commented Oct 12, 2020

Shopify JWT Authentication

Provides new auth.token middleware you can place on API endpoints.

See: https://shopify.dev/tutorials/authenticate-your-app-using-session-tokens

This means you can now build apps without cookies (see #522) and will no longer experience bugs like #443. It also allows for functionality requested for in #526 and #599 because Shopify does most of the hard work now, meaning issues like #588 (and everything else cookie related) is a thing of the past.

Usage

This package only covers the backend functionality changes.

I'm not submitting any frontend changes (creating an external package instead), so follow the frontend instructions in the Shopify documentation.

You can install some Vue-based frontend scaffolding to test this with: UnicornGlobal/laravel-shopify-vue.

Send the JWT you get from Shopify in the Authorization: bearer <xyz> header directly to any API endpoint secured with the auth.token middleware.

Don't forget to get a fresh JWT from Shopify on your frontend every 30 seconds so your token stays valid.

Changes

  • New Middleware for Shopify Auth Tokens auth.token
  • API Endpoints - current user details on /api/me and plan list on /api/plans
  • Split API and Web routes files
  • Added missing API Key and API Secret env variables to phpunit example
  • Updated HMACs in unit tests to work with default phpunit example env variables
  • Bugfix for missing client_id in redirections

The reason the phpunit and HMACs have changed is due to a bug that allowed the application to operate even when there was no Shopify API key set. This PR adds a key and secret to the phpunit.dist.xml file.

You must update your phpunit.xml file to include the missing env vars

Also fixes an issue where the billing screen sometimes didn't know which shop to load.

Resolves #551

@gnikyt
Copy link
Owner

gnikyt commented Oct 13, 2020

Fantastic @darrynten ! Definitely would help out people! I think a small wiki addition on how to set this up would be great after too.

Tests are passing which is awesome, thanks. I was unaware of the billing screen issue, but passing in the input vars as you've done seems like a fine solution.

From my end, stuff looks fine. I'm wondering for the API error responses though, should we just throw an exception? This way it can be handled alternatively by the user if they wish to with a custom render function.

Something like: throw new HttpException(Response::BAD_REQUEST, 'Invalid token') for the errors?

Edit: @darrynten Can you also shoot me an email for an aside thing I wanted to run past you.

@gnikyt gnikyt added the feature Enhancement to the code label Oct 14, 2020
@gnikyt gnikyt self-assigned this Oct 14, 2020
@gnikyt gnikyt self-requested a review October 14, 2020 14:02
@lucasmichot lucasmichot self-requested a review October 14, 2020 14:52
@darrynten
Copy link
Contributor Author

@osiset I've updated the PR to throw exceptions instead of returning plain responses, and ensure that JSON responses are returned if the accept header is set to application/json.

All tests have been updated and additional tests have been added.

@darrynten darrynten mentioned this pull request Oct 15, 2020
@gnikyt
Copy link
Owner

gnikyt commented Oct 15, 2020

@darrynten Thanks, I'll merge it!

@gnikyt gnikyt merged commit a1d6371 into gnikyt:master Oct 15, 2020
@gnikyt
Copy link
Owner

gnikyt commented Oct 15, 2020

I'll look at tryin to add a wiki for this later the week.

@kouloughli-hemza
Copy link

im just confused about the database part , when first opening the app how to deal with installation process ?
do we use the API routes ? or the package process using web , and we use JWT only for installed apps?
im way confused about this point .
thank you

@andreuka
Copy link

Trying to understand how to make it work with server side rendered apps.

@andreuka
Copy link

seems like https://github.com/turbolinks/turbolinks is the way to go

@bilfeldt
Copy link
Contributor

bilfeldt commented Dec 10, 2020

Maybe we can all assist on documenting this awesome feature :)

I am not 100% sure I get how we switch to using JWT as authentication mechanisms in a new or existing project nor when exactly it is recommended or even possible.

As far as I can see the relevant places to document this would be:

Unresolved question that I think is worth documenting:

Authentication in general

There are two aspects of authentication when developing Shopify apps:

  1. A user that is logged into a Shopify store needs to authenticate when interacting with a Shopify App
  2. A Shopify app needs to authenticate to Shopify whenever it makes RestAPI or GraphQL requests

When installing a new app the user goes through a rather cumbersome OAuth process as describe by Shopify here and the documentation of this package here. Essentially using the AuthShopify middleware on a route would ensure that a user is authenticated using OAuth correct?

Once the app is installed correctly using OAuth then a Shopify Api Token will be saved in the password field of the users table. The scope (abilities) of this Api Token is determined in the configuration of this package and determine what information the app can request from Shopify.

Each time the App makes a RestAPI or GraphQL request to Shopify then it uses this API Token to authenticate.

Now that the app is installed and the user has granted the app access to their Shopify account by going through the OAuth flow, then we need to know how to authenticate on every day use. There are mainly four interesting parts to take into account:

  • User clicks on the app in the Shopify app listing
  • User types the URL of the app in the browser directly
  • User clicks a Shopify Admin Link which points to a route on the app
  • Shopify fires a webhook

NOTE: How the result should look depends on wether or not the app is an Embedded App or not.

Now for the use cases we can authenticate them differently:

  • Webhooks: Webhooks contain a X-Shopify-Hmac-SHA256 header which can be verified using the apps shared secret - read more here
  • Admin links: I have no idea how best to authenticate these????
  • Page views: Can use OAuth which causes problems for embedded apps due to new restrictions of 3rd party cookies. An alternative is using JWT Tokens, but this seems to only work properly for Single Page Applications??

@andreuka did you manage to find out how to use this properly on a service side rendered app (most obvious example would be a multi-page app using blade files)?

Do anyone knows how this works with admin links?

@andreuka
Copy link

andreuka commented Dec 10, 2020

@bilfeldt Im using turbolinks to work with it. its not perfect and creating additional redirect when user visits first page of app. but it works.

It would be perfect if we will get middleware which will check for HMAC auth OR Token auth is valid in 1 middleware.
Then it will be perfect solution to work with server render app without initial redirect.

This is sample of code I using at the moment.

Route::get('/', 'Admin\Settings@set_auth')->middleware(['auth.shopify'])->name('home');

class Settings extends Controller{
    public function set_auth(){
        $shop          = Auth::user();
        $shop_id       = $shop->id;

        return view('admin.auth', [
            'shop'          => $shop,
        ]);
    }
}
@extends('layouts.default')

@section('scripts')
    @parent
@endsection
@section('content')
    <div data-link="{{ $url ?? '' }}" id="redir-to">Redirection...</div>
@endsection
<script src="https://www.unpkg.com/turbolinks@5.2.0/dist/turbolinks.js"></script>

var app = createApp({
    apiKey: '{{ config('shopify-app.api_key') }}',
    shopOrigin: shopOrigin,
    forceRedirect: true,
});

var Intervals = {};
var redirectStarted = false;

function redirectIfNeeded() {
    if( redirectStarted ) return false;

    if (sessionToken !== '') {
        var redirTo = $('#redir-to');

        if (location.pathname === '/') {
            redirectStarted = true;
            Turbolinks.visit("/home");
        } else if (redirTo.length > 0) {
            redirectStarted = true;
            var link = redirTo.data('link');
            Turbolinks.visit(link);
        }
    }
}

function updateSessionToken(){
    getSessionToken(app).then(function(_sessionToken){
        sessionToken = _sessionToken;

        localStorage.setItem('sessionToken', sessionToken);

        redirectIfNeeded();

        $.ajaxSetup({
            headers: {
                'Authorization': "Bearer " + sessionToken
            }
        });
    });
}


document.addEventListener("turbolinks:load", function () {
    redirectIfNeeded();
});

@LHongy
Copy link

LHongy commented Dec 17, 2020

@andreuka Hello, I am having a hard time to understand your solution, why does the route has to middleware with auth.shopify? I thought we need an unauthenticated page to retrieve the session token, and then redirect to our authenticated page using that token?

@gnikyt
Copy link
Owner

gnikyt commented Jan 19, 2021

@darrynten Would you have a couple jot notes about implementation for this, off the top of your head, that I can take and turn into a wiki entry?

@diemah77
Copy link

@andreuka Hello, I am having a hard time to understand your solution, why does the route has to middleware with auth.shopify? I thought we need an unauthenticated page to retrieve the session token, and then redirect to our authenticated page using that token?

Because you need to check whether the request to your app is authorized, e.g. if the app is loaded for the first time during the installation process.

@andreuka
Copy link

@andreuka Hello, I am having a hard time to understand your solution, why does the route has to middleware with auth.shopify? I thought we need an unauthenticated page to retrieve the session token, and then redirect to our authenticated page using that token?

Because you need to check whether the request to your app is authorized, e.g. if the app is loaded for the first time during the installation process.

Yeah, but with latest updates its not relevant anymore as there is better way.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature Enhancement to the code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for new JWT auth flow
7 participants