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
|
export default {
async logIn(graffitiURL) {
// Generate a random client secret and state
const clientSecret = this.randomString()
const state = this.randomString()
// 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
},
randomString() {
return Math.random().toString(36).substr(2)
},
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('')
}
}
|