-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
159 lines (134 loc) · 4.77 KB
/
index.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
/**
* Simple general purpose OAuth 2.0 Client flow library
*/
const parseParams = hashOrSearch => hashOrSearch.slice(1) // drop '#' or '?'
.split('&')
.reduce((obj, curr) => {
let pair = curr.split('=')
obj[pair[0]] = pair[1]
return obj
}, {})
const buildParams = kv => Object.keys(kv)
.reduce((str, key) => kv[key] === undefined ? str : str + `${key}=${encodeURIComponent(kv[key])}&`, '?')
.slice(0, -1) // drop trailing '&'
export const ENDPOINT_GOOGLE = 'https://accounts.google.com/o/oauth2/v2/auth'
export const ENDPOINT_FACEBOOK = 'https://www.facebook.com/v2.8/dialog/oauth'
export const ENDPOINT_GITHUB = 'http://github.com/login/oauth/authorize'
import {
EVENT_REQUEST_OPENER_ORIGIN,
EVENT_PROVIDE_OPENER_ORIGIN,
EVENT_OAUTH_RESULT
} from './events'
import { fixWronglyDecoded } from './util'
let popupWindow, pollInterval
export default function oauth(endpoint, {
// Required params
client_id,
// Optional params
scope,
state = Math.random().toString(32).substr(2),
display = 'popup',
prompt = 'consent',
...others
// redirect_uri
// include_granted_scopes
} = {}, {
// Popup window features
popupHeight = 600,
popupWidth = 400
} = {}) {
if (!endpoint) throw new Error('OAuth `endpoint` must be provided')
if (!client_id) throw new Error('OAuth `client_id` must be provided')
if (popupWindow) throw new Error("Previous OAuth flow hasn't finished")
if (Array.isArray(scope)) scope = scope.join(' ')
let options = {
client_id,
scope,
display,
prompt,
state,
...others
}
addNonceForGoogleIdToken(options)
let url = endpoint + buildParams(options)
/**
* Open the popup
* Since Chrome 59, `location` must be `no`
* https://stackoverflow.com/questions/44417724/facebook-authentication-opening-tab-instead-of-popup-in-chrome-59
*/
popupWindow = window.open(
url,
'',
`width=${popupWidth},height=${popupHeight},top=${window.screenY + (window.outerHeight - popupHeight) / 2},left=${window.screenX + (window.outerWidth - popupWidth) / 2},location=no,toolbar=no,menubar=no`
)
return new Promise((res, rej) => {
const reject = arg => {
cleanup()
window.removeEventListener('message', handleMessage)
rej(arg)
}
const resolve = arg => {
cleanup()
window.removeEventListener('message', handleMessage)
res(arg)
}
// poll to see if the popup is closed
pollInterval = setInterval(() => {
try {
if (!popupWindow || popupWindow.closed) {
reject(new Error('User cancelled oauth'))
}
} catch (e) {
// The iframe has been redirected `redirect_url`, which may not be same origin as host
if (e.message === 'no access') {
// do nothing
} else {
reject(e)
}
}
}, 100)
const handleMessage = ({ origin, source, data: { type, hash, search } = {} }) => {
/**
* Check whether the event comes from pending instance's popup,
* before providing `origin` and `state`
*/
if (!popupWindow || source !== popupWindow) return
/**
* `state` is encoded before transfer, so we need to compare encoded result
*/
let encodedState = encodeURIComponent(state)
/**
* Send host(opener) origin to popup when requested,
* so it can postMessage back with proper origin
*/
if (type === EVENT_REQUEST_OPENER_ORIGIN) {
source.postMessage({
type: EVENT_PROVIDE_OPENER_ORIGIN,
origin: location.origin,
encodedState
}, origin)
}
/**
* Handle OAuth result from popup
*/
else if (type === EVENT_OAUTH_RESULT) {
let result = parseParams(hash || search)
if (encodedState !== result.state &&
encodedState !== fixWronglyDecoded(result.state)) return
delete result.state
result.error ? reject(result) : resolve(result)
}
}
window.addEventListener('message', handleMessage)
})
}
function cleanup() {
popupWindow && popupWindow.close()
clearInterval(pollInterval)
popupWindow = null
}
function addNonceForGoogleIdToken(options) {
if (options.response_type === 'id_token' && options.nonce === undefined) {
options.nonce = Math.round(Math.random() * 10000000000)
}
}