Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Allow offline sessions in loadCurrentSession
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Mar 1, 2021
1 parent 89ea780 commit ac9c03c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## Unreleased
- Minor text/doc changes
- Added `2021-01` API version to enum. [#117](https://github.com/shopify/shopify-node-api/pull/117)
- Allow retrieving offline sessions using `loadCurrentSession`. [#119](https://github.com/shopify/shopify-node-api/pull/119)

## [1.0.0]

Expand Down
10 changes: 5 additions & 5 deletions docs/usage/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,16 @@ You can use the `Shopify.Utils.loadCurrentSession()` method to load an online se

As mentioned in the previous sections, you can use the OAuth methods to create both offline and online sessions. Once the process is completed, the session will be stored as per your `Context.SESSION_STORAGE` strategy, and can be retrieved with the below utitilies.

- To load an online session:
- To load a session, you can use the following method. You can load both online and offline sessions from the current request / response objects.
```ts
await Shopify.Utils.loadCurrentSession(request, response)
await Shopify.Utils.loadCurrentSession(request, response, isOnline);
```
- To load an offline session:
- If you need to load a session for a background job, you can get offline sessions directly from the shop.
```ts
await Shopify.Utils.loadOfflineSession(shop)
await Shopify.Utils.loadOfflineSession(shop);
```

The library supports creating both offline and online sessions for the same shop, so it is up to the app to call the appropriate loading method depending on its needs.
**Note**: the `loadOfflineSession` method does not perform any validations on the `shop` parameter. You should never called it from user inputs or URLs.

## Detecting scope changes

Expand Down
17 changes: 14 additions & 3 deletions src/auth/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,15 @@ const ShopifyOAuth = {
/**
* Extracts the current session id from the request / response pair.
*
* @param request HTTP request object
* @param request HTTP request object
* @param response HTTP response object
* @param isOnline Whether to load online or offline sessions
*/
getCurrentSessionId(request: http.IncomingMessage, response: http.ServerResponse): string | undefined {
getCurrentSessionId(
request: http.IncomingMessage,
response: http.ServerResponse,
isOnline = true,
): string | undefined {
let currentSessionId: string | undefined;

if (Context.IS_EMBEDDED_APP) {
Expand All @@ -239,13 +244,19 @@ const ShopifyOAuth = {
}

const jwtPayload = decodeSessionToken(matches[1]);
currentSessionId = this.getJwtSessionId(jwtPayload.dest.replace(/^https:\/\//, ''), jwtPayload.sub);
const shop = jwtPayload.dest.replace(/^https:\/\//, '');
if (isOnline) {
currentSessionId = this.getJwtSessionId(shop, jwtPayload.sub);
} else {
currentSessionId = this.getOfflineSessionId(shop);
}
}
}

// We fall back to the cookie session to allow apps to load their skeleton page after OAuth, so they can set up App
// Bridge and get a new JWT.
if (!currentSessionId) {
// We still want to get the offline session id from the cookie to make sure it's validated
currentSessionId = this.getCookieSessionId(request, response);
}

Expand Down
8 changes: 5 additions & 3 deletions src/utils/delete-current-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import * as ShopifyErrors from '../error';
/**
* Finds and deletes the current user's session, based on the given request and response
*
* @param req Current HTTP request
* @param res Current HTTP response
* @param request Current HTTP request
* @param response Current HTTP response
* @param isOnline Whether to load online or offline sessions
*/
export default async function deleteCurrentSession(
request: http.IncomingMessage,
response: http.ServerResponse,
isOnline = true,
): Promise<boolean | never> {
Context.throwIfUninitialized();

const sessionId = ShopifyOAuth.getCurrentSessionId(request, response);
const sessionId = ShopifyOAuth.getCurrentSessionId(request, response, isOnline);
if (!sessionId) {
throw new ShopifyErrors.SessionNotFound('No active session found.');
}
Expand Down
8 changes: 5 additions & 3 deletions src/utils/load-current-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import {Session} from '../auth/session';
/**
* Loads the current user's session, based on the given request and response.
*
* @param req Current HTTP request
* @param res Current HTTP response
* @param request Current HTTP request
* @param response Current HTTP response
* @param isOnline Whether to load online or offline sessions
*/
export default async function loadCurrentSession(
request: http.IncomingMessage,
response: http.ServerResponse,
isOnline = true,
): Promise<Session | undefined> {
Context.throwIfUninitialized();

const sessionId = ShopifyOAuth.getCurrentSessionId(request, response);
const sessionId = ShopifyOAuth.getCurrentSessionId(request, response, isOnline);
if (!sessionId) {
return Promise.resolve(undefined);
}
Expand Down
38 changes: 38 additions & 0 deletions src/utils/test/delete-current-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {Session} from '../../auth/session';
import {JwtPayload} from '../decode-session-token';
import deleteCurrentSession from '../delete-current-session';
import loadCurrentSession from '../load-current-session';
import {ShopifyOAuth} from '../../auth/oauth/oauth';

jest.mock('cookies');

Expand Down Expand Up @@ -67,6 +68,43 @@ describe('deleteCurrenSession', () => {
await expect(loadCurrentSession(req, res)).resolves.toBe(undefined);
});

it('finds and deletes the current offline session when using cookies', async () => {
Context.IS_EMBEDDED_APP = false;
Context.initialize(Context);

const req = {} as http.IncomingMessage;
const res = {} as http.ServerResponse;

const cookieId = ShopifyOAuth.getOfflineSessionId('test-shop.myshopify.io');

const session = new Session(cookieId);
await expect(Context.SESSION_STORAGE.storeSession(session)).resolves.toEqual(true);

Cookies.prototype.get.mockImplementation(() => cookieId);

await expect(deleteCurrentSession(req, res, false)).resolves.toBe(true);
await expect(loadCurrentSession(req, res, false)).resolves.toBe(undefined);
});

it('finds and deletes the current offline session when using JWT', async () => {
Context.IS_EMBEDDED_APP = true;
Context.initialize(Context);

const token = jwt.sign(jwtPayload, Context.API_SECRET_KEY, {algorithm: 'HS256'});
const req = {
headers: {
authorization: `Bearer ${token}`,
},
} as http.IncomingMessage;
const res = {} as http.ServerResponse;

const session = new Session(ShopifyOAuth.getOfflineSessionId('test-shop.myshopify.io'));
await expect(Context.SESSION_STORAGE.storeSession(session)).resolves.toEqual(true);

await expect(deleteCurrentSession(req, res, false)).resolves.toBe(true);
await expect(loadCurrentSession(req, res, false)).resolves.toBe(undefined);
});

it('throws an error when no cookie is found', async () => {
Context.IS_EMBEDDED_APP = false;
Context.initialize(Context);
Expand Down
38 changes: 37 additions & 1 deletion src/utils/test/load-current-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as ShopifyErrors from '../../error';
import {Session} from '../../auth/session';
import {JwtPayload} from '../decode-session-token';
import loadCurrentSession from '../load-current-session';
import {ShopifyOAuth} from '../../auth/oauth/oauth';

jest.mock('cookies');

Expand Down Expand Up @@ -74,7 +75,7 @@ describe('loadCurrentSession', () => {
const session = new Session(`test-shop.myshopify.io_${jwtPayload.sub}`);
await expect(Context.SESSION_STORAGE.storeSession(session)).resolves.toEqual(true);

await expect(loadCurrentSession(req, res)).resolves.toEqual(session);
await expect(loadCurrentSession(req, res, true)).resolves.toEqual(session);
});

it('loads nothing if no authorization header is present', async () => {
Expand Down Expand Up @@ -136,4 +137,39 @@ describe('loadCurrentSession', () => {

await expect(loadCurrentSession(req, res)).resolves.toEqual(session);
});

it('loads offline sessions from cookies', async () => {
Context.IS_EMBEDDED_APP = false;
Context.initialize(Context);

const req = {} as http.IncomingMessage;
const res = {} as http.ServerResponse;

const cookieId = ShopifyOAuth.getOfflineSessionId('test-shop.myshopify.io');

const session = new Session(cookieId);
await expect(Context.SESSION_STORAGE.storeSession(session)).resolves.toEqual(true);

Cookies.prototype.get.mockImplementation(() => cookieId);

await expect(loadCurrentSession(req, res, false)).resolves.toEqual(session);
});

it('loads offline sessions from JWT token', async () => {
Context.IS_EMBEDDED_APP = true;
Context.initialize(Context);

const token = jwt.sign(jwtPayload, Context.API_SECRET_KEY, {algorithm: 'HS256'});
const req = {
headers: {
authorization: `Bearer ${token}`,
},
} as http.IncomingMessage;
const res = {} as http.ServerResponse;

const session = new Session(ShopifyOAuth.getOfflineSessionId('test-shop.myshopify.io'));
await expect(Context.SESSION_STORAGE.storeSession(session)).resolves.toEqual(true);

await expect(loadCurrentSession(req, res, false)).resolves.toEqual(session);
});
});

0 comments on commit ac9c03c

Please # to comment.