OAuth 400 Error When Using Google Drive File Picker in Streamlit Application with ngrok HTTPS Tunnel

I’m building a Python Streamlit application and trying to add Google Drive file selection functionality using the Drive Picker API. Since I’m more comfortable with Python than JavaScript, I’m struggling with the OAuth integration part.

I set up OAuth credentials in Google Cloud Console and I’m using ngrok to create an HTTPS tunnel for my local development server. However, I keep getting this OAuth error:

Error 400: invalid_request - You can’t sign in to this app because it doesn’t comply with Google’s OAuth 2.0 policy for keeping apps secure.

Here’s what I’ve already configured:

  • OAuth 2.0 Client ID for web application
  • Added ngrok HTTPS URL to authorized JavaScript origins
  • Set proper redirect URIs in the OAuth settings
  • Enabled Google Drive API in the project
  • Used Google Identity Services library for authentication

I’ve tried various approaches with the JavaScript picker implementation but the OAuth flow keeps failing. The main challenge is debugging this JavaScript-heavy integration within a Python-based Streamlit environment.

Has anyone successfully implemented Google Drive file picker in Streamlit using ngrok for local HTTPS? What could be causing this OAuth policy violation error?

file_selector_code = f"""
<html>
<head>
    <script type="text/javascript">
      const OAUTH_CLIENT_ID = "{oauth_id}";
      const DRIVE_API_KEY = "{drive_key}";
      const PROJECT_ID = "{project_id}";
      const PERMISSION_SCOPE = 'https://www.googleapis.com/auth/drive.file';

      let userToken = null;
      let oauthClient;

      function loadGoogleAPI() {{
        gapi.load('client:picker', setupDrivePicker);
      }}

      async function setupDrivePicker() {{
        await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest');
        document.getElementById('login_btn').style.display = 'inline-block';
      }}

      function initializeOAuth() {{
        oauthClient = google.accounts.oauth2.initTokenClient({{
          client_id: OAUTH_CLIENT_ID,
          scope: PERMISSION_SCOPE,
          callback: (response) => {{
            if (response.error) {{
              console.error("OAuth failed", response.error);
              return;
            }}
            userToken = response.access_token;
            showFilePicker();
          }}
        }});
      }}

      function startAuthentication() {{
        if (!userToken) {{
          oauthClient.requestAccessToken({{ prompt: 'select_account' }});
        }} else {{
          showFilePicker();
        }}
      }}

      function showFilePicker() {{
        const fileView = new google.picker.View(google.picker.ViewId.FOLDERS);
        const pickerInstance = new google.picker.PickerBuilder()
          .enableFeature(google.picker.Feature.SIMPLE_UPLOAD_ENABLED)
          .setAppId(PROJECT_ID)
          .setOAuthToken(userToken)
          .setDeveloperKey(DRIVE_API_KEY)
          .addView(fileView)
          .setCallback(handleFileSelection)
          .build();

        pickerInstance.setVisible(true);
      }}

      function handleFileSelection(result) {{
        if (result.action === google.picker.Action.PICKED) {{
          const selectedFile = result.docs[0];
          document.getElementById("result").innerHTML = 
            "Selected: " + selectedFile.name + "<br>File ID: " + selectedFile.id;
        }}
      }}

      window.addEventListener('load', function() {{
        loadGoogleAPI();
        initializeOAuth();
      }});
    </script>
</head>
<body>
    <button id="login_btn" onclick="startAuthentication()" style="display:none; padding:10px 20px; background:#1a73e8; color:white; border:none; border-radius:4px;">Select Drive Files</button>
    <div id="result" style="margin-top:15px;"></div>
</body>
</html>
"""

The Problem: You’re building a Streamlit app to select files from Google Drive using the Drive Picker API. You’ve configured OAuth credentials, enabled the Drive API, and are using ngrok for local HTTPS development. However, you’re encountering the error “Error 400: invalid_request - You can’t sign in to this app because it doesn’t comply with Google’s OAuth 2.0 policy for keeping apps secure.” This usually happens because Google’s OAuth system doesn’t trust the dynamic URLs generated by ngrok’s free tier.

:thinking: Understanding the “Why” (The Root Cause):

Google’s OAuth 2.0 security policies are stringent. They prioritize verifying the identity and stability of the application requesting access to user data. Ngrok’s free tier provides temporary, randomly generated subdomains. This unpredictability makes it difficult for Google to reliably verify your application’s identity, leading to the invalid_request error. The solution is to use a stable, consistent domain that Google can verify.

:gear: Step-by-Step Guide:

  1. Migrate to a Stable Domain: The core solution is to replace ngrok’s free, dynamic subdomain with a static domain. This could involve upgrading to a paid ngrok plan (providing a custom subdomain) or using an alternative solution like localtunnel (allowing custom subdomains) that offers greater reliability. Register this fixed subdomain in your Google Cloud Console project’s OAuth 2.0 Client ID settings. Make sure the redirect URI in Google Cloud Console accurately reflects your new, stable domain.

  2. Update your ngrok Configuration (If Applicable): If using ngrok, follow the ngrok documentation to set up a custom domain. Remember to update your redirect URIs accordingly.

  3. Verify Google Cloud Console Settings: Double-check the following in your Google Cloud Console project:

    • Authorized JavaScript origins: Ensure your stable domain is listed here.
    • Authorized redirect URIs: Verify that your stable domain with the appropriate path is correctly listed. The URI should match the redirect_uri parameter you are using in your JavaScript OAuth initialization.
    • OAuth Consent Screen: Make sure the consent screen is properly configured and published.
  4. Test with your Stable Domain: After configuring your new domain and updating your Google Cloud Console settings, restart your Streamlit app. Use the new stable URL in your Streamlit application (where you’re embedding your HTML code).

  5. Inspect the Console: If the problem persists, open your browser’s developer console (usually by pressing F12). Look for additional error messages that may provide more clues.

:mag: Common Pitfalls & What to Check Next:

  • Incorrect Redirect URIs: This is a very frequent mistake. Ensure the redirect_uri you’ve set in your Google Cloud Console and in your JavaScript code precisely matches. Even a slight difference will cause the invalid_request error.
  • API Key vs. Client ID: Don’t confuse your API Key with your OAuth 2.0 Client ID. The OAuth flow uses the Client ID. The API Key is separate and might be used for other Google APIs, not authentication.
  • Cache Invalidation: Clear your browser’s cache and cookies after making changes to your settings. Browsers might be holding on to old authentication information.
  • Firewall Restrictions: Verify that your firewall isn’t blocking connections to Google’s OAuth servers or the Google Drive API.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

Google’s gotten way stricter with temporary tunnel URLs like ngrok. Your code’s probably fine - Google’s just flagging the dynamic subdomain as sketchy. I hit this exact issue building something similar. What worked for me was upgrading to ngrok’s paid plan for a custom domain with a fixed subdomain. Google’s OAuth validator doesn’t freak out since the domain stays the same every time. You could also ditch the frontend approach completely and go server-side with Python. Use google-auth-oauthlib to handle the OAuth flow - redirect users to Google’s auth page, grab the callback, then make Drive API calls from your backend. Yeah, you’ll lose the fancy picker UI, but you can build a basic file browser with the Drive API’s list endpoint. Plus you get way better error handling.

Had the same issue three months ago building a doc management feature. Your JavaScript’s fine - the problem is ngrok’s free tier creates random subdomains every restart, and Google’s OAuth hates that. I fixed it by ditching ngrok for localtunnel with a custom subdomain, then registered that exact subdomain in Google Cloud Console. The consistency stopped Google from freaking out. Or just skip the JavaScript entirely. I had better luck handling OAuth in Python with streamlit-authenticator and storing credentials in streamlit.secrets. You lose the picker’s pretty UI but get proper error handling and token management. Here’s the thing - once you’ve got valid tokens, Google Drive API works great from Python. The picker’s just eye candy, not required.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.