diff options
Diffstat (limited to 'src/auth.js')
-rw-r--r-- | src/auth.js | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..8ee803d --- /dev/null +++ b/src/auth.js @@ -0,0 +1,125 @@ +export default { + + async logIn(graffitiURL) { + // Generate a random client secret and state + const clientSecret = crypto.randomUUID() + const state = crypto.randomUUID() + + // The client ID is the secret's hex hash + const clientID = await this.sha256(clientSecret) + + // Store the client secret as a local variable + window.localStorage.setItem('graffitiClientSecret', clientSecret) + window.localStorage.setItem('graffitiClientID', clientID) + window.localStorage.setItem('graffitiAuthState', state) + + // Redirect to the login window + const loginURL = this.authURL(graffitiURL) + loginURL.searchParams.set('client_id', clientID) + loginURL.searchParams.set('redirect_uri', window.location.href) + loginURL.searchParams.set('state', state) + window.location.href = loginURL + }, + + async connect(graffitiURL) { + + // Check to see if we are already logged in + let token = window.localStorage.getItem('graffitiToken') + let myID = window.localStorage.getItem('graffitiID') + + if (!token || !myID) { + // Remove them both in case one exists + // and the other does not + token = myID = null + + // Check to see if we are redirecting back + const url = new URL(window.location) + if (url.searchParams.has('code')) { + + // Extract the code and state from the URL and strip it from the history + const code = url.searchParams.get('code') + const state = url.searchParams.get('state') + url.searchParams.delete('code') + url.searchParams.delete('state') + window.history.replaceState({}, '', url) + + // Get stored variables and remove them + const clientSecret = window.localStorage.getItem('graffitiClientSecret') + const clientID = window.localStorage.getItem('graffitiClientID') + const storedState = window.localStorage.getItem('graffitiAuthState') + window.localStorage.removeItem('graffitiClientSecret') + window.localStorage.removeItem('graffitiClientID') + window.localStorage.removeItem('graffitiAuthState') + + // Make sure state has been preserved + if (state != storedState) { + throw new Error("The state in local storage does not match the state sent by the server") + } + + // Construct the body of the POST + let form = new FormData() + form.append('client_id', clientID) + form.append('client_secret', clientSecret) + form.append('code', code) + + // Ask to exchange the code for a token + const tokenURL = this.authURL(graffitiURL) + tokenURL.pathname = '/token' + const response = await fetch(tokenURL, { + method: 'post', + body: form + }) + + // Make sure the response is OK + if (!response.ok) { + let reason = response.status + ": " + try { + reason += (await response.json()).detail + } catch (e) { + reason += response.statusText + } + + throw new Error(`The authorization code could not be exchanged for a token.\n\n${reason}`) + } + + // Parse out the token + const data = await response.json() + token = data.access_token + myID = data.owner_id + + // And make sure that the token is valid + if (!token || !myID) { + throw new Error(`The authorization token could not be parsed from the response.\n\n${data}`) + } + + // Store the token and ID + window.localStorage.setItem('graffitiToken', token) + window.localStorage.setItem('graffitiID', myID) + } + } + + return { myID, token } + + }, + + logOut() { + window.localStorage.removeItem('graffitiToken') + window.localStorage.removeItem('graffitiID') + window.location.reload() + }, + + authURL(graffitiURL) { + const url = new URL(graffitiURL) + url.host = "auth." + url.host + return url + }, + + async sha256(input) { + const encoder = new TextEncoder() + const inputBytes = encoder.encode(input) + const outputBuffer = await crypto.subtle.digest('SHA-256', inputBytes) + const outputArray = Array.from(new Uint8Array(outputBuffer)) + return outputArray.map(b => b.toString(16).padStart(2, '0')).join('') + } + +} |