Skip to main content
Multi-Cloud Manager uses Google OAuth 2.0 to authenticate users and access Google Cloud Platform resources through the Cloud Platform API.

Authentication Flow

The GCP authentication module implements the OAuth 2.0 authorization code flow with offline access:

Environment Variables

Configure these environment variables in your .env file:
# Google OAuth Configuration
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:5000/google/callback

Variable Descriptions

VariableDescriptionExample
GOOGLE_CLIENT_IDOAuth 2.0 Client ID from Google Cloud Console123456789-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRETOAuth 2.0 Client SecretGOCSPX-abc123xyz...
GOOGLE_REDIRECT_URIAuthorized redirect URI for OAuth callbackshttps://api.yourdomain.com/google/callback
Keep your GOOGLE_CLIENT_SECRET secure and never commit it to version control.

Google Cloud Console Setup

1

Create OAuth Client

Navigate to Google Cloud Console → APIs & Services → Credentials → Create Credentials → OAuth client ID
  • Application type: Web application
  • Name: Multi-Cloud Manager
  • Authorized redirect URIs: Add http://localhost:5000/google/callback (for development)
2

Download Credentials

After creating the client:
  • Copy the Client IDGOOGLE_CLIENT_ID
  • Copy the Client SecretGOOGLE_CLIENT_SECRET
3

Enable Required APIs

Enable these APIs in your Google Cloud project:
  • Cloud Resource Manager API
  • Identity and Access Management (IAM) API
4

Configure OAuth Consent Screen

Set up the OAuth consent screen:
  • User type: Internal or External
  • Add scopes: openid, email, profile, cloud-platform
  • Add test users if using external user type

Code Implementation

The GCP authentication module is located in backend/auth/gcp_auth.py:

Configuration

import os
import requests as http_requests
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests

GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
TOKEN_URI = "https://oauth2.googleapis.com/token"

OAuth Scopes

The application requests the following scopes:
scopes = [
    "openid",                                            # OpenID Connect
    "email",                                             # User email address
    "profile",                                           # User profile info
    "https://www.googleapis.com/auth/cloud-platform"   # Full GCP access
]
The cloud-platform scope grants full access to all Google Cloud resources. Consider using more granular scopes for production deployments.

API Endpoints

Login Endpoint

Route: /api/login/google Method: GET Description: Initiates the Google OAuth flow by redirecting to Google’s authorization endpoint.
@gcp_auth.route("/api/login/google")
def login_google():
    url = (
        "https://accounts.google.com/o/oauth2/v2/auth"
        f"?client_id={GOOGLE_CLIENT_ID}"
        f"&redirect_uri={GOOGLE_REDIRECT_URI}"
        "&response_type=code"
        "&scope=openid%20email%20profile%20https://www.googleapis.com/auth/cloud-platform"
        "&access_type=offline"
        "&prompt=consent%20select_account"
    )
    return redirect(url)
Query Parameters:
  • access_type=offline: Requests a refresh token for offline access
  • prompt=consent select_account: Forces account selection and consent screen
Usage:
curl -L http://localhost:5000/api/login/google

Callback Endpoint

Route: /google/callback Method: GET Description: Handles the OAuth callback, exchanges the authorization code for tokens, verifies the ID token, and stores account information.
@gcp_auth.route("/google/callback")
def google_callback():
    code = request.args.get("code")
    if not code:
        return jsonify({"error": "Brak kodu autoryzacyjnego Google w odpowiedzi"}), 401

    # Exchange authorization code for tokens
    token_res = http_requests.post(TOKEN_URI, data={
        "code": code,
        "client_id": GOOGLE_CLIENT_ID,
        "client_secret": GOOGLE_CLIENT_SECRET,
        "redirect_uri": GOOGLE_REDIRECT_URI,
        "grant_type": "authorization_code"
    })

    if token_res.status_code != 200:
        error_details = token_res.json()
        return jsonify({"error": "Błąd wymiany kodu na token", "details": error_details}), 500

    token_data = token_res.json()
    id_token_str = token_data.get("id_token")
    access_token = token_data.get("access_token")
    refresh_token = token_data.get("refresh_token")

    # Verify ID token
    try:
        idinfo = id_token.verify_oauth2_token(
            id_token_str, 
            google_requests.Request(), 
            GOOGLE_CLIENT_ID, 
            clock_skew_in_seconds=10
        )
    except Exception as e:
        return jsonify({"error": "Błąd weryfikacji tokenu ID", "details": str(e)}), 401

    # Store user and tokens in session
    session["user"] = idinfo
    session["access_token"] = access_token

    # Create or update GCP account
    new_gcp_account = {
        "provider": "gcp",
        "email": idinfo.get("email"),
        "displayName": idinfo.get("name"),
        "access_token": access_token,
        "refresh_token": refresh_token
    }

    # Update accounts in session
    accounts = session.setdefault("accounts", [])
    
    account_found = False
    for i, acc in enumerate(accounts):
        if acc.get("email") == new_gcp_account["email"] and acc.get("provider") == "gcp":
            accounts[i] = new_gcp_account
            account_found = True
            break

    if not account_found:
        accounts.append(new_gcp_account)

    session.modified = True
    return redirect("http://localhost:3000/dashboard")

Token Management

ID Token Verification

The application verifies ID tokens to ensure authenticity:
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests

try:
    idinfo = id_token.verify_oauth2_token(
        id_token_str, 
        google_requests.Request(), 
        GOOGLE_CLIENT_ID, 
        clock_skew_in_seconds=10  # Allow 10 seconds of clock skew
    )
except Exception as e:
    return jsonify({"error": "Token verification failed", "details": str(e)}), 401
ID Token Claims:
idinfo = {
    "iss": "https://accounts.google.com",
    "sub": "user-google-id",
    "email": "[email protected]",
    "email_verified": True,
    "name": "John Doe",
    "picture": "https://lh3.googleusercontent.com/...",
    "given_name": "John",
    "family_name": "Doe",
    "locale": "en"
}

Refresh Tokens

The application requests offline access and stores refresh tokens:
new_gcp_account = {
    "provider": "gcp",
    "email": idinfo.get("email"),
    "displayName": idinfo.get("name"),
    "access_token": access_token,
    "refresh_token": refresh_token  # Stored for token refresh
}
Refresh tokens are long-lived and should be stored securely. Consider encrypting them at rest in production.

Session Data Structure

After successful authentication, the session contains:
session = {
    "user": {
        "iss": "https://accounts.google.com",
        "sub": "112233445566778899",
        "email": "[email protected]",
        "email_verified": True,
        "name": "John Doe",
        "picture": "https://lh3.googleusercontent.com/..."
    },
    "access_token": "ya29.a0AfB_byC...",
    "accounts": [
        {
            "provider": "gcp",
            "email": "[email protected]",
            "displayName": "John Doe",
            "access_token": "ya29.a0AfB_byC...",
            "refresh_token": "1//0gZxyz..."
        }
    ]
}

Account Deduplication

The system prevents duplicate GCP accounts in the session by comparing email addresses:
accounts = session.setdefault("accounts", [])

account_found = False
for i, acc in enumerate(accounts):
    if acc.get("email") == new_gcp_account["email"] and acc.get("provider") == "gcp":
        # Update existing account
        accounts[i] = new_gcp_account
        account_found = True
        break

if not account_found:
    # Add new account
    accounts.append(new_gcp_account)

session.modified = True

Troubleshooting

Error: “Brak kodu autoryzacyjnego”

Cause: No authorization code in callback URL Solution:
  • Verify the redirect URI in Google Cloud Console matches GOOGLE_REDIRECT_URI
  • Check that the URI is in the authorized redirect URIs list

Error: “Błąd wymiany kodu na token”

Cause: Failed to exchange authorization code for tokens Solution:
  • Verify GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are correct
  • Ensure the authorization code hasn’t expired (they expire quickly)
  • Check that the redirect URI matches exactly

Error: “Błąd weryfikacji tokenu ID”

Cause: ID token verification failed Solution:
  • Check system clock is synchronized (clock skew)
  • Verify GOOGLE_CLIENT_ID matches the token’s audience
  • Ensure the token hasn’t expired

No Refresh Token Received

Cause: User has already granted consent Solution:

Security Best Practices

  1. Always use HTTPS in production for GOOGLE_REDIRECT_URI
  2. Validate the state parameter to prevent CSRF attacks (consider implementing)
  3. Store refresh tokens encrypted in production
  4. Use more granular scopes instead of cloud-platform where possible
  5. Implement token refresh logic before access tokens expire
  6. Set proper CORS policies for your frontend

Token Refresh Example

To refresh an expired access token using the stored refresh token:
import requests

def refresh_gcp_token(refresh_token):
    response = requests.post(
        "https://oauth2.googleapis.com/token",
        data={
            "client_id": GOOGLE_CLIENT_ID,
            "client_secret": GOOGLE_CLIENT_SECRET,
            "refresh_token": refresh_token,
            "grant_type": "refresh_token"
        }
    )
    
    if response.status_code == 200:
        token_data = response.json()
        return token_data["access_token"]
    else:
        raise Exception(f"Token refresh failed: {response.text}")

Next Steps

Build docs developers (and LLMs) love