forked from SmartThingsCommunity/smartthings-core-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
authenticator.ts
148 lines (125 loc) · 4.51 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
142
143
144
145
146
147
148
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
import { MutexInterface } from 'async-mutex'
import { EndpointClientConfig } 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?(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void>
acquireRefreshMutex?(): Promise<MutexInterface.Releaser>
/**
* Performs required authentication steps to add credentials to the axios config, typically via Bearer Auth headers.
* Expected to call other functions such as @see refresh as needed to return valid credentials.
*
* @param requestConfig AxiosRequestConfig to add credentials to and return otherwise unmodified
*/
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig>
/**
* Performs required authentication steps and returns credentials as a string value
* Expected to perform any required steps (such as token refresh) needed to return valid credentials.
*
* @returns {string} valid auth token
*/
authenticateGeneric?(): Promise<string>
}
/**
* For use in tests or on endpoints that don't need any authentication.
*/
export class NoOpAuthenticator implements Authenticator {
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
return Promise.resolve(requestConfig)
}
authenticateGeneric(): Promise<string> {
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(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
return Promise.resolve({
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: `Bearer ${this.token}`,
},
})
}
authenticateGeneric(): Promise<string> {
return Promise.resolve(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.
*/
export class RefreshTokenAuthenticator implements Authenticator {
constructor(public token: string, private tokenStore: RefreshTokenStore) {
// simple
}
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
return Promise.resolve({
...requestConfig,
headers: {
...requestConfig.headers,
Authorization: `Bearer ${this.token}`,
},
})
}
async refresh(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void> {
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
requestConfig.headers.Authorization = `Bearer ${this.token}`
return this.tokenStore.putAuthData(authData)
}
throw Error(`error ${response.status} refreshing token, with message ${response.data}`)
}
}
export class SequentialRefreshTokenAuthenticator extends RefreshTokenAuthenticator {
constructor(token: string, tokenStore: RefreshTokenStore, private refreshMutex: MutexInterface) {
super(token, tokenStore)
}
acquireRefreshMutex(): Promise<MutexInterface.Releaser> {
return this.refreshMutex.acquire()
}
}