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

Allow loading offline sessions from loadCurrentSession #119

Merged
merged 1 commit into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 avoid calling it from user inputs or URLs.

## Detecting scope changes

Expand Down
22 changes: 17 additions & 5 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 (default) or offline sessions (optional)
*/
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,20 @@ 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.
// Non-embedded apps will always load sessions using cookies. However, we fall back to the cookie session for
// embedded apps 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 (default) or offline sessions (optional)
*/
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 (default) or offline sessions (optional)
*/
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
36 changes: 36 additions & 0 deletions 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 @@ -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);
});
});