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
| Variable | Description | Example |
|---|
GOOGLE_CLIENT_ID | OAuth 2.0 Client ID from Google Cloud Console | 123456789-abc.apps.googleusercontent.com |
GOOGLE_CLIENT_SECRET | OAuth 2.0 Client Secret | GOCSPX-abc123xyz... |
GOOGLE_REDIRECT_URI | Authorized redirect URI for OAuth callbacks | https://api.yourdomain.com/google/callback |
Keep your GOOGLE_CLIENT_SECRET secure and never commit it to version control.
Google Cloud Console Setup
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)
Download Credentials
After creating the client:
- Copy the Client ID →
GOOGLE_CLIENT_ID
- Copy the Client Secret →
GOOGLE_CLIENT_SECRET
Enable Required APIs
Enable these APIs in your Google Cloud project:
- Cloud Resource Manager API
- Identity and Access Management (IAM) API
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
- Always use HTTPS in production for
GOOGLE_REDIRECT_URI
- Validate the
state parameter to prevent CSRF attacks (consider implementing)
- Store refresh tokens encrypted in production
- Use more granular scopes instead of
cloud-platform where possible
- Implement token refresh logic before access tokens expire
- 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