ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: 1.17.6, 2.9.2
    • Component/s: Authentication
    • None
    • None
    • Go Drivers
    • None
    • None
    • None
    • None
    • None
    • None

      With authMechanism=MONGODB-OIDC and ENVIRONMENT:gcp, applications occasionally get ReauthenticationRequired (391) from Cursor.Next / Cursor.All when the access token expires while a cursor is open. The initial find/aggregate recovers from 391, but the following getMore does not, so the error reaches the application. Calling Next again cannot recover the cursor.

      Expected: per the auth specification ("Reauthentication"), the driver reauthenticates and re-attempts the getMore, irrespective of retryability.

      Cause (from the driver source):

      • x/mongo/driver/operation.go handles 391 only if op.Authenticator != nil (v2.9.2 line 955, v1.17.6 line 920).
      • x/mongo/driver/batch_cursor.go builds the getMore and killCursors operations without an Authenticator, and CursorOptions has no field to carry it (v2.9.2 line 477, v1.17.6 line 429). mongo/collection.go sets Authenticator only on the initial command.
      • x/mongo/driver/auth/oidc.go caches the GCP token without an expiry (ExpiresAt is nil and unused) and refreshes it only after a 391 or a failed handshake, so all pooled connections expire together and cursors spanning that moment fail.

      Affected: v1.17.6, v2.4.1, v2.8.0, v2.9.2, and master as of 2026-10-08.

      Reproduction: open a cursor with a batchSize small enough to need several getMore calls, let the OIDC token expire (or have the server return 391 on getMore), then iterate. Cursor.Err() returns (ReauthenticationRequired).

      Suggested fix: add an Authenticator to CursorOptions / BatchCursor, set it in the cursor-returning operations' Result (find, aggregate, listCollections, listIndexes), and pass it to the getMore and killCursors operations.

      Environment:
      Go 1.27.1 (linux/amd64 containers on GKE; repro run on darwin/arm64)
      Driver: go.mongodb.org/mongo-driver/v2 v2.8.0 in production; reproduced with v2.9.2. Also affects go.mongodb.org/mongo-driver v1.17.6.
      Server: MongoDB Atlas, dedicated replica set on GCP, MongoDB 8.0.34. Repro verified against a local mongod 8.0.32.
      Auth: MONGODB-OIDC, ENVIRONMENT:gcp with TOKEN_RESOURCE (GKE Workload Identity).

      Repro without waiting for token expiry: a failCommand failpoint returns 391 once. Run against a mongod started with --auth and --setParameter enableTestCommands=1. With SCRAM the reauth itself cannot succeed, but the output shows that the driver attempts reauthentication for find and does not attempt it for getMore.

      Output (driver v2.9.2, mongod 8.0.32):
      391 on find: error reauthenticating: DefaultAuthenticator does not support reauthentication
      391 on getMore: (ReauthenticationRequired) Failing command via 'failCommand' failpoint (after 2 of 10 documents)

      // Repro: the driver attempts reauthentication when find returns 391, but not when getMore does.
      // Run against a mongod started with --auth and --setParameter enableTestCommands=1, for example:
      //
      //	MONGODB_URI='mongodb://admin:admin@localhost:27017/?authSource=admin' go run .
      package main
      
      import (
      	"context"
      	"fmt"
      	"log"
      	"os"
      
      	"go.mongodb.org/mongo-driver/v2/bson"
      	"go.mongodb.org/mongo-driver/v2/mongo"
      	"go.mongodb.org/mongo-driver/v2/mongo/options"
      )
      
      func main() {
      	ctx := context.Background()
      	client, err := mongo.Connect(options.Client().ApplyURI(os.Getenv("MONGODB_URI")))
      	if err != nil {
      		log.Fatal(err)
      	}
      	defer func() { _ = client.Disconnect(ctx) }()
      
      	coll := client.Database("repro").Collection("docs")
      	_ = coll.Drop(ctx)
      	docs := make([]any, 10)
      	for i := range docs {
      		docs[i] = bson.D{{Key: "i", Value: i}}
      	}
      	if _, err := coll.InsertMany(ctx, docs); err != nil {
      		log.Fatal(err)
      	}
      
      	failNext(ctx, client, "find")
      	_, err = coll.Find(ctx, bson.D{})
      	fmt.Println("391 on find:   ", err)
      
      	failNext(ctx, client, "getMore")
      	cursor, err := coll.Find(ctx, bson.D{}, options.Find().SetBatchSize(2))
      	if err != nil {
      		log.Fatal(err)
      	}
      	n := 0
      	for cursor.Next(ctx) {
      		n++
      	}
      	fmt.Printf("391 on getMore: %v (after %d of %d documents)\n", cursor.Err(), n, len(docs))
      }
      
      // failNext makes the server answer the next cmd with ReauthenticationRequired (391).
      func failNext(ctx context.Context, client *mongo.Client, cmd string) {
      	err := client.Database("admin").RunCommand(ctx, bson.D{
      		{Key: "configureFailPoint", Value: "failCommand"},
      		{Key: "mode", Value: bson.D{{Key: "times", Value: 1}}},
      		{Key: "data", Value: bson.D{
      			{Key: "failCommands", Value: bson.A{cmd}},
      			{Key: "errorCode", Value: 391},
      		}},
      	}).Err()
      	if err != nil {
      		log.Fatal(err)
      	}
      }
      

            Assignee:
            Preston Vasquez
            Reporter:
            Stefan Martinov (EXT)
            None
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: