1
1
import createClient , { Client , Middleware } from "openapi-fetch" ;
2
2
import type { FetchOptions } from "openapi-fetch" ;
3
- import { AccessToken , ClientCredentials } from "simple-oauth2" ;
4
3
import { ApiClientError } from "./apiClientError.js" ;
5
4
import { paths , operations } from "./openapi.js" ;
6
5
import { CommonProperties , TelemetryEvent } from "../../telemetry/types.js" ;
7
6
import { packageInfo } from "../packageInfo.js" ;
8
7
import logger , { LogId } from "../logger.js" ;
9
- import { createFetch , useOrCreateAgent } from "@mongodb-js/devtools-proxy-support" ;
10
- import HTTPS from "https " ;
8
+ import { createFetch } from "@mongodb-js/devtools-proxy-support" ;
9
+ import * as oauth from "oauth4webapi " ;
11
10
12
11
const ATLAS_API_VERSION = "2025-03-12" ;
13
12
@@ -22,6 +21,11 @@ export interface ApiClientOptions {
22
21
userAgent ?: string ;
23
22
}
24
23
24
+ interface AccessToken {
25
+ access_token : string ;
26
+ expires_at ?: number ;
27
+ }
28
+
25
29
export class ApiClient {
26
30
private options : {
27
31
baseUrl : string ;
@@ -36,24 +40,33 @@ export class ApiClient {
36
40
useEnvironmentVariableProxies : true ,
37
41
} ) as unknown as typeof fetch ;
38
42
39
- private static customAgent = useOrCreateAgent ( {
40
- useEnvironmentVariableProxies : true ,
41
- } ) ;
42
-
43
43
private client : Client < paths > ;
44
- private oauth2Client ?: ClientCredentials ;
44
+
45
+ private oauth2Client ?: oauth . Client ;
46
+ private oauth2Issuer ?: oauth . AuthorizationServer ;
45
47
private accessToken ?: AccessToken ;
46
48
47
- private ensureAgentIsInitialized = async ( ) => {
48
- await ApiClient . customAgent ?. initialize ?.( ) ;
49
- } ;
49
+ public hasCredentials ( ) : boolean {
50
+ return ! ! this . oauth2Client && ! ! this . oauth2Issuer ;
51
+ }
52
+
53
+ private isAccessTokenValid ( ) : boolean {
54
+ return ! ! (
55
+ this . accessToken &&
56
+ ( this . accessToken . expires_at == undefined || this . accessToken . expires_at > Date . now ( ) / 1000 )
57
+ ) ;
58
+ }
50
59
51
60
private getAccessToken = async ( ) => {
52
- // await this.ensureAgentIsInitialized();
53
- if ( this . oauth2Client && ( ! this . accessToken || this . accessToken . expired ( ) ) ) {
54
- this . accessToken = await this . oauth2Client . getToken ( { } ) ;
61
+ if ( ! this . hasCredentials ( ) ) {
62
+ return undefined ;
55
63
}
56
- return this . accessToken ?. token . access_token as string | undefined ;
64
+
65
+ if ( ! this . isAccessTokenValid ( ) ) {
66
+ this . accessToken = await this . getNewAccessToken ( ) ;
67
+ }
68
+
69
+ return this . accessToken ?. access_token ;
57
70
} ;
58
71
59
72
private authMiddleware : Middleware = {
@@ -90,46 +103,93 @@ export class ApiClient {
90
103
} ,
91
104
fetch : ApiClient . customFetch ,
92
105
} ) ;
106
+
93
107
if ( this . options . credentials ?. clientId && this . options . credentials ?. clientSecret ) {
94
- this . oauth2Client = new ClientCredentials ( {
95
- client : {
96
- id : this . options . credentials . clientId ,
97
- secret : this . options . credentials . clientSecret ,
98
- } ,
99
- auth : {
100
- tokenHost : this . options . baseUrl ,
101
- tokenPath : "/api/oauth/token" ,
102
- revokePath : "/api/oauth/revoke" ,
103
- } ,
104
- http : {
105
- headers : {
106
- "User-Agent" : this . options . userAgent ,
107
- } ,
108
- agent : ApiClient . customAgent ,
109
- } ,
110
- } ) ;
108
+ this . oauth2Issuer = {
109
+ issuer : this . options . baseUrl ,
110
+ token_endpoint : new URL ( "/api/oauth/token" , this . options . baseUrl ) . toString ( ) ,
111
+ revocation_endpoint : new URL ( "/api/oauth/revoke" , this . options . baseUrl ) . toString ( ) ,
112
+ token_endpoint_auth_methods_supported : [ "client_secret_basic" ] ,
113
+ grant_types_supported : [ "client_credentials" ] ,
114
+ } ;
115
+
116
+ this . oauth2Client = {
117
+ client_id : this . options . credentials . clientId ,
118
+ client_secret : this . options . credentials . clientSecret ,
119
+ } ;
120
+
111
121
this . client . use ( this . authMiddleware ) ;
112
122
}
113
123
}
114
124
115
- public hasCredentials ( ) : boolean {
116
- return ! ! this . oauth2Client ;
125
+ private getOauthClientAuth ( ) : { client : oauth . Client | undefined ; clientAuth : oauth . ClientAuth | undefined } {
126
+ if ( this . options . credentials ?. clientId && this . options . credentials . clientSecret ) {
127
+ const clientSecret = this . options . credentials . clientSecret ;
128
+ const clientId = this . options . credentials . clientId ;
129
+
130
+ // We are using our own ClientAuth because ClientSecretBasic URL encodes wrongly
131
+ // the username and password (for example, encodes `_` which is wrong).
132
+ return {
133
+ client : { client_id : clientId } ,
134
+ clientAuth : ( _as , client , _body , headers ) => {
135
+ const credentials = Buffer . from ( `${ clientId } :${ clientSecret } ` ) . toString ( "base64" ) ;
136
+ headers . set ( "Authorization" , `Basic ${ credentials } ` ) ;
137
+ } ,
138
+ } ;
139
+ }
140
+
141
+ return { client : undefined , clientAuth : undefined } ;
142
+ }
143
+
144
+ private async getNewAccessToken ( ) : Promise < AccessToken | undefined > {
145
+ if ( ! this . hasCredentials ( ) || ! this . oauth2Issuer ) {
146
+ return undefined ;
147
+ }
148
+
149
+ const { client, clientAuth } = this . getOauthClientAuth ( ) ;
150
+ if ( client && clientAuth ) {
151
+ try {
152
+ const response = await oauth . clientCredentialsGrantRequest (
153
+ this . oauth2Issuer ,
154
+ client ,
155
+ clientAuth ,
156
+ new URLSearchParams ( ) ,
157
+ {
158
+ [ oauth . customFetch ] : ApiClient . customFetch ,
159
+ headers : {
160
+ "User-Agent" : this . options . userAgent ,
161
+ } ,
162
+ }
163
+ ) ;
164
+
165
+ const result = await oauth . processClientCredentialsResponse ( this . oauth2Issuer , client , response ) ;
166
+ this . accessToken = {
167
+ access_token : result . access_token ,
168
+ expires_at : Date . now ( ) / 1000 + ( result . expires_in ?? 0 ) ,
169
+ } ;
170
+ } catch ( error : unknown ) {
171
+ const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
172
+ logger . error ( LogId . atlasConnectFailure , "apiClient" , `Failed to request access token: ${ err . message } ` ) ;
173
+ }
174
+ return this . accessToken ;
175
+ }
117
176
}
118
177
119
178
public async validateAccessToken ( ) : Promise < void > {
120
179
await this . getAccessToken ( ) ;
121
180
}
122
181
123
182
public async close ( ) : Promise < void > {
124
- if ( this . accessToken ) {
125
- try {
126
- await this . accessToken . revoke ( "access_token" ) ;
127
- } catch ( error : unknown ) {
128
- const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
129
- logger . error ( LogId . atlasApiRevokeFailure , "apiClient" , `Failed to revoke access token: ${ err . message } ` ) ;
183
+ const { client, clientAuth } = this . getOauthClientAuth ( ) ;
184
+ try {
185
+ if ( this . oauth2Issuer && this . accessToken && client && clientAuth ) {
186
+ await oauth . revocationRequest ( this . oauth2Issuer , client , clientAuth , this . accessToken . access_token ) ;
130
187
}
131
- this . accessToken = undefined ;
188
+ } catch ( error : unknown ) {
189
+ const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
190
+ logger . error ( LogId . atlasApiRevokeFailure , "apiClient" , `Failed to revoke access token: ${ err . message } ` ) ;
132
191
}
192
+ this . accessToken = undefined ;
133
193
}
134
194
135
195
public async getIpInfo ( ) : Promise < {
0 commit comments