• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 20853900721

09 Jan 2026 01:45PM UTC coverage: 79.931% (+0.3%) from 79.647%
20853900721

Pull #836

github

web-flow
Merge 97195e48d into 7043c219d
Pull Request #836: [DRAFT] Add configurable api / auth client support

1536 of 1988 branches covered (77.26%)

Branch coverage included in aggregate %.

206 of 209 new or added lines in 6 files covered. (98.56%)

66 existing lines in 2 files now uncovered.

6979 of 8665 relevant lines covered (80.54%)

88.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.46
/src/common/atlas/auth/clientCredentials.ts
1
import * as oauth from "oauth4webapi";
3✔
2
import type { Middleware } from "openapi-fetch";
3
import type { LoggerBase } from "../../logger.js";
4
import { LogId } from "../../logger.js";
3✔
5
import { createFetch } from "@mongodb-js/devtools-proxy-support";
3✔
6
import type { AccessToken, AuthProvider } from "./authProvider.js";
7

8
export interface ClientCredentialsAuthOptions {
9
    clientId: string;
10
    clientSecret: string;
11
    baseUrl: string;
12
    userAgent: string;
13
}
14

15
export class ClientCredentialsAuthProvider implements AuthProvider {
3✔
16
    private oauth2Client?: oauth.Client;
17
    private oauth2Issuer?: oauth.AuthorizationServer;
18
    private accessToken?: AccessToken;
19
    private readonly options: ClientCredentialsAuthOptions;
20
    private readonly logger: LoggerBase;
21
    private static customFetch: typeof fetch = createFetch({
3✔
22
        useEnvironmentVariableProxies: true,
55✔
23
    }) as unknown as typeof fetch;
55✔
24

25
    constructor(options: ClientCredentialsAuthOptions, logger: LoggerBase) {
3✔
26
        this.options = options;
44✔
27
        this.logger = logger;
44✔
28

29
        this.oauth2Issuer = {
44✔
30
            issuer: options.baseUrl,
44✔
31
            token_endpoint: new URL("/api/oauth/token", options.baseUrl).toString(),
44✔
32
            revocation_endpoint: new URL("/api/oauth/revoke", options.baseUrl).toString(),
44✔
33
            token_endpoint_auth_methods_supported: ["client_secret_basic"],
44✔
34
            grant_types_supported: ["client_credentials"],
44✔
35
        };
44✔
36

37
        this.oauth2Client = {
44✔
38
            client_id: options.clientId,
44✔
39
            client_secret: options.clientSecret,
44✔
40
        };
44✔
41
    }
44✔
42

43
    public async getAuthHeaders(): Promise<Record<string, string> | undefined> {
3✔
44
        const accessToken = await this.getAccessToken();
45✔
45
        return accessToken
44✔
46
            ? {
34✔
47
                  Authorization: `Bearer ${accessToken}`,
34✔
48
              }
34!
49
            : undefined;
10✔
50
    }
45✔
51

52
    public hasCredentials(): boolean {
3✔
53
        return !!this.oauth2Client && !!this.oauth2Issuer;
205✔
54
    }
205✔
55

56
    private isAccessTokenValid(): boolean {
3✔
57
        return !!(
165✔
58
            this.accessToken &&
165✔
59
            this.accessToken.expires_at !== undefined &&
138✔
60
            this.accessToken.expires_at > Date.now()
138✔
61
        );
62
    }
165✔
63

64
    private getOauthClientAuth(): { client: oauth.Client | undefined; clientAuth: oauth.ClientAuth | undefined } {
3✔
65
        const clientSecret = this.options.clientSecret;
48✔
66
        const clientId = this.options.clientId;
48✔
67

68
        // We are using our own ClientAuth because ClientSecretBasic URL encodes wrongly
69
        // the username and password (for example, encodes `_` to %5F, which is wrong).
70
        return {
48✔
71
            client: { client_id: clientId },
48✔
72
            clientAuth: (_as, client, _body, headers): void => {
48✔
73
                const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
34✔
74
                headers.set("Authorization", `Basic ${credentials}`);
34✔
75
            },
34✔
76
        };
48✔
77
    }
48✔
78

79
    private async getNewAccessToken(): Promise<AccessToken | undefined> {
3✔
80
        if (!this.hasCredentials() || !this.oauth2Issuer) {
29!
NEW
81
            return undefined;
×
NEW
82
        }
×
83

84
        const { client, clientAuth } = this.getOauthClientAuth();
29✔
85
        if (client && clientAuth) {
29✔
86
            try {
29✔
87
                const response = await oauth.clientCredentialsGrantRequest(
29✔
88
                    this.oauth2Issuer,
29✔
89
                    client,
29✔
90
                    clientAuth,
29✔
91
                    new URLSearchParams(),
29✔
92
                    {
29✔
93
                        [oauth.customFetch]: ClientCredentialsAuthProvider.customFetch,
29✔
94
                        headers: {
29✔
95
                            "User-Agent": this.options.userAgent,
29✔
96
                        },
29✔
97
                    }
29✔
98
                );
29✔
99

100
                const result = await oauth.processClientCredentialsResponse(this.oauth2Issuer, client, response);
27✔
101
                this.accessToken = {
8✔
102
                    access_token: result.access_token,
8✔
103
                    expires_at: Date.now() + (result.expires_in ?? 0) * 1000,
29!
104
                };
29✔
105
            } catch (error: unknown) {
29!
106
                const err = error instanceof Error ? error : new Error(String(error));
21!
107
                this.logger.error({
21✔
108
                    id: LogId.atlasConnectFailure,
21✔
109
                    context: "clientCredentialsAuth",
21✔
110
                    message: `Failed to request access token: ${err.message}`,
21✔
111
                });
21✔
112
            }
21✔
113
            return this.accessToken;
29✔
114
        }
29!
115

NEW
116
        return undefined;
×
117
    }
29✔
118

119
    public async getAccessToken(): Promise<string | undefined> {
3✔
120
        if (!this.hasCredentials()) {
166!
121
            return undefined;
1✔
122
        }
1✔
123

124
        if (!this.isAccessTokenValid()) {
166✔
125
            this.accessToken = await this.getNewAccessToken();
29✔
126
        }
29✔
127

128
        return this.accessToken?.access_token;
166✔
129
    }
166✔
130

131
    public async validateAccessToken(): Promise<void> {
3✔
132
        await this.getAccessToken();
21✔
133
    }
21✔
134

135
    public async revokeAccessToken(): Promise<void> {
3✔
136
        const { client, clientAuth } = this.getOauthClientAuth();
19✔
137
        try {
19✔
138
            if (this.oauth2Issuer && this.accessToken && client && clientAuth) {
19✔
139
                await oauth.revocationRequest(this.oauth2Issuer, client, clientAuth, this.accessToken.access_token);
10✔
140
            }
8✔
141
        } catch (error: unknown) {
19!
142
            const err = error instanceof Error ? error : new Error(String(error));
2!
143
            this.logger.error({
2✔
144
                id: LogId.atlasApiRevokeFailure,
2✔
145
                context: "clientCredentialsAuth",
2✔
146
                message: `Failed to revoke access token: ${err.message}`,
2✔
147
            });
2✔
148
        }
2✔
149
        this.accessToken = undefined;
19✔
150
    }
19✔
151

152
    public middleware(): Middleware {
3✔
153
        return {
30✔
154
            onRequest: async ({ request, schemaPath }): Promise<Request | undefined> => {
30✔
155
                if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
103!
156
                    return undefined;
2✔
157
                }
2✔
158

159
                try {
101✔
160
                    const accessToken = await this.getAccessToken();
101✔
161
                    if (accessToken) {
103✔
162
                        request.headers.set("Authorization", `Bearer ${accessToken}`);
98✔
163
                    }
98✔
164
                    return request;
100✔
165
                } catch {
103!
166
                    // ignore not available tokens, API will return 401
167
                    return undefined;
1✔
168
                }
1✔
169
            },
103✔
170
        };
30✔
171
    }
30✔
172
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc