|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# setup_google_oauth.sh |
| 4 | +# |
| 5 | +# Automates the creation of Google OAuth 2.0 credentials for py4web Google SSO. |
| 6 | +# |
| 7 | +# Prerequisites: |
| 8 | +# - gcloud CLI installed and authenticated (run: gcloud auth login) |
| 9 | +# - A Google Cloud billing account (required to enable APIs) |
| 10 | +# |
| 11 | +# Usage: |
| 12 | +# ./scripts/setup_google_oauth.sh [OPTIONS] |
| 13 | +# |
| 14 | +# Options: |
| 15 | +# --project-id ID GCP project ID (default: auto-generated) |
| 16 | +# --project-name NAME GCP project display name (default: "py4web OAuth") |
| 17 | +# --app-name NAME py4web app name (default: "_scaffold") |
| 18 | +# --host HOST App host for redirect URIs (default: "localhost:8000") |
| 19 | +# --support-email EMAIL OAuth consent screen email (default: gcloud account email) |
| 20 | +# --external Set consent screen to external (default: internal) |
| 21 | +# --scoped Also output config for OAuth2GoogleScoped |
| 22 | +# --help Show this help |
| 23 | +# |
| 24 | +set -euo pipefail |
| 25 | + |
| 26 | +# ---------- defaults ---------- |
| 27 | +PROJECT_ID="" |
| 28 | +PROJECT_NAME="py4web OAuth" |
| 29 | +APP_NAME="_scaffold" |
| 30 | +HOST="localhost:8000" |
| 31 | +SUPPORT_EMAIL="" |
| 32 | +USER_TYPE="INTERNAL" |
| 33 | +SCOPED=false |
| 34 | + |
| 35 | +# ---------- parse args ---------- |
| 36 | +while [[ $# -gt 0 ]]; do |
| 37 | + case $1 in |
| 38 | + --project-id) PROJECT_ID="$2"; shift 2 ;; |
| 39 | + --project-name) PROJECT_NAME="$2"; shift 2 ;; |
| 40 | + --app-name) APP_NAME="$2"; shift 2 ;; |
| 41 | + --host) HOST="$2"; shift 2 ;; |
| 42 | + --support-email) SUPPORT_EMAIL="$2"; shift 2 ;; |
| 43 | + --external) USER_TYPE="EXTERNAL"; shift ;; |
| 44 | + --scoped) SCOPED=true; shift ;; |
| 45 | + --help) |
| 46 | + sed -n '3,/^set /p' "$0" | head -n -1 |
| 47 | + exit 0 |
| 48 | + ;; |
| 49 | + *) echo "Unknown option: $1"; exit 1 ;; |
| 50 | + esac |
| 51 | +done |
| 52 | + |
| 53 | +# ---------- helpers ---------- |
| 54 | +info() { echo -e "\033[1;34m==>\033[0m $*"; } |
| 55 | +ok() { echo -e "\033[1;32m==>\033[0m $*"; } |
| 56 | +warn() { echo -e "\033[1;33m==>\033[0m $*"; } |
| 57 | +fail() { echo -e "\033[1;31mERROR:\033[0m $*" >&2; exit 1; } |
| 58 | + |
| 59 | +require_cmd() { |
| 60 | + command -v "$1" >/dev/null 2>&1 || fail "'$1' is required but not installed." |
| 61 | +} |
| 62 | + |
| 63 | +gcp_api() { |
| 64 | + # $1 = method, $2 = url, $3 = optional JSON body |
| 65 | + local method="$1" url="$2" body="${3:-}" |
| 66 | + local token |
| 67 | + token=$(gcloud auth print-access-token 2>/dev/null) || fail "Not authenticated. Run: gcloud auth login" |
| 68 | + if [[ -n "$body" ]]; then |
| 69 | + curl -sf -X "$method" "$url" \ |
| 70 | + -H "Authorization: Bearer $token" \ |
| 71 | + -H "Content-Type: application/json" \ |
| 72 | + -d "$body" |
| 73 | + else |
| 74 | + curl -sf -X "$method" "$url" \ |
| 75 | + -H "Authorization: Bearer $token" |
| 76 | + fi |
| 77 | +} |
| 78 | + |
| 79 | +# ---------- preflight ---------- |
| 80 | +require_cmd gcloud |
| 81 | +require_cmd curl |
| 82 | +require_cmd jq |
| 83 | + |
| 84 | +# Verify gcloud is authenticated |
| 85 | +info "Checking gcloud authentication..." |
| 86 | +ACCOUNT=$(gcloud config get-value account 2>/dev/null) || true |
| 87 | +if [[ -z "$ACCOUNT" || "$ACCOUNT" == "(unset)" ]]; then |
| 88 | + fail "Not authenticated. Run: gcloud auth login" |
| 89 | +fi |
| 90 | +ok "Authenticated as $ACCOUNT" |
| 91 | + |
| 92 | +if [[ -z "$SUPPORT_EMAIL" ]]; then |
| 93 | + SUPPORT_EMAIL="$ACCOUNT" |
| 94 | +fi |
| 95 | + |
| 96 | +# ---------- step 1: create or select project ---------- |
| 97 | +if [[ -z "$PROJECT_ID" ]]; then |
| 98 | + PROJECT_ID="py4web-oauth-$(date +%s | tail -c 7)" |
| 99 | +fi |
| 100 | + |
| 101 | +info "Checking if project '$PROJECT_ID' exists..." |
| 102 | +if gcloud projects describe "$PROJECT_ID" &>/dev/null; then |
| 103 | + ok "Project '$PROJECT_ID' already exists, using it." |
| 104 | +else |
| 105 | + info "Creating project '$PROJECT_ID'..." |
| 106 | + gcloud projects create "$PROJECT_ID" --name="$PROJECT_NAME" --quiet |
| 107 | + ok "Project created." |
| 108 | +fi |
| 109 | + |
| 110 | +gcloud config set project "$PROJECT_ID" --quiet |
| 111 | + |
| 112 | +# Get project number (needed for REST APIs) |
| 113 | +PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)') |
| 114 | +info "Project number: $PROJECT_NUMBER" |
| 115 | + |
| 116 | +# ---------- step 2: enable required APIs ---------- |
| 117 | +info "Enabling required APIs (this may take a moment)..." |
| 118 | +gcloud services enable \ |
| 119 | + iap.googleapis.com \ |
| 120 | + people.googleapis.com \ |
| 121 | + oauth2.googleapis.com \ |
| 122 | + --quiet 2>/dev/null || { |
| 123 | + warn "Some APIs may require billing. Checking..." |
| 124 | + # Try enabling one at a time for clearer errors |
| 125 | + for api in iap.googleapis.com people.googleapis.com; do |
| 126 | + if ! gcloud services enable "$api" --quiet 2>/dev/null; then |
| 127 | + warn "Could not enable $api — billing may be required." |
| 128 | + echo " Enable billing: https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID" |
| 129 | + echo " Then re-run this script with: --project-id $PROJECT_ID" |
| 130 | + fail "Billing required to enable APIs." |
| 131 | + fi |
| 132 | + done |
| 133 | +} |
| 134 | +ok "APIs enabled." |
| 135 | + |
| 136 | +# ---------- step 3: create OAuth consent screen (brand) ---------- |
| 137 | +info "Configuring OAuth consent screen..." |
| 138 | + |
| 139 | +# Check if brand already exists |
| 140 | +EXISTING_BRAND=$(gcp_api GET \ |
| 141 | + "https://iap.googleapis.com/v1/projects/$PROJECT_NUMBER/brands" \ |
| 142 | + | jq -r '.brands[0].name // empty' 2>/dev/null) || true |
| 143 | + |
| 144 | +if [[ -n "$EXISTING_BRAND" ]]; then |
| 145 | + ok "OAuth consent screen already configured." |
| 146 | + BRAND_NAME="$EXISTING_BRAND" |
| 147 | +else |
| 148 | + BRAND_BODY=$(jq -n \ |
| 149 | + --arg title "$PROJECT_NAME" \ |
| 150 | + --arg email "$SUPPORT_EMAIL" \ |
| 151 | + '{applicationTitle: $title, supportEmail: $email}') |
| 152 | + |
| 153 | + BRAND_RESP=$(gcp_api POST \ |
| 154 | + "https://iap.googleapis.com/v1/projects/$PROJECT_NUMBER/brands" \ |
| 155 | + "$BRAND_BODY") |
| 156 | + |
| 157 | + BRAND_NAME=$(echo "$BRAND_RESP" | jq -r '.name // empty') |
| 158 | + if [[ -z "$BRAND_NAME" ]]; then |
| 159 | + warn "Could not create brand via API. Response:" |
| 160 | + echo "$BRAND_RESP" |
| 161 | + fail "OAuth consent screen creation failed." |
| 162 | + fi |
| 163 | + ok "OAuth consent screen created." |
| 164 | +fi |
| 165 | + |
| 166 | +# ---------- step 4: create OAuth client ---------- |
| 167 | +info "Creating OAuth 2.0 client credentials..." |
| 168 | + |
| 169 | +CLIENT_BODY=$(jq -n --arg name "py4web SSO Client" '{displayName: $name}') |
| 170 | + |
| 171 | +CLIENT_RESP=$(gcp_api POST \ |
| 172 | + "https://iap.googleapis.com/v1/${BRAND_NAME}/identityAwareProxyClients" \ |
| 173 | + "$CLIENT_BODY") |
| 174 | + |
| 175 | +CLIENT_ID=$(echo "$CLIENT_RESP" | jq -r '.name // empty' | awk -F/ '{print $NF}') |
| 176 | +CLIENT_SECRET=$(echo "$CLIENT_RESP" | jq -r '.secret // empty') |
| 177 | + |
| 178 | +if [[ -z "$CLIENT_ID" || -z "$CLIENT_SECRET" ]]; then |
| 179 | + warn "IAP client creation response:" |
| 180 | + echo "$CLIENT_RESP" |
| 181 | + fail "Could not create OAuth client credentials." |
| 182 | +fi |
| 183 | + |
| 184 | +ok "OAuth client created." |
| 185 | + |
| 186 | +# ---------- step 5: configure redirect URIs ---------- |
| 187 | +# The IAP API creates clients but redirect URIs must be set via the Cloud Console |
| 188 | +# or the OAuth2 config API. We'll try the newer endpoint. |
| 189 | + |
| 190 | +# Determine the redirect URI |
| 191 | +if [[ "$HOST" == localhost* || "$HOST" == 127.0.0.1* ]]; then |
| 192 | + SCHEME="http" |
| 193 | +else |
| 194 | + SCHEME="https" |
| 195 | +fi |
| 196 | +REDIRECT_URI="${SCHEME}://${HOST}/${APP_NAME}/auth/plugin/oauth2google/callback" |
| 197 | +SCOPED_REDIRECT_URI="${SCHEME}://${HOST}/${APP_NAME}/auth/plugin/oauth2googlescoped/callback" |
| 198 | + |
| 199 | +info "Configuring redirect URIs..." |
| 200 | + |
| 201 | +# Try to set redirect URIs via the Cloud Console REST API |
| 202 | +OAUTH_CLIENT_FULL_ID="projects/$PROJECT_NUMBER/brands/$PROJECT_NUMBER/identityAwareProxyClients/$CLIENT_ID" |
| 203 | + |
| 204 | +# The redirect URI configuration often requires manual steps. |
| 205 | +# We'll attempt to use the newer Google Auth Platform API if available. |
| 206 | +REDIRECT_SET=false |
| 207 | + |
| 208 | +# Try the googleapis.com credentials API |
| 209 | +CRED_BODY=$(jq -n \ |
| 210 | + --arg redirect "$REDIRECT_URI" \ |
| 211 | + --arg scoped_redirect "$SCOPED_REDIRECT_URI" \ |
| 212 | + --arg origin "${SCHEME}://${HOST}" \ |
| 213 | + '{ |
| 214 | + web: { |
| 215 | + redirect_uris: [$redirect, $scoped_redirect], |
| 216 | + javascript_origins: [$origin] |
| 217 | + } |
| 218 | + }') |
| 219 | + |
| 220 | +# This endpoint may not be publicly available; we try and fall back gracefully |
| 221 | +if gcp_api PUT \ |
| 222 | + "https://oauth2.googleapis.com/v1/projects/$PROJECT_NUMBER/oauthClients/$CLIENT_ID" \ |
| 223 | + "$CRED_BODY" &>/dev/null; then |
| 224 | + REDIRECT_SET=true |
| 225 | + ok "Redirect URIs configured automatically." |
| 226 | +fi |
| 227 | + |
| 228 | +# ---------- step 6: output results ---------- |
| 229 | +echo "" |
| 230 | +echo "==============================================" |
| 231 | +echo " Google OAuth Credentials for py4web" |
| 232 | +echo "==============================================" |
| 233 | +echo "" |
| 234 | +echo " Client ID: $CLIENT_ID" |
| 235 | +echo " Client Secret: $CLIENT_SECRET" |
| 236 | +echo "" |
| 237 | +echo " Redirect URI: $REDIRECT_URI" |
| 238 | +if $SCOPED; then |
| 239 | + echo " Scoped Redirect: $SCOPED_REDIRECT_URI" |
| 240 | +fi |
| 241 | +echo "" |
| 242 | + |
| 243 | +if ! $REDIRECT_SET; then |
| 244 | + warn "Redirect URIs must be configured manually:" |
| 245 | + echo " 1. Go to: https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID" |
| 246 | + echo " 2. Click on the OAuth 2.0 Client ID just created" |
| 247 | + echo " 3. Under 'Authorized redirect URIs', add:" |
| 248 | + echo " $REDIRECT_URI" |
| 249 | + if $SCOPED; then |
| 250 | + echo " $SCOPED_REDIRECT_URI" |
| 251 | + fi |
| 252 | + echo " 4. Under 'Authorized JavaScript origins', add:" |
| 253 | + echo " ${SCHEME}://${HOST}" |
| 254 | + echo " 5. Click Save" |
| 255 | + echo "" |
| 256 | +fi |
| 257 | + |
| 258 | +if [[ "$USER_TYPE" == "EXTERNAL" ]]; then |
| 259 | + warn "You selected EXTERNAL user type." |
| 260 | + echo " Add test users at: https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID" |
| 261 | + echo " Until the app is verified, only test users can log in." |
| 262 | + echo "" |
| 263 | +fi |
| 264 | + |
| 265 | +# ---------- step 7: write py4web settings ---------- |
| 266 | +echo "----------------------------------------------" |
| 267 | +echo " py4web Configuration" |
| 268 | +echo "----------------------------------------------" |
| 269 | +echo "" |
| 270 | +echo " Add to apps/${APP_NAME}/settings.py:" |
| 271 | +echo "" |
| 272 | +echo " OAUTH2GOOGLE_CLIENT_ID = \"$CLIENT_ID\"" |
| 273 | +echo " OAUTH2GOOGLE_CLIENT_SECRET = \"$CLIENT_SECRET\"" |
| 274 | +echo "" |
| 275 | + |
| 276 | +if $SCOPED; then |
| 277 | + # Write the scoped credentials JSON file |
| 278 | + SECRETS_FILE="apps/${APP_NAME}/private/google_client_secrets.json" |
| 279 | + SECRETS_DIR="$(dirname "$SECRETS_FILE")" |
| 280 | + |
| 281 | + echo " For OAuth2GoogleScoped, a secrets file will be written to:" |
| 282 | + echo " $SECRETS_FILE" |
| 283 | + echo "" |
| 284 | + |
| 285 | + mkdir -p "$SECRETS_DIR" |
| 286 | + |
| 287 | + jq -n \ |
| 288 | + --arg client_id "$CLIENT_ID" \ |
| 289 | + --arg client_secret "$CLIENT_SECRET" \ |
| 290 | + --arg redirect "$SCOPED_REDIRECT_URI" \ |
| 291 | + --arg project_id "$PROJECT_ID" \ |
| 292 | + '{ |
| 293 | + web: { |
| 294 | + client_id: $client_id, |
| 295 | + client_secret: $client_secret, |
| 296 | + project_id: $project_id, |
| 297 | + auth_uri: "https://accounts.google.com/o/oauth2/auth", |
| 298 | + token_uri: "https://oauth2.googleapis.com/token", |
| 299 | + auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", |
| 300 | + redirect_uris: [$redirect] |
| 301 | + } |
| 302 | + }' > "$SECRETS_FILE" |
| 303 | + |
| 304 | + echo " Add to apps/${APP_NAME}/settings.py:" |
| 305 | + echo "" |
| 306 | + echo " OAUTH2GOOGLE_SCOPED_CREDENTIALS_FILE = \"private/google_client_secrets.json\"" |
| 307 | + echo "" |
| 308 | +fi |
| 309 | + |
| 310 | +echo "----------------------------------------------" |
| 311 | +echo "" |
| 312 | +ok "Done! Your Google OAuth credentials are ready." |
| 313 | +echo "" |
| 314 | +echo " Console: https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID" |
| 315 | +echo "" |
0 commit comments