summaryrefslogtreecommitdiff
path: root/auth.js
blob: 872b4aa3ab4bb925b24f33625268abcfbcc03f81 (plain)
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('')
  }

}