sharetribe-flex-sdk 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +22 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +19 -0
- package/CHANGELOG.md +231 -0
- package/LICENSE +201 -0
- package/README.md +76 -0
- package/build/sharetribe-flex-sdk-node.js +13592 -0
- package/build/sharetribe-flex-sdk-web.js +1 -0
- package/docs/README.md +20 -0
- package/docs/authentication.md +179 -0
- package/docs/calling-the-api.md +217 -0
- package/docs/configurations.md +95 -0
- package/docs/developing-sdk.md +131 -0
- package/docs/docpress.json +14 -0
- package/docs/features.md +14 -0
- package/docs/keep-alive.md +48 -0
- package/docs/object-query-parameters.md +36 -0
- package/docs/scripts.js +41 -0
- package/docs/serializing-types-to-json.md +40 -0
- package/docs/sharing-session-between-client-and-server.md +19 -0
- package/docs/styles.css +95 -0
- package/docs/token-store.md +114 -0
- package/docs/try-it-in-browser.md +32 -0
- package/docs/try-it-in-the-playground.md +153 -0
- package/docs/types.md +27 -0
- package/docs/writing-your-own-token-store.md +29 -0
- package/docs/your-own-types.md +61 -0
- package/examples/README.md +5 -0
- package/examples/getting-started-browser/README.md +22 -0
- package/examples/getting-started-browser/index.html +89 -0
- package/examples/getting-started-browser/index.js +156 -0
- package/examples/getting-started-browser/screenshots/screenshot1.png +0 -0
- package/examples/getting-started-browser/screenshots/screenshot2.png +0 -0
- package/examples/getting-started-node/README.md +23 -0
- package/examples/getting-started-node/index.js +139 -0
- package/examples/getting-started-node/screenshots/screenshot.png +0 -0
- package/package.json +83 -0
- package/playground.js +295 -0
- package/src/browser_cookie_store.js +26 -0
- package/src/context_runner.js +151 -0
- package/src/context_runner.test.js +185 -0
- package/src/detect.js +11 -0
- package/src/express_cookie_store.js +57 -0
- package/src/fake/adapter.js +130 -0
- package/src/fake/api.js +137 -0
- package/src/fake/auth.js +84 -0
- package/src/fake/token_store.js +231 -0
- package/src/index.js +25 -0
- package/src/interceptors/.eslintrc.js +5 -0
- package/src/interceptors/add_auth_header.js +32 -0
- package/src/interceptors/add_auth_header.test.js +50 -0
- package/src/interceptors/add_auth_token_response.js +16 -0
- package/src/interceptors/add_client_id_to_params.js +12 -0
- package/src/interceptors/add_client_secret_to_params.js +15 -0
- package/src/interceptors/add_grant_type_to_params.js +23 -0
- package/src/interceptors/add_idp_client_id_to_params.js +17 -0
- package/src/interceptors/add_idp_id_to_params.js +17 -0
- package/src/interceptors/add_idp_token_to_params.js +17 -0
- package/src/interceptors/add_scope_to_params.js +18 -0
- package/src/interceptors/add_subject_token_to_params.js +18 -0
- package/src/interceptors/add_token_exchange_grant_type_to_params.js +12 -0
- package/src/interceptors/auth_info.js +50 -0
- package/src/interceptors/clear_token_after_revoke.js +45 -0
- package/src/interceptors/default_params.js +12 -0
- package/src/interceptors/fetch_auth_token_from_api.js +33 -0
- package/src/interceptors/fetch_auth_token_from_store.js +27 -0
- package/src/interceptors/fetch_refresh_token_for_revoke.js +24 -0
- package/src/interceptors/multipart_request.js +35 -0
- package/src/interceptors/retry_with_anon_token.js +58 -0
- package/src/interceptors/retry_with_refresh_token.js +70 -0
- package/src/interceptors/save_token.js +20 -0
- package/src/interceptors/transit_request.js +27 -0
- package/src/interceptors/transit_request.test.js +58 -0
- package/src/interceptors/transit_response.js +27 -0
- package/src/memory_store.js +19 -0
- package/src/params_serializer.js +65 -0
- package/src/params_serializer.test.js +58 -0
- package/src/sdk.js +894 -0
- package/src/sdk.test.js +908 -0
- package/src/serializer.js +279 -0
- package/src/serializer.test.js +229 -0
- package/src/token_store.js +15 -0
- package/src/types.js +108 -0
- package/src/types.test.js +75 -0
- package/src/utils.js +68 -0
- package/src/utils.test.js +85 -0
- package/webpack.config.babel.js +47 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const generateKey = (clientId, namespace) => `${namespace}-${clientId}-token`;
|
|
2
|
+
|
|
3
|
+
const createStore = ({ clientId, req, res, secure }) => {
|
|
4
|
+
const expiration = 180; // 180 days
|
|
5
|
+
const namespace = 'st';
|
|
6
|
+
const key = generateKey(clientId, namespace);
|
|
7
|
+
|
|
8
|
+
// A mutable variable containing the current token.
|
|
9
|
+
// When a `setToken` is called, the current token will be
|
|
10
|
+
// stored to this variable. `getToken` will read subsequent
|
|
11
|
+
// calls from this variable.
|
|
12
|
+
let currentToken;
|
|
13
|
+
|
|
14
|
+
const readCookie = () => {
|
|
15
|
+
const cookie = req.cookies[key];
|
|
16
|
+
|
|
17
|
+
if (cookie) {
|
|
18
|
+
return JSON.parse(cookie);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const getToken = () => {
|
|
25
|
+
currentToken = currentToken || readCookie();
|
|
26
|
+
|
|
27
|
+
return currentToken;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const setToken = tokenData => {
|
|
31
|
+
currentToken = tokenData;
|
|
32
|
+
const secureFlag = secure ? { secure: true } : {};
|
|
33
|
+
|
|
34
|
+
// Manually stringify tokenData.
|
|
35
|
+
// Express supports passing object to `res.cookie` which will be then automatically
|
|
36
|
+
// JSON stringified. However, we CAN NOT use it, because it seems to output invalid JSON
|
|
37
|
+
// with a "j" tag in front of the content (`"j:{ ...json here... }`). Because we want
|
|
38
|
+
// to read that cookie also in browser, we don't want to produce invalid JSON.
|
|
39
|
+
res.cookie(key, JSON.stringify(tokenData), {
|
|
40
|
+
maxAge: 1000 * 60 * 60 * 24 * expiration,
|
|
41
|
+
...secureFlag,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const removeToken = () => {
|
|
46
|
+
currentToken = null;
|
|
47
|
+
res.clearCookie(key);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
getToken,
|
|
52
|
+
setToken,
|
|
53
|
+
removeToken,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default createStore;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import createTokenStore from './token_store';
|
|
3
|
+
import * as auth from './auth';
|
|
4
|
+
import * as api from './api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
This file implements a fake adapters for testing purposes only.
|
|
8
|
+
|
|
9
|
+
The test responses are copy-pasted from real API responses.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const adapterHelper = adapterDef => config =>
|
|
13
|
+
new Promise((resolve, reject) => {
|
|
14
|
+
const rejectWithError = errorOrResponse => {
|
|
15
|
+
if (errorOrResponse instanceof Error) {
|
|
16
|
+
return reject(errorOrResponse);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const error = new Error(`Request failed with status code ${errorOrResponse.status}`);
|
|
20
|
+
error.response = errorOrResponse;
|
|
21
|
+
|
|
22
|
+
return reject(error);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
adapterDef.call(null, config, resolve, rejectWithError);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const parseAuthorizationHeader = value => {
|
|
29
|
+
if (!_.isString(value)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const splitted = value.split(' ');
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
tokenType: splitted[0],
|
|
37
|
+
accessToken: splitted[1],
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const requireAuth = (config, reject, tokenStore) => {
|
|
42
|
+
const { accessToken, tokenType } = parseAuthorizationHeader(config.headers.Authorization);
|
|
43
|
+
|
|
44
|
+
if (!accessToken && !tokenType) {
|
|
45
|
+
return reject({
|
|
46
|
+
status: 401,
|
|
47
|
+
data: '{}', // FIXME This is not what the server sends
|
|
48
|
+
|
|
49
|
+
__additionalTestInfo: 'Authorization header missing',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validToken = tokenStore.validToken(accessToken, tokenType);
|
|
54
|
+
|
|
55
|
+
if (validToken) {
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return reject({
|
|
60
|
+
status: 401,
|
|
61
|
+
data: '{}', // FIXME This is not what the server sends
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const defaultHandler = (config, resolve, reject, tokenStore) => {
|
|
66
|
+
switch (config.url) {
|
|
67
|
+
case 'api/users/show':
|
|
68
|
+
return requireAuth(config, reject, tokenStore).then(() => api.users.show(config, resolve));
|
|
69
|
+
case 'api/marketplace/show':
|
|
70
|
+
return requireAuth(config, reject, tokenStore).then(() =>
|
|
71
|
+
api.marketplace.show(config, resolve)
|
|
72
|
+
);
|
|
73
|
+
case 'api/listings/search':
|
|
74
|
+
return requireAuth(config, reject, tokenStore).then(() =>
|
|
75
|
+
api.listings.search(config, resolve)
|
|
76
|
+
);
|
|
77
|
+
case 'api/own_listings/create':
|
|
78
|
+
return requireAuth(config, reject, tokenStore).then(() =>
|
|
79
|
+
api.ownListings.create(config, resolve, reject)
|
|
80
|
+
);
|
|
81
|
+
case 'auth/token':
|
|
82
|
+
return auth.token(config, resolve, reject, tokenStore);
|
|
83
|
+
case 'auth/revoke':
|
|
84
|
+
return requireAuth(config, reject, tokenStore).then(() =>
|
|
85
|
+
auth.revoke(config, resolve, reject, tokenStore)
|
|
86
|
+
);
|
|
87
|
+
case 'auth/auth_with_idp':
|
|
88
|
+
return auth.authWithIdp(config, resolve, reject, tokenStore);
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Not implemented to Fake adapter: ${config.url}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
Create a fake adapter instance.
|
|
96
|
+
|
|
97
|
+
Features:
|
|
98
|
+
|
|
99
|
+
- Handle requests
|
|
100
|
+
- Store all requests (so that they can be inspected in tests)
|
|
101
|
+
- Implement fake token store
|
|
102
|
+
*/
|
|
103
|
+
const createAdapter = handlerFn => {
|
|
104
|
+
const requests = [];
|
|
105
|
+
const tokenStore = createTokenStore();
|
|
106
|
+
let offlineAfter;
|
|
107
|
+
const offline = () => offlineAfter != null && requests.length > offlineAfter;
|
|
108
|
+
const handler = handlerFn || defaultHandler;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
requests,
|
|
112
|
+
tokenStore,
|
|
113
|
+
offlineAfter: numOfRequests => {
|
|
114
|
+
offlineAfter = numOfRequests;
|
|
115
|
+
},
|
|
116
|
+
adapterFn: adapterHelper((config, resolve, reject) => {
|
|
117
|
+
// Store each request to `requests` array
|
|
118
|
+
requests.push(config);
|
|
119
|
+
|
|
120
|
+
if (offline()) {
|
|
121
|
+
return reject(new Error('Network error'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Call router to handle the request
|
|
125
|
+
return handler(config, resolve, reject, tokenStore);
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default createAdapter;
|
package/src/fake/api.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import transit from 'transit-js';
|
|
2
|
+
|
|
3
|
+
const reader = transit.reader('json');
|
|
4
|
+
|
|
5
|
+
export const marketplace = {
|
|
6
|
+
show: (config, resolve) => {
|
|
7
|
+
const res = `["^ ",
|
|
8
|
+
"~:data", ["^ ",
|
|
9
|
+
"~:id", "~u${config.params.id}",
|
|
10
|
+
"~:type", "~:marketplace",
|
|
11
|
+
"~:attributes", ["^ ",
|
|
12
|
+
"~:name", "Awesome skies.",
|
|
13
|
+
"~:description", "Meet and greet with fanatical sky divers."],
|
|
14
|
+
"~:relationships", ["^ "]],
|
|
15
|
+
"~:meta", ["^ "],
|
|
16
|
+
"~:included", []]`;
|
|
17
|
+
|
|
18
|
+
return resolve({ data: res });
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const users = {
|
|
23
|
+
show: (config, resolve) => {
|
|
24
|
+
const res = `["^ ",
|
|
25
|
+
"~:data", ["^ ",
|
|
26
|
+
"~:id", "~u0e0b60fe-d9a2-11e6-bf26-cec0c932ce01",
|
|
27
|
+
"~:type", "~:user",
|
|
28
|
+
"~:attributes", ["^ ",
|
|
29
|
+
"~:email", "user@sharetribe.com",
|
|
30
|
+
"~:description", "A team member"],
|
|
31
|
+
"~:relationships", ["^ "]],
|
|
32
|
+
"~:meta", ["^ "],
|
|
33
|
+
"~:included", []]`;
|
|
34
|
+
|
|
35
|
+
return resolve({ data: res });
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const listings = {
|
|
40
|
+
search: (config, resolve) => {
|
|
41
|
+
const res = `["^ ",
|
|
42
|
+
"~:data", [
|
|
43
|
+
["^ ",
|
|
44
|
+
"~:id", "~u9009efe1-25ec-4ed5-9413-e80c584ff6bf",
|
|
45
|
+
"~:type", "~:listing",
|
|
46
|
+
"~:links", ["^ ",
|
|
47
|
+
"~:self", "/v1/api/listings/show?id=9009efe1-25ec-4ed5-9413-e80c584ff6bf"],
|
|
48
|
+
"~:attributes", ["^ ",
|
|
49
|
+
"~:title", "Nishiki 401",
|
|
50
|
+
"~:description", "27-speed Hybrid. Fully functional.",
|
|
51
|
+
"~:address", "230 Hamilton Ave, Staten Island, NY 10301, USA",
|
|
52
|
+
"~:geolocation", [
|
|
53
|
+
"~#geo", [40.64542, -74.08508]]],
|
|
54
|
+
"~:relationships", ["^ ",
|
|
55
|
+
"~:author", ["^ ",
|
|
56
|
+
"^4", ["^ ",
|
|
57
|
+
"~:related", "/v1/api/users/show?id=3c073fae-6172-4e75-8b92-f560d58cd47c"]],
|
|
58
|
+
"~:marketplace", ["^ ",
|
|
59
|
+
"^4", ["^ ",
|
|
60
|
+
"^>", "/v1/api/marketplace/show"]]]],
|
|
61
|
+
["^ ",
|
|
62
|
+
"^1", "~u5e1f2086-522c-46f3-87b4-451c6770c833",
|
|
63
|
+
"^2", "^3",
|
|
64
|
+
"^4", ["^ ",
|
|
65
|
+
"^5", "/v1/api/listings/show?id=5e1f2086-522c-46f3-87b4-451c6770c833"],
|
|
66
|
+
"^6", ["^ ",
|
|
67
|
+
"^7", "Pelago Brooklyn",
|
|
68
|
+
"^8", "Goes together perfectly with a latte and a bow tie.",
|
|
69
|
+
"^9", "230 Hamilton Ave, Staten Island, NY 10301, USA",
|
|
70
|
+
"^:", [
|
|
71
|
+
"^;", [40.64542, -74.08508]]],
|
|
72
|
+
"^<", ["^ ",
|
|
73
|
+
"^=", ["^ ",
|
|
74
|
+
"^4", ["^ ",
|
|
75
|
+
"^>", "/v1/api/users/show?id=3c073fae-6172-4e75-8b92-f560d58cd47c"]],
|
|
76
|
+
"^?", ["^ ",
|
|
77
|
+
"^4", ["^ ",
|
|
78
|
+
"^>", "/v1/api/marketplace/show"]]]]],
|
|
79
|
+
"~:meta", ["^ "],
|
|
80
|
+
"~:included", []]`;
|
|
81
|
+
|
|
82
|
+
return resolve({ data: res });
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ownListings = {
|
|
87
|
+
create: (config, resolve, reject) => {
|
|
88
|
+
const body = reader.read(config.data);
|
|
89
|
+
|
|
90
|
+
const requiredFields = ['title', 'description', 'address', 'geolocation'].map(k =>
|
|
91
|
+
body.get(transit.keyword(k))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (requiredFields.some(v => v == null)) {
|
|
95
|
+
return reject({
|
|
96
|
+
status: 400,
|
|
97
|
+
statusText: 'Bad Request',
|
|
98
|
+
data: `["^ ",
|
|
99
|
+
"~:errors", [
|
|
100
|
+
["^ ",
|
|
101
|
+
"~:id", "~u57b3f476-19a0-4e07-9a44-923d9dbbe361",
|
|
102
|
+
"~:status", 400,
|
|
103
|
+
"~:code", "bad-request",
|
|
104
|
+
"~:title", "Bad request",
|
|
105
|
+
"~:details", ["^ ",
|
|
106
|
+
"~:error", ["^ ",
|
|
107
|
+
"~:body-params", ["^ ",
|
|
108
|
+
"^4", "missing-required-key",
|
|
109
|
+
"~:description", "missing-required-key",
|
|
110
|
+
"~:address", "missing-required-key",
|
|
111
|
+
"~:geolocation", "missing-required-key"]]]]]]`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let res;
|
|
116
|
+
|
|
117
|
+
if (config.params.expand === true) {
|
|
118
|
+
res = `["^ ",
|
|
119
|
+
"~:data", ["^ ",
|
|
120
|
+
"~:id", "~u58c660f5-a39a-49a5-9270-8a917b7d6c9e",
|
|
121
|
+
"~:type", "~:ownListing",
|
|
122
|
+
"~:attributes", ["^ ",
|
|
123
|
+
"~:title", "Pelago bike",
|
|
124
|
+
"~:description", "City bike for city hipster!",
|
|
125
|
+
"~:price", ["~#mn", [12000, "USD"]],
|
|
126
|
+
"~:address", "Bulevardi 14, 00200 Helsinki, Finland",
|
|
127
|
+
"~:geolocation", ["~#geo", [40.0, 73.0]]]]]`;
|
|
128
|
+
} else {
|
|
129
|
+
res = `["^ ",
|
|
130
|
+
"~:data", ["^ ",
|
|
131
|
+
"~:id", "~u58c6610d-1ffd-4fa5-b386-4f9b6e46e732",
|
|
132
|
+
"~:type", "~:ownListing"]]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return resolve({ data: res });
|
|
136
|
+
},
|
|
137
|
+
};
|
package/src/fake/auth.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
const parseFormData = data =>
|
|
4
|
+
_.fromPairs(data.split('&').map(keyValue => keyValue.split('=').map(decodeURIComponent)));
|
|
5
|
+
|
|
6
|
+
export const revoke = (config, resolve, reject, tokenStore) => {
|
|
7
|
+
const formData = parseFormData(config.data);
|
|
8
|
+
|
|
9
|
+
if (formData.token) {
|
|
10
|
+
if (tokenStore) {
|
|
11
|
+
const revoked = tokenStore.revokeRefreshToken(formData.token);
|
|
12
|
+
|
|
13
|
+
if (revoked.length) {
|
|
14
|
+
return resolve({ data: { action: 'revoked' } });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// FIXME The `data` is not what the server returns
|
|
19
|
+
return resolve({ data: { action: 'nothing' } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// FIXME The `data` is not what the server returns
|
|
23
|
+
return reject({ data: {}, __additionalTestInfo: formData });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const token = (config, resolve, reject, fakeTokenStore) => {
|
|
27
|
+
const formData = parseFormData(config.data);
|
|
28
|
+
let res;
|
|
29
|
+
|
|
30
|
+
if (formData.client_id === '08ec69f6-d37e-414d-83eb-324e94afddf0') {
|
|
31
|
+
if (formData.grant_type === 'client_credentials') {
|
|
32
|
+
res = fakeTokenStore.createAnonToken();
|
|
33
|
+
} else if (formData.grant_type === 'password') {
|
|
34
|
+
res = fakeTokenStore.createTokenWithCredentials(formData.username, formData.password);
|
|
35
|
+
} else if (formData.grant_type === 'authorization_code') {
|
|
36
|
+
res = fakeTokenStore.createTokenWithAuthorizationCode(formData.code);
|
|
37
|
+
} else if (formData.grant_type === 'refresh_token') {
|
|
38
|
+
res = fakeTokenStore.freshToken(formData.refresh_token);
|
|
39
|
+
} else if (
|
|
40
|
+
formData.grant_type === 'token_exchange' &&
|
|
41
|
+
formData.client_secret === '8af2bf99c380b3a303ab90ae4012c8cd8f69d309'
|
|
42
|
+
) {
|
|
43
|
+
res = fakeTokenStore.exchangeToken(formData.subject_token);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (res) {
|
|
48
|
+
return resolve({ data: JSON.stringify(res) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return reject({
|
|
52
|
+
status: 401,
|
|
53
|
+
statusText: 'Unauthorized',
|
|
54
|
+
data: 'Unauthorized',
|
|
55
|
+
|
|
56
|
+
// Add additional information to help debugging when testing.
|
|
57
|
+
// This key is NOT returned by the real API.
|
|
58
|
+
__additionalTestInfo: { formData },
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const authWithIdp = (config, resolve, reject, fakeTokenStore) => {
|
|
63
|
+
const formData = parseFormData(config.data);
|
|
64
|
+
const { idpId, idpClientId, idpToken } = formData;
|
|
65
|
+
let res;
|
|
66
|
+
|
|
67
|
+
if (formData.client_id === '08ec69f6-d37e-414d-83eb-324e94afddf0') {
|
|
68
|
+
res = fakeTokenStore.createTokenWithIdp(idpId, idpClientId, idpToken);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (res) {
|
|
72
|
+
return resolve({ data: JSON.stringify(res) });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return reject({
|
|
76
|
+
status: 401,
|
|
77
|
+
statusText: 'Unauthorized',
|
|
78
|
+
data: 'Unauthorized',
|
|
79
|
+
|
|
80
|
+
// Add additional information to help debugging when testing.
|
|
81
|
+
// This key is NOT returned by the real API.
|
|
82
|
+
__additionalTestInfo: { formData },
|
|
83
|
+
});
|
|
84
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
const createTokenStore = () => {
|
|
4
|
+
const tokens = [];
|
|
5
|
+
let anonAccessTokenCount = 0;
|
|
6
|
+
let accessTokenCount = 0;
|
|
7
|
+
let refreshTokenCount = 0;
|
|
8
|
+
|
|
9
|
+
const knownUsers = [['joe.dunphy@example.com', 'secret-joe']];
|
|
10
|
+
|
|
11
|
+
const knownAuthorizationCodes = [
|
|
12
|
+
{ code: 'flex-authorization-code', username: 'joe.dunphy@example.com' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const knownIdpTokens = [
|
|
16
|
+
{
|
|
17
|
+
id: 'facebook',
|
|
18
|
+
token: 'idp-token',
|
|
19
|
+
clientId: 'idp-client-id',
|
|
20
|
+
username: 'joe.dunphy@example.com',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Private
|
|
25
|
+
|
|
26
|
+
const generateAnonAccessToken = () => {
|
|
27
|
+
anonAccessTokenCount += 1;
|
|
28
|
+
return `anonymous-access-${anonAccessTokenCount}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const generateAccessToken = username => {
|
|
32
|
+
accessTokenCount += 1;
|
|
33
|
+
return `${username}-access-${accessTokenCount}`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const generateRefreshToken = username => {
|
|
37
|
+
refreshTokenCount += 1;
|
|
38
|
+
return `${username}-refresh-${refreshTokenCount}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Public
|
|
42
|
+
|
|
43
|
+
const validToken = (accessToken, tokenType) =>
|
|
44
|
+
_.find(
|
|
45
|
+
tokens,
|
|
46
|
+
({ token }) =>
|
|
47
|
+
token.access_token &&
|
|
48
|
+
accessToken &&
|
|
49
|
+
token.token_type &&
|
|
50
|
+
tokenType &&
|
|
51
|
+
token.access_token.toLowerCase() === accessToken.toLowerCase() &&
|
|
52
|
+
token.token_type.toLowerCase() === tokenType.toLowerCase()
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const createAnonToken = () => {
|
|
56
|
+
const token = {
|
|
57
|
+
token: {
|
|
58
|
+
access_token: generateAnonAccessToken(),
|
|
59
|
+
token_type: 'bearer',
|
|
60
|
+
expires_in: 86400,
|
|
61
|
+
scope: 'public-read',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
tokens.push(token);
|
|
65
|
+
|
|
66
|
+
return token.token;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const createTokenWithCredentials = (username, password) => {
|
|
70
|
+
const user = _.find(knownUsers, u => _.isEqual(u, [username, password]));
|
|
71
|
+
|
|
72
|
+
if (!user) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const token = {
|
|
77
|
+
token: {
|
|
78
|
+
access_token: generateAccessToken(username),
|
|
79
|
+
refresh_token: generateRefreshToken(username),
|
|
80
|
+
token_type: 'bearer',
|
|
81
|
+
expires_in: 86400,
|
|
82
|
+
scope: 'user',
|
|
83
|
+
},
|
|
84
|
+
user: {
|
|
85
|
+
username,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
tokens.push(token);
|
|
89
|
+
|
|
90
|
+
return token.token;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const createTokenWithAuthorizationCode = authorizationCode => {
|
|
94
|
+
const knownCode = _.find(knownAuthorizationCodes, ({ code }) => code === authorizationCode);
|
|
95
|
+
|
|
96
|
+
if (!knownCode) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { username } = knownCode;
|
|
101
|
+
const token = {
|
|
102
|
+
token: {
|
|
103
|
+
access_token: generateAccessToken(username),
|
|
104
|
+
refresh_token: generateRefreshToken(username),
|
|
105
|
+
token_type: 'bearer',
|
|
106
|
+
expires_in: 86400,
|
|
107
|
+
scope: 'user:limited',
|
|
108
|
+
},
|
|
109
|
+
user: {
|
|
110
|
+
username,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
tokens.push(token);
|
|
114
|
+
|
|
115
|
+
return token.token;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const createTokenWithIdp = (idpId, idpClientId, idpToken) => {
|
|
119
|
+
const knownIdpToken = _.find(
|
|
120
|
+
knownIdpTokens,
|
|
121
|
+
({ id, token, clientId }) => id === idpId && token === idpToken && clientId === idpClientId
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!knownIdpToken) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { username } = knownIdpToken;
|
|
129
|
+
const token = {
|
|
130
|
+
token: {
|
|
131
|
+
access_token: generateAccessToken(username),
|
|
132
|
+
refresh_token: generateRefreshToken(username),
|
|
133
|
+
token_type: 'bearer',
|
|
134
|
+
expires_in: 86400,
|
|
135
|
+
scope: 'user',
|
|
136
|
+
},
|
|
137
|
+
user: {
|
|
138
|
+
username,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
tokens.push(token);
|
|
142
|
+
|
|
143
|
+
return token.token;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const exchangeToken = accessToken => {
|
|
147
|
+
const currentToken = _.find(
|
|
148
|
+
tokens,
|
|
149
|
+
({ token }) =>
|
|
150
|
+
token.access_token &&
|
|
151
|
+
accessToken &&
|
|
152
|
+
token.access_token.toLowerCase() === accessToken.toLowerCase()
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!currentToken) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { username } = currentToken.user;
|
|
160
|
+
|
|
161
|
+
const trustedToken = {
|
|
162
|
+
token: {
|
|
163
|
+
access_token: generateAccessToken(username),
|
|
164
|
+
refresh_token: generateRefreshToken(username),
|
|
165
|
+
token_type: 'bearer',
|
|
166
|
+
expires_in: 86400,
|
|
167
|
+
scope: 'trusted:user',
|
|
168
|
+
},
|
|
169
|
+
user: {
|
|
170
|
+
username,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
tokens.push(trustedToken);
|
|
174
|
+
|
|
175
|
+
return trustedToken.token;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const expireAccessToken = accessToken => {
|
|
179
|
+
_.map(tokens, t => {
|
|
180
|
+
const { token } = t;
|
|
181
|
+
|
|
182
|
+
if (token.access_token === accessToken) {
|
|
183
|
+
token.access_token = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return t;
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const revokeRefreshToken = refreshToken =>
|
|
191
|
+
_.remove(tokens, t => t.token.refresh_token === refreshToken);
|
|
192
|
+
|
|
193
|
+
const freshToken = refreshToken => {
|
|
194
|
+
const existingToken = revokeRefreshToken(refreshToken);
|
|
195
|
+
|
|
196
|
+
if (existingToken.length) {
|
|
197
|
+
const { username } = existingToken[0].user;
|
|
198
|
+
|
|
199
|
+
const token = {
|
|
200
|
+
token: {
|
|
201
|
+
access_token: generateAccessToken(username),
|
|
202
|
+
refresh_token: generateRefreshToken(username),
|
|
203
|
+
token_type: 'bearer',
|
|
204
|
+
expires_in: 86400,
|
|
205
|
+
},
|
|
206
|
+
user: {
|
|
207
|
+
username,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
tokens.push(token);
|
|
211
|
+
|
|
212
|
+
return token.token;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
createAnonToken,
|
|
220
|
+
createTokenWithCredentials,
|
|
221
|
+
createTokenWithAuthorizationCode,
|
|
222
|
+
createTokenWithIdp,
|
|
223
|
+
exchangeToken,
|
|
224
|
+
freshToken,
|
|
225
|
+
revokeRefreshToken,
|
|
226
|
+
validToken,
|
|
227
|
+
expireAccessToken,
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export default createTokenStore;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import SharetribeSdk from './sdk';
|
|
2
|
+
import * as types from './types';
|
|
3
|
+
import browserCookieStore from './browser_cookie_store';
|
|
4
|
+
import expressCookieStore from './express_cookie_store';
|
|
5
|
+
import memoryStore from './memory_store';
|
|
6
|
+
import { read, write } from './serializer';
|
|
7
|
+
import { objectQueryString } from './utils';
|
|
8
|
+
|
|
9
|
+
const createInstance = config => new SharetribeSdk(config);
|
|
10
|
+
|
|
11
|
+
// Export token stores
|
|
12
|
+
const tokenStore = {
|
|
13
|
+
memoryStore,
|
|
14
|
+
browserCookieStore,
|
|
15
|
+
expressCookieStore,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Export Transit serialization helpers
|
|
19
|
+
const transit = { read, write };
|
|
20
|
+
|
|
21
|
+
// Export util functions
|
|
22
|
+
const util = { objectQueryString };
|
|
23
|
+
|
|
24
|
+
/* eslint-disable import/prefer-default-export */
|
|
25
|
+
export { createInstance, types, tokenStore, transit, util };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const constructAuthHeader = authToken => {
|
|
2
|
+
/* eslint-disable camelcase */
|
|
3
|
+
const token_type = authToken.token_type && authToken.token_type.toLowerCase();
|
|
4
|
+
|
|
5
|
+
switch (token_type) {
|
|
6
|
+
case 'bearer':
|
|
7
|
+
return `Bearer ${authToken.access_token}`;
|
|
8
|
+
default:
|
|
9
|
+
throw new Error(`Unknown token type: ${token_type}`);
|
|
10
|
+
}
|
|
11
|
+
/* eslint-enable camelcase */
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
Read `authToken` from `ctx`. Then construct Authorize header and add it to `headers`.
|
|
16
|
+
|
|
17
|
+
Changes to `ctx`:
|
|
18
|
+
|
|
19
|
+
- Add `headers.Authorize`
|
|
20
|
+
*/
|
|
21
|
+
export default class AddAuthHeader {
|
|
22
|
+
enter(ctx) {
|
|
23
|
+
const { authToken, headers = {} } = ctx;
|
|
24
|
+
|
|
25
|
+
if (!authToken) {
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const authHeaders = { Authorization: constructAuthHeader(authToken) };
|
|
30
|
+
return { ...ctx, headers: { ...headers, ...authHeaders } };
|
|
31
|
+
}
|
|
32
|
+
}
|