-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
217 lines (169 loc) · 6.2 KB
/
Copy pathindex.js
File metadata and controls
217 lines (169 loc) · 6.2 KB
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
async function authorize(options) {
validateAuthOptions(options);
const pkceCodeVerifier = generateRandomString();
localStorage.setItem('pkceCodeVerifier', pkceCodeVerifier);
const pkceCodeChallenge = await pkceChallengeFromVerifier(pkceCodeVerifier);
const redirectUri = options.redirectUri ? options.redirectUri : window.location.href;
let authUrl = options.driveUri + '?pauth-method=authorize'
+ `&response_type=code`
+ `&client_id=${encodeURIComponent(window.location.origin)}`
+ `&redirect_uri=${encodeURIComponent(redirectUri)}`
+ `&code_challenge=${encodeURIComponent(pkceCodeChallenge)}`
+ `&code_challenge_method=S256`;
if (options.perms) {
const scope = encodeScopeFromPerms(options.perms);
authUrl += `&scope=${encodeURIComponent(scope)}`;
}
const stateCode = generateRandomString();
localStorage.setItem('oauthState', stateCode);
localStorage.setItem('gemdriveAuthDriveUri', options.driveUri);
if (options.state) {
authUrl += `&state=${encodeURIComponent(stateCode + options.state)}`;
}
else {
authUrl += `&state=${encodeURIComponent(stateCode)}`;
}
// TODO: This doesn't appear to work in Firefox private mode, at least not on
// the first try in a new private window. localStorage.setItem doesn't seem
// to complete before the redirect, such that when the OAuth server redirects
// back, the saved state isn't there. Steps to reproduce:
// 1. Open new private window in Firefox
// 2. Begin authorization flow from a GemDrive app
// 3. Attempt to complete flow, but it fails because of missing oauthState,
// which should be there because of the setItem above.
// Adding a setTimeout for 500ms seems to fix it, but I don't know what the
// root cause is.
window.location.href = authUrl;
}
async function completeAuthorization(options) {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
urlParams.delete('code');
const savedState = localStorage.getItem('oauthState');
localStorage.removeItem('oauthState');
const returnedState = urlParams.get('state');
if (savedState !== returnedState.slice(0, savedState.length)) {
alert("Invalid state returned from authorization server. Aborting");
// go back to app home
window.location = window.location.origin + window.location.pathname;
}
const driveUri = localStorage.getItem('gemdriveAuthDriveUri');
localStorage.removeItem('gemdriveAuthDriveUri');
const state = returnedState.slice(savedState.length);
urlParams.delete('state');
const scopeParam = urlParams.get('scope');
let perms;
if (scopeParam) {
urlParams.delete('scope');
perms = parsePermsFromScope(scopeParam);
}
const redirParamsStr = decodeURIComponent(urlParams.toString());
if (redirParamsStr !== '') {
history.pushState(null, '', window.location.pathname + '?' + redirParamsStr);
}
else {
history.pushState(null, '', window.location.pathname);
}
const codeVerifier = localStorage.getItem('pkceCodeVerifier');
localStorage.removeItem('pkceCodeVerifier');
const tokenUrl = driveUri + `?pauth-method=token`
const params = `grant_type=authorization_code`
+ `&client_id=${encodeURIComponent(window.location.origin)}`
+ `&redirect_uri=${encodeURIComponent(window.location.href)}`
+ `&code=${code}`
+ `&code_verifier=${codeVerifier}`;
const accessToken = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: params,
})
.then(r => r.json())
.then(json => json.access_token);
return {
state,
accessToken,
perms,
urlParams,
};
}
function validateAuthOptions(options) {
if (!options) {
throw new Error("Must provide options object");
}
const required = [
'driveUri',
];
for (const req of required) {
if (!options[req]) {
throw new Error("Missing " + req);
}
}
}
function encodeScopeFromPerms(perms) {
let scope = '';
for (const permParams of perms) {
scope += `type=${permParams.type};perm=${permParams.perm}`;
if (permParams.path) {
const path = permParams.path;
const trimmedPath = path.length > 1 && path.endsWith('/') ? path.slice(0, path.length - 1) : path;
scope += `;path=${trimmedPath.replace(/ /g, '[]')}`;
}
if (permParams.hint) {
scope += `;hint=${permParams.hint.replace(/ /g, '[]')}`
}
scope += ' ';
}
// remove trailing space
return scope.slice(0, scope.length - 1);
}
function parsePermsFromScope(scope) {
const allPerms = [];
const items = scope.split(' ');
for (const item of items) {
const perms = {};
const params = item.split(';');
for (const param of params) {
const parts = param.split('=');
const key = parts[0];
const value = parts[1];
perms[key] = value.replace(/\[\]/g, ' ');
}
allPerms.push(perms);
}
return allPerms;
}
// The following functions were taken from:
// https://github.com/aaronpk/pkce-vanilla-js
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
const array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
export {
authorize,
completeAuthorization,
};