-
Notifications
You must be signed in to change notification settings - Fork 54
/
authenticator.ts
141 lines (121 loc) · 4.29 KB
/
authenticator.ts
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
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
import { MutexInterface } from 'async-mutex'
import { EndpointClientConfig, HttpClientHeaders } from './endpoint-client'
/**
* Implement this interface to implement a process for handling authentication.
*
* This is not meant to be a "service" in the traditional sense because
* implementors are not expected to be stateless.
*/
export interface Authenticator {
login?(): Promise<void>
logout?(): Promise<void>
refresh?(clientConfig: EndpointClientConfig): Promise<HttpClientHeaders>
acquireRefreshMutex?(): Promise<MutexInterface.Releaser>
/**
* Performs required authentication steps and returns credentials as a set of HTTP headers which
* must be included in authenticated requests.
* Expected to perform any required steps (such as token refresh) needed to return valid credentials.
*
* @returns {string} valid auth token
*/
authenticate(): Promise<HttpClientHeaders>
}
/**
* For use in tests or on endpoints that don't need any authentication.
*/
export class NoOpAuthenticator implements Authenticator {
authenticate(): Promise<HttpClientHeaders> {
return Promise.resolve({})
}
}
/**
* A simple bearer token authenticator that knows nothing about refreshing
* or logging in our out. If the token is expired, it simply won't work.
*/
export class BearerTokenAuthenticator implements Authenticator {
constructor(public token: string) {
// simple
}
authenticate(): Promise<HttpClientHeaders> {
/*
return Promise.resolve({
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: `Bearer ${this.token}`,
},
})
*/
return Promise.resolve({ Authorization: `Bearer ${this.token}` })
}
}
export interface AuthData {
authToken: string
refreshToken: string
}
export interface RefreshData {
refreshToken: string
clientId: string
clientSecret: string
}
export interface RefreshTokenStore {
getRefreshData(): Promise<RefreshData>
putAuthData(data: AuthData): Promise<void>
}
/**
* An authenticator that supports refreshing of the access token using a refresh token by loading
* the refresh token, client ID, and client secret from a token store, performing the refresh, and
* storing the new tokens.
*
* Note that corruption of the refresh token is unlikely but possible if two of the same
* authenticators refresh the same token at the same time.
*/
export class RefreshTokenAuthenticator implements Authenticator {
constructor(public token: string, private tokenStore: RefreshTokenStore) {
// simple
}
authenticate(): Promise<HttpClientHeaders> {
return Promise.resolve({ Authorization: `Bearer ${this.token}` })
}
async refresh(clientConfig: EndpointClientConfig): Promise<HttpClientHeaders> {
const refreshData: RefreshData = await this.tokenStore.getRefreshData()
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(`${refreshData.clientId}:${refreshData.clientSecret}`, 'ascii').toString('base64'),
'Accept': 'application/json',
}
const axiosConfig: AxiosRequestConfig = {
url: clientConfig.urlProvider?.authURL,
method: 'POST',
headers,
data: `grant_type=refresh_token&client_id=${refreshData.clientId}&refresh_token=${refreshData.refreshToken}`,
}
const response: AxiosResponse = await axios.request(axiosConfig)
if (response.status > 199 && response.status < 300) {
const authData: AuthData = {
authToken: response.data.access_token,
refreshToken: response.data.refresh_token,
}
this.token = authData.authToken
await this.tokenStore.putAuthData(authData)
return { Authorization: `Bearer ${this.token}` }
}
throw Error(`error ${response.status} refreshing token, with message ${response.data}`)
}
}
/**
* A an authenticator that works like RefreshTokenAuthenticator but which can use a mutex to help
* prevent corruption of the refresh token.
*
* Note that while `acquireRefreshMutex` is provided for you to use the mutex, the mutex is not
* automatically used.
*/
export class SequentialRefreshTokenAuthenticator extends RefreshTokenAuthenticator {
constructor(token: string, tokenStore: RefreshTokenStore, private refreshMutex: MutexInterface) {
super(token, tokenStore)
}
acquireRefreshMutex(): Promise<MutexInterface.Releaser> {
return this.refreshMutex.acquire()
}
}