2

I’ve built a web application that uses the Google Drive API across multiple pages. On each page, I need to make authenticated API calls to Google Drive. When I log in on a page, I obtain an access token via Google login and my API requests work correctly. However, refreshing or navigating to a different page forces a re-login every time.

To solve this, I implemented an automatic Google login on the main page and store the access token in Redis. The idea is to reuse the same token across pages so that users don’t have to log in again. The problem is: when I use the token stored in Redis for my API requests, I receive a 401 error.

Even though the token I get from logging in on each page exactly matches the one stored in Redis, API calls fail with the following error:

error:
  code: 401
  errors: [{…}]
  message: "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity-sign-in/web/devconsole-project."
  status: "UNAUTHENTICATED"

Token Storage (on page load):

$(document).ready(async function() {
    if ("{{.DriveCookie}}" === "") {
        iziToast.info({
            title: 'Warning!',
            message: 'Drive connection not established. Connecting...',
            position: 'topCenter'
        });
        const accessToken = await getAccessToken();

        // Post the access token to store it in Redis
        const formData = new FormData();
        formData.append("driveCookie", accessToken);
        formData.append("slug", "[USER_SLUG]"); // Censored

        fetch("/user/driveCookie", {
            method: "POST",
            body: formData
        })
        .then(response => response.json())
        .then(data => {
            iziToast.success({
                title: 'Success!',
                message: 'Drive connection established.',
                position: 'topCenter'
            });
        })
        .catch(error => {
            console.error("Error:", error);
            alert("An error occurred!");
        });
    }
});

Saving the access token in Redis on the backend.

app.Post("user/driveCookie", middlewares.AuthMiddleware, handlers.HandleDriveCookie)
func HandleDriveCookie(c *fiber.Ctx) error {
    driveCookie := c.FormValue("driveCookie")
    if driveCookie == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "driveCookie not found",
        })
    }
    userID := c.FormValue("userID")
    if userID == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "userID not found",
        })
    }

    // Initialize Redis service
    rdb := services.NewRedisService(database.RedisClient)

    // Store driveCookie in Redis
    err := rdb.StoreDriveCookie(context.Background(), userID, driveCookie)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Failed to store driveCookie in Redis",
        })
    }

    return c.Status(fiber.StatusOK).JSON(fiber.Map{
        "message": "Drive cookie successfully stored in Redis",
    })
}

func GetUserWithDriveCookie(c *fiber.Ctx) error {
    userID := c.Params("userID")
    if userID == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "userID not found",
        })
    }

    // Retrieve driveCookie from Redis
    rs := services.NewRedisService(database.RedisClient)
    driveCookie, err := rs.GetDriveCookie(context.Background(), userID)
    if err != nil {
        if err == redis.Nil {
            driveCookie = "" // Return empty value if cookie expired
        } else {
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Failed to retrieve driveCookie",
            })
        }
    }

    // Return userID and driveCookie as JSON
    response := struct {
        UserID      string `json:"userID"`
        DriveCookie string `json:"driveCookie"`
    }{
        UserID:      userID,
        DriveCookie: driveCookie,
    }

    return c.Status(fiber.StatusOK).JSON(response)
}
type RedisService struct {
    rdb *redis.Client
}

func NewRedisService(rdb *redis.Client) *RedisService {
    return &RedisService{rdb: rdb}
}

func (rs *RedisService) StoreDriveCookie(ctx context.Context, userID string, driveCookie string) error {
    key := "drive_cookie:" + userID
    err := rs.rdb.Set(ctx, key, driveCookie, time.Hour*8).Err() // 8-hour expiration
    if err != nil {
        return err
    }
    return nil
}

func (rs *RedisService) GetDriveCookie(ctx context.Context, userID string) (string, error) {
    key := "drive_cookie:" + userID
    driveCookie, err := rs.rdb.Get(ctx, key).Result()
    if err == redis.Nil {
        return "", nil // Cookie not found
    } else if err != nil {
        return "", err // Other error
    }
    return driveCookie, nil
}

File Upload Function (API Request):

async function loadFile() {
    const form = document.getElementById('uploadDocumentForm');
    const formData = new FormData(form);
    const fileInput = document.getElementById('documentFile');

    if (!fileInput.files.length) {
        iziToast.error({
            title: 'Error',
            message: 'Please select a file.',
            position: 'topCenter'
        });
        return;
    }

    const file = fileInput.files[0];
    let fileType = file.name.split('.').pop();
    if (fileType.includes('-')) {
        fileType = fileType.split('-')[0];
    }
    if(fileType === 'docx'){
        const accessToken = await getAccessToken();
        const accessTokenString = accessToken.toString();

        const metadata = {
            name: file.name,
            mimeType: file.type,
            parents: ['[PARENT_FOLDER_ID]'] // Censored
        };

        const formData2 = new FormData();
        formData2.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
        formData2.append("file", file);

        try {
            const response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", {
                method: "POST",
                headers: new Headers({ "Authorization": "Bearer " + accessTokenString }),
                body: formData2
            });

            const result = await response.json();
            if (response.ok) {
                formData.append('driveID', result.id);
            } else {
                console.error('Upload error:', result);
                iziToast.error({
                    title: 'Error',
                    message: 'Error uploading file.',
                    position: 'topCenter'
                });
            }
        } catch (error) {
            console.error('Error:', error);
            iziToast.error({
                title: 'Error',
                message: 'Error uploading file.',
                position: 'topCenter'
            });
            return;
        }
    }
}
async function getAccessToken() {
    return new Promise((resolve, reject) => {
        // Simplified for demonstration; returns the cached token
        resolve(cachedAccessToken);
    });
}

To check whether the access token is stored correctly in Redis, I printed both the previously logged-in access token stored in Redis and the newly obtained access token from the latest login to the console :

Previously logged-in access token stored in Redis: ya29.a0AXeO80S-***************ME8yaCgYKAfcSARASFQHGX2MiEqNw2FBDguC2VN4xZdFq0Q0175  // Censored
Access token obtained from a new login: ya29.a0AXeO80S-***************ME8yaCgYKAfcSARASFQHGX2MiEqNw2FBDguC2VN4xZdFq0Q0175  // Censored

Summary:

When I perform a Google login on each page, the access token is valid and my API calls work. I store the access token in Redis (using the above token storage code) to avoid repeated logins. However, when I use the Redis-stored token for a Drive API call (as shown in loadFile()), I receive a 401 error, even though the token from Redis is identical to the one obtained during login. Any insights into why the API request fails with the stored token—even when both tokens match—and how to resolve this issue would be greatly appreciated.

I stored the access token in Redis to avoid re-login across pages. I expected the stored token to work for Drive API requests, but instead, I received a 401 UNAUTHENTICATED error, even though the token matched the one from login.

1
  • I need to see the manipulation on the token before and after sending to redis, and the call to get and set to redis, the flow is missing
    – avifen
    Commented Feb 23 at 21:40

1 Answer 1

1

Managing Access and Refresh Tokens for Google Drive API

You mentioned that:

On each page, you need to make authenticated API calls to Google Drive. When you log in on a page, you obtain an access token via Google login, and your API requests work correctly. However, refreshing or navigating to a different page forces a re-login every time.

You are implementing an automatic Google login to solve the problem on the main page and storing the access token in Redis. The idea is to reuse the same token across pages so that users don’t have to log in again.

Upon researching your problem, I found in this documentation that access tokens have a limited lifetime.

Access tokens have limited lifetimes. If your application needs access to a Google API beyond the lifetime of a single access token, it can obtain a refresh token. A refresh token allows your application to obtain new access tokens.

By implementing refresh tokens in your OAuth 2.0 flow, you can ensure uninterrupted access to Google APIs for your application without requiring the user to authenticate every time the access token expires. But you should keep in mind the reason for the refresh token expiration.

You may also refer to this SO post: Why does Oauth v2 have both access and refresh tokens

Additionally, the following documentation might help you understand your current limitations:

You can also check out this article for a practical implementation guide:

For further understanding, refer to the official specification:

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.