saasufy-components 2.2.0 → 2.3.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/oauth-handler.js +98 -0
- package/oauth-link.js +84 -0
- package/package.json +1 -1
- package/utils.js +12 -0
package/oauth-handler.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
|
+
import { generateRandomHexString } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AUTH_TIMEOUT = 10000;
|
|
5
|
+
|
|
6
|
+
class OAuthHandler extends SocketConsumer {
|
|
7
|
+
static get observedAttributes() {
|
|
8
|
+
return [
|
|
9
|
+
'provider',
|
|
10
|
+
'code-param-name',
|
|
11
|
+
'state-param-name',
|
|
12
|
+
'state-session-storage-key',
|
|
13
|
+
'success-url',
|
|
14
|
+
'success-location-hash',
|
|
15
|
+
'navigate-event-path',
|
|
16
|
+
'auth-timeout'
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
successRedirect() {
|
|
21
|
+
let successURL = this.getAttribute('success-url');
|
|
22
|
+
let successLocationHash = this.getAttribute('success-location-hash');
|
|
23
|
+
let navigateEventPath = this.getAttribute('navigate-event-path');
|
|
24
|
+
|
|
25
|
+
if (successLocationHash) {
|
|
26
|
+
location.hash = successLocationHash;
|
|
27
|
+
} else if (successURL) {
|
|
28
|
+
location.href = successURL;
|
|
29
|
+
} else {
|
|
30
|
+
this.dispatchEvent(
|
|
31
|
+
new CustomEvent('navigate', {
|
|
32
|
+
detail: {
|
|
33
|
+
path: navigateEventPath
|
|
34
|
+
},
|
|
35
|
+
bubbles: true
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async connectedCallback() {
|
|
42
|
+
this.innerHTML = '';
|
|
43
|
+
|
|
44
|
+
let provider = this.getAttribute('provider');
|
|
45
|
+
if (!provider) {
|
|
46
|
+
throw new Error('The provider attribute of oauth-handler was not specified');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let sessionStorageKey = this.getAttribute('state-session-storage-key') || `oauth.${provider}.state`;
|
|
50
|
+
let expectedOAuthState = sessionStorage.getItem(sessionStorageKey);
|
|
51
|
+
if (!expectedOAuthState) return;
|
|
52
|
+
|
|
53
|
+
let codeParamName = this.getAttribute('code-param-name') || 'code';
|
|
54
|
+
let stateParamName = this.getAttribute('state-param-name') || 'state';
|
|
55
|
+
|
|
56
|
+
let authTimeout = Number(this.getAttribute('auth-timeout') || DEFAULT_AUTH_TIMEOUT);
|
|
57
|
+
|
|
58
|
+
let urlSearchParams = new URLSearchParams(location.search);
|
|
59
|
+
let code = urlSearchParams.get(codeParamName);
|
|
60
|
+
|
|
61
|
+
if (code) {
|
|
62
|
+
let state = urlSearchParams.get(stateParamName);
|
|
63
|
+
let socket = this.getSocket();
|
|
64
|
+
|
|
65
|
+
history.replaceState({}, document.title, location.pathname);
|
|
66
|
+
|
|
67
|
+
if (state === expectedOAuthState) {
|
|
68
|
+
try {
|
|
69
|
+
await socket.invoke('log-in-oauth', { provider, data: { code } });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.innerHTML = `
|
|
72
|
+
<div class="error">OAuth authentication failed. ${error.message}</div>
|
|
73
|
+
`;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (socket.authState === 'authenticated') {
|
|
77
|
+
this.successRedirect();
|
|
78
|
+
} else {
|
|
79
|
+
try {
|
|
80
|
+
await socket.listener('authenticate').once(authTimeout);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.innerHTML = `
|
|
83
|
+
<div class="error">OAuth socket authentication timed out</div>
|
|
84
|
+
`;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.successRedirect();
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
this.innerHTML = `
|
|
91
|
+
<div class="error">OAuth state query parameter was invalid</div>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
window.customElements.define('oauth-handler', OAuthHandler);
|
package/oauth-link.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { renderTemplate, generateRandomHexString } from './utils.js';
|
|
2
|
+
|
|
3
|
+
class OAuthLink extends HTMLElement {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.isReady = false;
|
|
7
|
+
this.attachShadow({ mode: 'open' });
|
|
8
|
+
|
|
9
|
+
this.handleSlotChangeEvent = () => {
|
|
10
|
+
this.renderItem();
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static get observedAttributes() {
|
|
15
|
+
return [
|
|
16
|
+
'provider',
|
|
17
|
+
'client-id',
|
|
18
|
+
'state-size',
|
|
19
|
+
'state-session-storage-key'
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
24
|
+
if (!this.isReady) return;
|
|
25
|
+
this.render();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connectedCallback() {
|
|
29
|
+
this.isReady = true;
|
|
30
|
+
this.render();
|
|
31
|
+
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
disconnectedCallback() {
|
|
35
|
+
this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
renderItem() {
|
|
39
|
+
let provider = this.getAttribute('provider');
|
|
40
|
+
if (!provider) {
|
|
41
|
+
throw new Error('The provider attribute of oauth-link was not specified');
|
|
42
|
+
}
|
|
43
|
+
let viewportNode = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
|
|
44
|
+
if (!viewportNode) return;
|
|
45
|
+
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
46
|
+
if (!itemTemplate) return;
|
|
47
|
+
|
|
48
|
+
let clientId = this.getAttribute('client-id');
|
|
49
|
+
let stateSize = Number(this.getAttribute('state-size') || 20);
|
|
50
|
+
let sessionStorageKey = this.getAttribute('state-session-storage-key') || `oauth.${provider}.state`;
|
|
51
|
+
let state = generateRandomHexString(stateSize);
|
|
52
|
+
sessionStorage.setItem(sessionStorageKey, state);
|
|
53
|
+
|
|
54
|
+
let itemString = renderTemplate(
|
|
55
|
+
itemTemplate.innerHTML,
|
|
56
|
+
{
|
|
57
|
+
oauth: {
|
|
58
|
+
clientId,
|
|
59
|
+
state
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
viewportNode.innerHTML = itemString;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
let urlSearchParams = new URLSearchParams(location.search);
|
|
68
|
+
let code = urlSearchParams.get('code');
|
|
69
|
+
|
|
70
|
+
if (code) {
|
|
71
|
+
this.shadowRoot.innerHTML = '';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.shadowRoot.innerHTML = `
|
|
76
|
+
<slot name="item"></slot>
|
|
77
|
+
<slot name="viewport"></slot>
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
this.renderItem();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
window.customElements.define('oauth-link', OAuthLink);
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -268,6 +268,18 @@ export function updateConsumerElements(parentElement, consumers, value) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
export function generateRandomHexString(byteLength) {
|
|
272
|
+
let byteArray = new Uint8Array(byteLength);
|
|
273
|
+
crypto.getRandomValues(byteArray);
|
|
274
|
+
return Array.from(byteArray, byte => {
|
|
275
|
+
let firstChar = byte >> 4;
|
|
276
|
+
firstChar = firstChar < 10 ? String(firstChar) : String.fromCharCode(firstChar + 87);
|
|
277
|
+
let secondChar = byte & 0x0f;
|
|
278
|
+
secondChar = secondChar < 10 ? String(secondChar) : String.fromCharCode(secondChar + 87);
|
|
279
|
+
return `${firstChar}${secondChar}`;
|
|
280
|
+
}).join('');
|
|
281
|
+
}
|
|
282
|
+
|
|
271
283
|
export function wait(duration) {
|
|
272
284
|
return new Promise((resolve) => setTimeout(resolve, duration));
|
|
273
285
|
}
|