saasufy-components 10.5.4 → 10.5.5
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/README.md +2 -2
- package/log-out.js +10 -0
- package/oauth-handler.js +33 -2
- package/oauth-link.js +20 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -990,7 +990,7 @@ Logging out of Keycloak as well as of the app:
|
|
|
990
990
|
|
|
991
991
|
**Attributes**
|
|
992
992
|
|
|
993
|
-
- `logout-url`: The log out endpoint of the OAuth provider; for OpenID Connect providers this is the `end_session_endpoint`. If it is not specified, the component only deauthenticates the socket. The redirect is skipped if the user did not log in via an OAuth provider.
|
|
993
|
+
- `logout-url`: The log out endpoint of the OAuth provider; for OpenID Connect providers this is the `end_session_endpoint`. If it is not specified, the component only deauthenticates the socket. The redirect is skipped if the user did not log in via an OAuth provider. It must be an `http` or `https` URL; relative paths are supported and are resolved against your app's origin.
|
|
994
994
|
- `provider`: The name of the OAuth provider, as specified on the `Authentication` page of your Saasufy control panel. If it is set, the redirect only happens when the user logged in via that specific provider; this is useful when your app offers several different ways to log in.
|
|
995
995
|
- `client-id`: The client ID to pass to the provider as the `client_id` query parameter. OpenID Connect providers require either this or an ID token in order to log the user out without asking them to confirm.
|
|
996
996
|
- `post-logout-redirect-uri`: The URL to send the user back to once the provider has logged them out, passed as the `post_logout_redirect_uri` query parameter. It usually needs to be registered with the provider in advance. If it is not specified, the user is left on the provider's own logged out page.
|
|
@@ -1051,7 +1051,7 @@ When using an OAuth provider, the callback URL which you register with the provi
|
|
|
1051
1051
|
|
|
1052
1052
|
- `provider` (required): The name of the provider. It must match the provider name specified on the `Authentication` page of your Saasufy control panel and also the value provided to the `oauth-link` component at the start of the OAuth flow.
|
|
1053
1053
|
- `success-location-hash`: If authentication is successful, the browser's `location.hash` will be set to this value. It can be used to redirect the user to their dashboard, for example.
|
|
1054
|
-
- `success-url`: If authentication is successful, the browser's `location.href` will be set to this value. It can be used to redirect the user to their dashboard, for example.
|
|
1054
|
+
- `success-url`: If authentication is successful, the browser's `location.href` will be set to this value. It can be used to redirect the user to their dashboard, for example. It must be an `http` or `https` URL; relative paths are supported and are resolved against the current page.
|
|
1055
1055
|
- `redirect-uri`: A URI to redirect the user to once OAuth is successful. This attribute may be required depending on the OAuth provider/client.
|
|
1056
1056
|
- `extra-data`: This attribute can be used to pass additional data to the OAuth `getAccessTokenURL` endpoint. For Google OAuth, for example, an additional `redirect_uri` field is required, so it should be set using `extra-data="redirect_uri=http://localhost.com:8000/"` (substitute the URI with your own).
|
|
1057
1057
|
- `navigate-event-path`: If authentication is successful and a path is set via this attribute, the component will emit a `navigate` event which will bubble up the component hierarchy and can be used by a parent component to perform the success redirection. This approach can be used as an alternative to `success-location-hash` or `success-url` for doing the final redirect.
|
package/log-out.js
CHANGED
|
@@ -20,6 +20,16 @@ class LogOut extends SocketConsumer {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// A javascript: or data: URL would execute when assigned to location.href.
|
|
24
|
+
if (
|
|
25
|
+
providerLogOutURL.protocol !== 'https:' &&
|
|
26
|
+
providerLogOutURL.protocol !== 'http:'
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'The logout-url attribute of log-out must be an http or https URL'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
let clientId = this.getAttribute('client-id');
|
|
24
34
|
let postLogoutRedirectURI = this.getAttribute('post-logout-redirect-uri');
|
|
25
35
|
let searchParams = providerLogOutURL.searchParams;
|
package/oauth-handler.js
CHANGED
|
@@ -12,7 +12,25 @@ class OAuthHandler extends SocketConsumer {
|
|
|
12
12
|
if (successLocationHash) {
|
|
13
13
|
location.hash = successLocationHash;
|
|
14
14
|
} else if (successURL) {
|
|
15
|
-
|
|
15
|
+
let resolvedURL;
|
|
16
|
+
// Resolved against the document URL so relative paths behave as they did
|
|
17
|
+
// with the plain assignment; only an absolute URL brings its own scheme.
|
|
18
|
+
try {
|
|
19
|
+
resolvedURL = new URL(successURL, location.href);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'The success-url attribute of oauth-handler was not a valid URL'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
resolvedURL.protocol !== 'https:' &&
|
|
27
|
+
resolvedURL.protocol !== 'http:'
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'The success-url attribute of oauth-handler must be an http or https URL'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
location.href = resolvedURL.href;
|
|
16
34
|
} else {
|
|
17
35
|
this.dispatchEvent(
|
|
18
36
|
new CustomEvent('navigate', {
|
|
@@ -42,7 +60,8 @@ class OAuthHandler extends SocketConsumer {
|
|
|
42
60
|
|
|
43
61
|
let useLocalStorage = !this.hasAttribute('use-session-storage');
|
|
44
62
|
let storageKey = this.getAttribute('state-storage-key') || 'oauth.state';
|
|
45
|
-
let
|
|
63
|
+
let storage = useLocalStorage ? localStorage : sessionStorage;
|
|
64
|
+
let expectedOAuthState = storage.getItem(storageKey);
|
|
46
65
|
if (!expectedOAuthState) {
|
|
47
66
|
this.innerHTML = `
|
|
48
67
|
<div class="error">OAuth authentication failed because the state was missing.</div>
|
|
@@ -83,6 +102,18 @@ class OAuthHandler extends SocketConsumer {
|
|
|
83
102
|
history.replaceState({}, document.title, location.pathname);
|
|
84
103
|
|
|
85
104
|
if (relevantStates.some(expectedState => expectedState === state)) {
|
|
105
|
+
// Single-use, so consumed before the exchange. Re-read rather than
|
|
106
|
+
// written back from the list captured above, which would drop a state
|
|
107
|
+
// another handler or tab added since.
|
|
108
|
+
let remainingStates = (storage.getItem(storageKey) || '')
|
|
109
|
+
.split(',')
|
|
110
|
+
.filter(existingState => existingState && existingState !== state);
|
|
111
|
+
if (remainingStates.length) {
|
|
112
|
+
storage.setItem(storageKey, remainingStates.join(','));
|
|
113
|
+
} else {
|
|
114
|
+
storage.removeItem(storageKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
try {
|
|
87
118
|
await socket.invoke('log-in-oauth', { provider, data: { code, ...extraData } });
|
|
88
119
|
} catch (error) {
|
package/oauth-link.js
CHANGED
|
@@ -11,7 +11,9 @@ class OAuthLink extends HTMLElement {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
this.handleClickEvent = () => {
|
|
14
|
-
|
|
14
|
+
// Re-asserts the stored state in case it was evicted; not regenerated,
|
|
15
|
+
// since the href was built from it at render time.
|
|
16
|
+
this.storeState();
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -73,33 +75,34 @@ class OAuthLink extends HTMLElement {
|
|
|
73
75
|
viewportNode.innerHTML = itemString;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// A fresh nonce per render; the handler consumes a state on use. Render only,
|
|
79
|
+
// since the href is rebuilt from the new state.
|
|
76
80
|
updateState() {
|
|
77
81
|
let provider = this.getAttribute('provider');
|
|
78
82
|
if (!provider) {
|
|
79
83
|
throw new Error('The provider attribute of oauth-link was not specified');
|
|
80
84
|
}
|
|
81
|
-
let useLocalStorage = !this.hasAttribute('use-session-storage');
|
|
82
85
|
let stateSize = Number(this.getAttribute('state-size') || 20);
|
|
86
|
+
this.provider = provider;
|
|
87
|
+
this.state = `${provider}-${generateRandomHexString(stateSize)}`;
|
|
88
|
+
this.storeState();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-read immediately before writing so another provider's state, from
|
|
92
|
+
// another link or tab, is not dropped.
|
|
93
|
+
storeState() {
|
|
94
|
+
if (!this.state) return;
|
|
95
|
+
let useLocalStorage = !this.hasAttribute('use-session-storage');
|
|
83
96
|
let storageKey = this.getAttribute('state-storage-key') || 'oauth.state';
|
|
84
|
-
|
|
85
97
|
let storage = useLocalStorage ? localStorage : sessionStorage;
|
|
86
98
|
let existingState = storage.getItem(storageKey);
|
|
87
|
-
let stateList;
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
stateList.pop();
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
stateList = []
|
|
96
|
-
}
|
|
97
|
-
if (this.provider !== provider) {
|
|
98
|
-
this.state = `${provider}-${generateRandomHexString(stateSize)}`;
|
|
99
|
-
this.provider = provider;
|
|
99
|
+
let stateList = existingState ? existingState.split(',').filter(Boolean) : [];
|
|
100
|
+
if (stateList.includes(this.state)) return;
|
|
101
|
+
let maxStateCount = Number(this.getAttribute('max-state-count') ?? 20);
|
|
102
|
+
if (stateList.length >= maxStateCount) {
|
|
103
|
+
stateList.pop();
|
|
100
104
|
}
|
|
101
105
|
stateList.unshift(this.state);
|
|
102
|
-
|
|
103
106
|
storage.setItem(storageKey, stateList.join(','));
|
|
104
107
|
}
|
|
105
108
|
|