This application demonstrates how to tweak Spring Security to authorize a Shopify embedded app.
- An environment using Java 24.
- A Shopify development store (you can create one from your Dev Dashboard)
- A Shopify app in the Dev Dashboard that can be used to test this project. You can create one from your Dev Dashboard and accept all the defaults.
- Make note of scopes you want to add to the app
- Maven
- Git clone this project.
- Obtain the following information from your app in the Dev Dashboard:
- client id -> save it in the env. variable
app_client_id - client secret -> save it in the env. variable
app_client_secret
- client id -> save it in the env. variable
- Save the scopes (comma-delimited list, no spaces) in an env. variable:
app_scopes- You can use
write_productsas a test
- You can use
- Set the profile to
dev(e.g. set the env. variable:SPRING_PROFILES_ACTIVE=dev) - cd into the backend module:
cd backend - Start the spring boot app:
mvn spring-boot:run - Create a tunnel to make
localhost:8080publicly accessible. You can use ngrok. - In your Dev Dashboard, click on your app and click
Create a version.- Enter the "App Url":
https://{your-hostname}/app/shopify - Select "Embed app in Shopify admin"
- Add all the scopes that are in the
application.properties - Under "Redirect URLs", add:
https://{your-hostname}/authorized/shopify - Click
Release
- Enter the "App Url":
- In your Dev Dashboarrd, click on your app and under
Home, clickInstall Appand select your test store.
After granting the permissions requested, you should see a welcome page.
These are the app endpoints:
/app/shopify: the app uri
- to access the embedded app (and install)
- if called by Shopify from an embedded app
- and already installed, the request will go through the chain and the SPA will be returned
- if not installed, we initiate the OAuth flow via a Shopify App Bridge redirect (written directly to the response)
- if not called by Shopify (e.g. when App Bridge redirects to break out of the iframe) it always initiates the OAuth flow.
- This requires
shopto be present as a request parameter.
- This requires
/authorized/shopify: the app redirect uri
- called by Shopify during the OAuth flow
The following outlines how this project meets the Shopify requirements for app installation as described here:
- We customize the Spring Security OAuth2 Client to perform the Authorization code grant flow and obtain the token upon installation:
Scenario 1: The shop is being installed: (/app/shopify)
-
Step 1: Verify the installation request: See
ShopifyRequestAuthenticationFilter,ShopifyRequestAuthenticationToken- Embedded:
ShopifyRequestAuthenticationProviderauthenticates the request, but the principal reflects that no OAuth token was found. - Not embedded: the request remains unauthenticated
- Embedded:
-
Step 2: Request authorization code
- In
OAuth2AuthorizationRequestRedirectFilter,ShopifyOAuth2AuthorizationRequestResolverbuilds aOAuth2AuthorizationRequestfor the redirect. We need theshopto build the OAuth uris.- Embedded: The
shopparameter is resolved from theAuthentication. All other params also resolved here. - Not embedded: The
shopparameter is resolved via a query param. All other params also resolved here.
- Embedded: The
ShopifyAuthorizationRequestRedirectStrategychooses where to redirect to.- Embedded: returns a generated html page that will exit the iframe page via an AppBridge redirect to the app uri
- Not embedded: redirects to the authorization uri
- In
-
Step 3: Validate authorization code:
ShopifyOAuth2AuthorizationCodeAuthenticationProvider- Nonce check (nonce sent to authorization uri in query = nonce in current request from Shopify): the nonce sent to the auth server is guaranteed to be the same as the nonce in the cookie. So it is sufficient to only check the cookie.
- Nonce check (cookie = nonce in the query)
CookieOAuth2AuthorizationRequestRepositoryreads theOAuth2AuthorizationRequestsaved in the cookie, which includes the nonce.OAuth2AuthorizationCodeAuthenticationProvidercompares with the nonce in current request params
- HMAC check (already done by
ShopifyRequestAuthenticationFilter) - Check for valid
shopparameter (seeShopifyOAuth2AuthorizationCodeAuthenticationProvider)
-
Step 4: Get an access token:
- insert shop name into token uri (
ShopifyOAuth2AuthorizationCodeAuthenticationProvider) - add parameters to body (already down by default:
RestClientAuthorizationCodeTokenResponseClientandDefaultOAuth2TokenRequestParametersConverter) - process response:
access_tokenandscopevaluesDefaultMapOAuth2AccessTokenResponseConverter(used byRestClientAuthorizationCodeTokenResponseClientto parse the response) correctly extracts these values.- However, it expects to find a token type in the response. We have to add it.
- However, the
scopestring is split with" "as delimiter. We need to use",". We also have to it add the token type to the response before passing it to the default impl.- see
ShopifyMapOAuth2AccessTokenResponseConverter
- see
- Note: if the authorization server responds with an error,
OAuth2AuthorizationCodeGrantFilterwill redirect to the redirect uri with error params. On the second pass, the filter will not match the request as an authorization response and will let the request continue. Further down the filter chain, if this path (redirect uri) requires the user to be authenticated, the AuthorizationFilter will throw anAccessDeniedExceptionbecause the request didn't come from Shopify. - The approved scopes are verified in
ShopifyOAuth2AuthorizationCodeAuthenticationProvider - The default
OAuth2AuthorizedClientRepositoryimplementation (AuthenticatedPrincipalOAuth2...) uses our customAccessTokenServiceto save the token
- insert shop name into token uri (
-
Step 5: Redirect to your app's UI: by default,
OAuth2AuthorizationCodeGrantFilterchecks theRequestCachefor aSavedRequestto determine where to redirect toShopifyAppRequestCachealways returns aSavedRequestwith the redirect url:- the full app url (
/app/shopify?shop={shop}&host={host}) - or to embedded app url (
"https://{base64_decoded_host}/apps/{api_key}/)
- the full app url (
-
See scenario 2.
-
Scenario 2: The shop is already installed, and we have a token (
/app/shopify) -
Step 1: Verify the installation request:
- Embedded:
ShopifyInstallationRequestFilterauthenticates the request - Not embedded: the request is left unauthenticated
- Embedded:
-
OAuth2AuthorizationRequestRedirectFilterdelegates toShopifyOAuth2AuthorizationRequestResolverwhich checks the scope of the token found:- if not all scopes granted, it reverts to scenario 1
- if the scopes match, the OAuth flow is not initiated and the request continues through the chain.
-
Since the request is authenticated, it'll go through the entire chain.
Note:
- if the app is not embedded, the OAuth authorization flow is entered every time.
- the SPA is returned if and only if there is a valid OAuth token for that shop.
- COMING SOON: Spring Security OAuth2 Resource Server will validate the session token
Your database table is expected to have the following schema:
|----------------shopify_oauth_access_tokens----------------|
| |
|----id----shop----access_token----scope----date_created----|
| |
|-----------------------------------------------------------|
An H2 in-memory database is configured to run when running in the dev profile.
If desired, an H2-in-memory database can be configured when running integration tests. The single existing integration test activates the test profile.
- encode the token in DB
- don't forget the webhooks!
- build up
ShopifyAppRequestCacheso that it is a fully functional cookie-based request cache ShopifyAccessTokenscopes should be a set, not a String. Better yet, replace the custom token with the Spring default- Offer a way of authenticating non-embedded requests. Currently the only way is via Shopify (embedded).
- Consolidate the retrieval of the OAuth token in
AccessTokenServiceto perhaps only use theOAuth2AuthorizedClientServiceinterface- (
ShopifyRequestAuthenticationFiltershould useAccessTokenService)
- (
ShopifyRequestAuthenticationFiltershould only allow authenticated access to the redirect uri