11import createClient , { Client , Middleware } from "openapi-fetch" ;
22import type { FetchOptions } from "openapi-fetch" ;
3- import { AccessToken , ClientCredentials } from "simple-oauth2" ;
43import { ApiClientError } from "./apiClientError.js" ;
54import { paths , operations } from "./openapi.js" ;
65import { CommonProperties , TelemetryEvent } from "../../telemetry/types.js" ;
76import { packageInfo } from "../packageInfo.js" ;
87import 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 " ;
1110
1211const ATLAS_API_VERSION = "2025-03-12" ;
1312
@@ -22,6 +21,11 @@ export interface ApiClientOptions {
2221 userAgent ?: string ;
2322}
2423
24+ interface AccessToken {
25+ access_token : string ;
26+ expires_at ?: number ;
27+ }
28+
2529export class ApiClient {
2630 private options : {
2731 baseUrl : string ;
@@ -36,24 +40,33 @@ export class ApiClient {
3640 useEnvironmentVariableProxies : true ,
3741 } ) as unknown as typeof fetch ;
3842
39- private static customAgent = useOrCreateAgent ( {
40- useEnvironmentVariableProxies : true ,
41- } ) ;
42-
4343 private client : Client < paths > ;
44- private oauth2Client ?: ClientCredentials ;
44+
45+ private oauth2Client ?: oauth . Client ;
46+ private oauth2Issuer ?: oauth . AuthorizationServer ;
4547 private accessToken ?: AccessToken ;
4648
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+ }
5059
5160 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 ;
5563 }
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 ;
5770 } ;
5871
5972 private authMiddleware : Middleware = {
@@ -90,46 +103,93 @@ export class ApiClient {
90103 } ,
91104 fetch : ApiClient . customFetch ,
92105 } ) ;
106+
93107 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+
111121 this . client . use ( this . authMiddleware ) ;
112122 }
113123 }
114124
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+ }
117176 }
118177
119178 public async validateAccessToken ( ) : Promise < void > {
120179 await this . getAccessToken ( ) ;
121180 }
122181
123182 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 ) ;
130187 }
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 } ` ) ;
132191 }
192+ this . accessToken = undefined ;
133193 }
134194
135195 public async getIpInfo ( ) : Promise < {
0 commit comments