-
-
Notifications
You must be signed in to change notification settings - Fork 798
Expand file tree
/
Copy pathcore.ts
More file actions
244 lines (202 loc) · 6.91 KB
/
Copy pathcore.ts
File metadata and controls
244 lines (202 loc) · 6.91 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import { NotAuthenticated, FeathersError } from '@feathersjs/errors'
import { Application, Params } from '@feathersjs/feathers'
import { AuthenticationRequest, AuthenticationResult } from '@feathersjs/authentication'
import { Storage, StorageWrapper } from './storage'
class OauthError extends FeathersError {
constructor(message: string, data?: any) {
super(message, 'OauthError', 401, 'oauth-error', data)
}
}
const getMatch = (location: Location, key: string): [string, RegExp] => {
const regex = new RegExp(`(?:\&?)${key}=([^&]*)`)
const match = location.hash ? location.hash.match(regex) : null
if (match !== null) {
const [, value] = match
return [value, regex]
}
return [null, regex]
}
export type ClientConstructor = new (
app: Application,
options: AuthenticationClientOptions
) => AuthenticationClient
export interface AuthenticationClientOptions {
storage: Storage
header: string
scheme: string
storageKey: string
locationKey: string
locationErrorKey: string
jwtStrategy: string
path: string
Authentication: ClientConstructor
}
export class AuthenticationClient {
app: Application
authenticated: boolean
options: AuthenticationClientOptions
constructor(app: Application, options: AuthenticationClientOptions) {
const socket = app.io
const storage = new StorageWrapper(app.get('storage') || options.storage)
this.app = app
this.options = options
this.authenticated = false
this.app.set('storage', storage)
if (socket) {
this.handleSocket(socket)
}
}
get service() {
return this.app.service(this.options.path)
}
get storage() {
return this.app.get('storage') as Storage
}
handleSocket(socket: any) {
// When the socket disconnects and we are still authenticated, try to reauthenticate right away
// the websocket connection will handle timeouts and retries
socket.on('disconnect', () => {
if (this.authenticated) {
this.reAuthenticate(true)
}
})
}
/**
* Parse the access token or authentication error from the window location hash. Will remove it from the hash
* if found.
*
* @param location The window location
* @returns The access token if available, will throw an error if found, otherwise null
*/
getFromLocation(location: Location) {
const [accessToken, tokenRegex] = getMatch(location, this.options.locationKey)
if (accessToken !== null) {
location.hash = location.hash.replace(tokenRegex, '')
return Promise.resolve(accessToken)
}
const [message, errorRegex] = getMatch(location, this.options.locationErrorKey)
if (message !== null) {
location.hash = location.hash.replace(errorRegex, '')
return Promise.reject(new OauthError(decodeURIComponent(message)))
}
return Promise.resolve(null)
}
/**
* Set the access token in storage.
*
* @param accessToken The access token to set
* @returns
*/
setAccessToken(accessToken: string) {
return this.storage.setItem(this.options.storageKey, accessToken)
}
/**
* Returns the access token from storage or the window location hash.
*
* @returns The access token from storage or location hash
*/
getAccessToken(): Promise<string | null> {
return this.storage.getItem(this.options.storageKey).then((accessToken: string) => {
if (!accessToken && typeof window !== 'undefined' && window.location) {
return this.getFromLocation(window.location)
}
return accessToken || null
})
}
/**
* Remove the access token from storage
* @returns The removed access token
*/
removeAccessToken() {
return this.storage.removeItem(this.options.storageKey)
}
/**
* Reset the internal authentication state. Usually not necessary to call directly.
*
* @returns null
*/
reset() {
this.app.set('authentication', null)
this.authenticated = false
return Promise.resolve(null)
}
handleError(error: FeathersError, type: 'authenticate' | 'logout') {
// For NotAuthenticated, PaymentError, Forbidden, NotFound, MethodNotAllowed, NotAcceptable
// errors, remove the access token
if (error.code > 400 && error.code < 408) {
const promise = this.removeAccessToken().then(() => this.reset())
return type === 'logout' ? promise : promise.then(() => Promise.reject(error))
}
return this.reset().then(() => Promise.reject(error))
}
/**
* Try to reauthenticate using the token from storage. Will do nothing if already authenticated unless
* `force` is true.
*
* @param force force reauthentication with the server
* @param strategy The name of the strategy to use. Defaults to `options.jwtStrategy`
* @returns The reauthentication result
*/
reAuthenticate(force = false, strategy?: string): Promise<AuthenticationResult> {
// Either returns the authentication state or
// tries to re-authenticate with the stored JWT and strategy
let authPromise = this.app.get('authentication')
if (!authPromise || force === true) {
authPromise = this.getAccessToken().then((accessToken) => {
if (!accessToken) {
return this.handleError(new NotAuthenticated('No accessToken found in storage'), 'authenticate')
}
return this.authenticate({
strategy: strategy || this.options.jwtStrategy,
accessToken
})
})
this.app.set('authentication', authPromise)
}
return authPromise
}
/**
* Authenticate using a specific strategy and data.
*
* @param authentication The authentication data
* @param params Additional parameters
* @returns The authentication result
*/
authenticate(authentication?: AuthenticationRequest, params?: Params): Promise<AuthenticationResult> {
if (!authentication) {
return this.reAuthenticate()
}
const promise = this.service
.create(authentication, params)
.then((authResult: AuthenticationResult) => {
const { accessToken } = authResult
this.authenticated = true
this.app.emit('login', authResult)
this.app.emit('authenticated', authResult)
return this.setAccessToken(accessToken).then(() => authResult)
})
.catch((error: FeathersError) => this.handleError(error, 'authenticate'))
this.app.set('authentication', promise)
return promise
}
/**
* Log out the current user and remove their token. Will do nothing
* if not authenticated.
*
* @returns The log out result.
*/
logout(): Promise<AuthenticationResult | null> {
return Promise.resolve(this.app.get('authentication'))
.then(() =>
this.service.remove(null).then((authResult: AuthenticationResult) =>
this.removeAccessToken()
.then(() => this.reset())
.then(() => {
this.app.emit('logout', authResult)
return authResult
})
)
)
.catch((error: FeathersError) => this.handleError(error, 'logout'))
}
}