-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathredirect.js
277 lines (258 loc) · 12.2 KB
/
redirect.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/*jslint this: true, browser: true, for: true, long: true, unordered: true */
/*global window console URLSearchParams history demonstrationHelper */
(function () {
// Create a helper function to remove some boilerplate code from the example itself.
const demo = demonstrationHelper({
"responseElm": document.getElementById("idResponse"),
"javaScriptElm": document.getElementById("idJavaScript"),
"footerElm": document.getElementById("idFooter")
});
let pageDisplayTime = new Date();
let accessToken;
let iFrameForTokenRefresh = null;
/**
* If login failed, the error can be found as a bookmark.
* @param {string} hash The hash part of the URL containing the auth result.
* @return {void}
*/
function hasErrors(hash) {
const urlParams = new URLSearchParams(hash);
const error = urlParams.get("error");
// Example: ?error=access_denied&error_description=Disclaimers+not+accepted+during+login.&state=eyJjc3Jm
if (error === null) {
console.log("No error found.");
return false;
}
console.error("Found error: " + error + " (" + urlParams.get("error_description") + ")");
// The error "login_required" can mean the authentication cookie is expired, or, in case of Firefox, "Enhanced Tracking Protection" was set to "Strict".
// The solution is to change this for the affected page: https://support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-desktop?as=u#w_what-to-do-if-a-site-seems-broken
// This must be done by the customer.
return true;
}
/**
* See if errors are returned by looking at the url.
* @return {void}
*/
function checkErrors() {
hasErrors(window.location.hash.replace("#", "?"));
}
/**
* After a successful authentication, the token can be found as bookmark.
* @param {string} hash The hash part of the URL containing the auth result.
* @return {void}
*/
function processToken(hash) {
// A bookmark (or anchor) is used, because the access_token doesn't leave the browser this way, so it doesn't end up in logfiles.
const urlParams = new URLSearchParams(hash);
const expiresInSeconds = urlParams.get("expires_in"); // Note that this doesn't work when the page is refreshed. To be sure, use a cookie, or sessionStorage
const accessTokenExpirationTime = new Date(pageDisplayTime.getTime() + expiresInSeconds * 1000);
accessToken = urlParams.get("access_token");
if (accessToken === null) {
console.error("Not a valid token supplied via the query parameters of the URL.");
} else {
console.log("Found access_token (valid until " + accessTokenExpirationTime.toLocaleString() + ").\nOnly use this token for API requests, don't send it to a backend, for security reasons:\n" + decodeURIComponent(accessToken));
}
}
/**
* See if a token is returned by looking at the url.
* @return {void}
*/
function getToken() {
processToken(window.location.hash.replace("#", "?"));
}
/**
* It is a good practice to remove the token from the URL, to prevent (1) js can steal the token (2) people sharing the link and (3) the app looks better without.
* @return {void}
*/
function hideTokenFromUrl() {
window.history.replaceState(
{}, // state
"", // unused
window.location.pathname // url
);
console.log("See the URL. The achor tag is hidden now.");
}
/**
* After a successful authentication, the csrf token in the state must be the expected one.
* @param {string} hash The hash part of the URL containing the auth result.
* @return {Object} The object with the state.
*/
function getStateObject(hash) {
// https://auth0.com/docs/protocols/oauth2/oauth-state
const urlParams = new URLSearchParams(hash);
const state = urlParams.get("state");
let stateUnencoded;
let stateObject = null;
if (state === null) {
console.error("No state found - don't try to get the token, but redirect the user back to the authentication.");
} else {
try {
stateUnencoded = window.atob(state);
stateObject = JSON.parse(stateUnencoded);
} catch (ignore) {
console.error("State returned in the URL parameter is invalid.");
}
}
return stateObject;
}
/**
* After a successful authentication, the state entered before authentication is passed as bookmark.
* @return {void}
*/
function getState() {
const stateObject = getStateObject(window.location.hash.replace("#", "?"));
if (stateObject !== null) {
console.log("Found state: " + JSON.stringify(stateObject, null, 4));
}
}
/**
* Compare the expected with retrieved CSRF token.
* @param {string} hash The hash part of the URL containing the auth result.
* @return {boolean} True when the CSRF token is expected.
*/
function isCsrfTokenOk(hash) {
const receivedStateObject = getStateObject(hash);
const expectedCsrfToken = window.localStorage.getItem("csrfToken");
if (expectedCsrfToken === null || receivedStateObject === null) {
console.error("Something messed with the input data, because the csrfToken can't be verified.");
} else if (receivedStateObject.csrfToken !== expectedCsrfToken) {
console.error("The generated csrfToken (" + expectedCsrfToken + ") differs from the csrfToken in the response (" + receivedStateObject.csrfToken + ").\nThis can indicate a malicious request. Stop further processing and redirect back to the authentication.");
} else {
// All fine!
console.log("This looks good. The csrfToken supplied in the response is the expected one.");
return true;
}
return false;
}
/**
* A CSRF (Cross Site Request Forgery) Token is a secret, unique and unpredictable value an application generates in order to protect CSRF vulnerable resources.
* @return {void}
*/
function verifyCsrfToken() {
isCsrfTokenOk(window.location.hash.replace("#", "?"));
}
/**
* Demonstrate a basic request to the Api, to show the token is valid.
* @return {void}
*/
function getUserData() {
fetch(
demo.apiUrl + "/port/v1/users/me",
{
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Authorization": "Bearer " + accessToken
},
"method": "GET"
}
).then(function (response) {
if (response.ok) {
response.json().then(function (responseJson) {
console.log("Connection to API created, hello " + responseJson.Name);
});
} else {
demo.processError(response);
}
}).catch(function (error) {
console.error(error);
});
}
/**
* Demonstrate how to refresh the token within the session time.
* @return {void}
*/
function refreshToken() {
/**
* Listen to messages broadcasted by the iframe.
* @return {void}
*/
function setupMessageReceiver() {
window.addEventListener("message", function (event) {
const expectedOrigin = window.location.protocol + "//" + window.location.host;
if (event.origin !== expectedOrigin) {
console.error("Received a message from an unexpected origin: " + event.origin + " - expected: " + expectedOrigin);
return;
}
console.log("Incoming message from expected iframe: " + event.data);
// If you are using Firefox with "Enhanced Tracking Protection" set to "Strict" then you get an error "login_required".
// The solution is to change this for the affected page: https://support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-desktop?as=u#w_what-to-do-if-a-site-seems-broken
// This must be done by the customer.
if (!hasErrors(event.data) && isCsrfTokenOk(event.data)) {
// No errors found. Is there a token?
processToken(event.data);
}
}, false);
}
/**
* Create a hidden iframe which loads the refresh page and broadcasts the url hash.
* @return {void}
*/
function createIframe() {
// Create an iframe which loads the token and broadcasts this to this page - but only once:
iFrameForTokenRefresh = document.createElement("iframe");
iFrameForTokenRefresh.style.display = "none";
document.body.appendChild(iFrameForTokenRefresh);
}
/**
* The redirect URL is on the same host, but the name is "refresh.html".
* @return {Object} The object with the state.
*/
function getRedirectUrl() {
let result = window.location.href;
const posOfHash = result.indexOf("#");
if (posOfHash > -1) {
result = result.substr(0, posOfHash);
}
return result.replace("redirect.html", "refresh.html");
}
/**
* This function generates a cryptographically strong random value.
* https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
* @return {string} A 'real' random value
*/
function getRandomValue() {
const randomValues = new Uint32Array(1);
window.crypto.getRandomValues(randomValues);
return randomValues[0].toString();
}
/**
* Create a link to the OAuth2 server, including the client_id of the app, a state and the flow.
* @return {void}
*/
function generateRefreshLink() {
// State contains a unique number, which must be stored in the client and compared with the incoming state after authentication
// It is passed as base64 encoded string
// https://auth0.com/docs/protocols/oauth2/oauth-state
const csrfToken = getRandomValue();
const stateString = window.btoa(JSON.stringify({
// Token is a random number - other data can be added as well
"csrfToken": csrfToken,
"state": "MyRefreshExample"
}));
window.localStorage.setItem("csrfToken", csrfToken); // Save it for verification..
let url = demo.authUrl +
"?client_id=1a6eb56ced7c4e04b1467e7e9be9bff7" +
"&response_type=token" +
"&prompt=none" + // This token prevents putting an x-frame-options header to "deny" by the server, so it can be loaded into a frame
"&state=" + encodeURIComponent(stateString) +
"&redirect_uri=" + encodeURIComponent(getRedirectUrl());
return url;
}
if (iFrameForTokenRefresh === null) {
setupMessageReceiver();
createIframe();
}
iFrameForTokenRefresh.setAttribute("src", generateRefreshLink());
pageDisplayTime = new Date();
}
demo.setupEvents([
{"evt": "click", "elmId": "idBtnCheckErrors", "func": checkErrors, "funcsToDisplay": [checkErrors, hasErrors]},
{"evt": "click", "elmId": "idBtnGetState", "func": getState, "funcsToDisplay": [getState, getStateObject]},
{"evt": "click", "elmId": "idBtnVerifyCsrfToken", "func": verifyCsrfToken, "funcsToDisplay": [verifyCsrfToken]},
{"evt": "click", "elmId": "idBtnGetToken", "func": getToken, "funcsToDisplay": [getToken, processToken]},
{"evt": "click", "elmId": "idBtnHideToken", "func": hideTokenFromUrl, "funcsToDisplay": [hideTokenFromUrl]},
{"evt": "click", "elmId": "idBtnGetUserData", "func": getUserData, "funcsToDisplay": [getUserData]},
{"evt": "click", "elmId": "idBtnRefreshToken", "func": refreshToken, "funcsToDisplay": [refreshToken]}
]);
demo.displayVersion("cs");
}());