33# setup_google_oauth.sh
44#
55# Automates the creation of Google OAuth 2.0 credentials for py4web Google SSO.
6+ # Works for both organization and personal Google accounts.
67#
78# Prerequisites:
89# - gcloud CLI installed and authenticated (run: gcloud auth login)
9- # - A Google Cloud billing account (required to enable APIs)
10+ # - A Google Cloud billing account (may be required to enable APIs)
1011#
1112# Usage:
1213# ./scripts/setup_google_oauth.sh [OPTIONS]
@@ -66,16 +67,27 @@ gcp_api() {
6667 local token
6768 token=$( gcloud auth print-access-token 2> /dev/null) || fail " Not authenticated. Run: gcloud auth login"
6869 if [[ -n " $body " ]]; then
69- curl -sf -X " $method " " $url " \
70+ curl -s --connect-timeout 10 --max-time 30 -X " $method " " $url " \
7071 -H " Authorization: Bearer $token " \
7172 -H " Content-Type: application/json" \
7273 -d " $body "
7374 else
74- curl -sf -X " $method " " $url " \
75+ curl -s --connect-timeout 10 --max-time 30 -X " $method " " $url " \
7576 -H " Authorization: Bearer $token "
7677 fi
7778}
7879
80+ open_url () {
81+ local url=" $1 "
82+ if command -v xdg-open & > /dev/null; then
83+ xdg-open " $url " 2> /dev/null || true
84+ elif command -v open & > /dev/null; then
85+ open " $url " 2> /dev/null || true
86+ else
87+ echo " Open in your browser: $url "
88+ fi
89+ }
90+
7991# ---------- preflight ----------
8092require_cmd gcloud
8193require_cmd curl
@@ -113,125 +125,182 @@ gcloud config set project "$PROJECT_ID" --quiet
113125PROJECT_NUMBER=$( gcloud projects describe " $PROJECT_ID " --format=' value(projectNumber)' )
114126info " Project number: $PROJECT_NUMBER "
115127
128+ # Check if project belongs to an organization
129+ ORG_ID=$( gcloud projects describe " $PROJECT_ID " --format=' value(parent.id)' 2> /dev/null) || true
130+ PARENT_TYPE=$( gcloud projects describe " $PROJECT_ID " --format=' value(parent.type)' 2> /dev/null) || true
131+ HAS_ORG=false
132+ if [[ " $PARENT_TYPE " == " organization" && -n " $ORG_ID " ]]; then
133+ HAS_ORG=true
134+ info " Project belongs to organization: $ORG_ID "
135+ else
136+ info " Project is a personal project (no organization)."
137+ fi
138+
116139# ---------- step 2: enable required APIs ----------
117140info " 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."
135141
136- # ---------- step 3: create OAuth consent screen (brand) ----------
137- info " Configuring OAuth consent screen..."
142+ REQUIRED_APIS=(people.googleapis.com)
143+ if $HAS_ORG ; then
144+ REQUIRED_APIS+=(iap.googleapis.com)
145+ fi
138146
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
147+ for api in " ${REQUIRED_APIS[@]} " ; do
148+ if ! gcloud services enable " $api " --quiet 2> /dev/null; then
149+ warn " Could not enable $api — billing may be required."
150+ echo " Enable billing: https://console.cloud.google.com/billing/linkedaccount?project=$PROJECT_ID "
151+ echo " Then re-run this script with: --project-id $PROJECT_ID "
152+ fail " Billing required to enable APIs."
153+ fi
154+ done
155+ ok " APIs enabled."
143156
144- if [[ -n " $EXISTING_BRAND " ]] ; then
145- ok " OAuth consent screen already configured. "
146- BRAND_NAME= " $EXISTING_BRAND "
157+ # ---------- compute redirect URIs ----------
158+ if [[ " $HOST " == localhost * || " $HOST " == 127.0.0.1 * ]] ; then
159+ SCHEME= " http "
147160else
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."
161+ SCHEME=" https"
164162fi
163+ REDIRECT_URI=" ${SCHEME} ://${HOST} /${APP_NAME} /auth/plugin/oauth2google/callback"
164+ SCOPED_REDIRECT_URI=" ${SCHEME} ://${HOST} /${APP_NAME} /auth/plugin/oauth2googlescoped/callback"
165165
166- # ---------- step 4: create OAuth client ----------
167- info " Creating OAuth 2.0 client credentials..."
166+ # ==========================================================================
167+ # Two paths: organization projects use the IAP API (fully automated),
168+ # personal projects use interactive browser-assisted flow.
169+ # ==========================================================================
168170
169- CLIENT_BODY=$( jq -n --arg name " py4web SSO Client" ' {displayName: $name}' )
171+ if $HAS_ORG ; then
172+ # ---- ORGANIZATION PATH: fully automated via IAP API ----
170173
171- CLIENT_RESP=$( gcp_api POST \
172- " https://iap.googleapis.com/v1/${BRAND_NAME} /identityAwareProxyClients" \
173- " $CLIENT_BODY " )
174+ # step 3: create OAuth consent screen (brand)
175+ info " Configuring OAuth consent screen via IAP API..."
174176
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+ BRAND_LIST_RESP= $( gcp_api GET \
178+ " https://iap.googleapis.com/v1/projects/ $PROJECT_NUMBER /brands " ) || true
177179
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
180+ if [[ -z " $BRAND_LIST_RESP " ]]; then
181+ fail " Cannot query OAuth brands. Check that iap.googleapis.com is enabled and you have Owner/Editor role."
182+ fi
183183
184- ok " OAuth client created. "
184+ EXISTING_BRAND= $( echo " $BRAND_LIST_RESP " | jq -r ' .brands[0].name // empty ' 2> /dev/null ) || true
185185
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.
186+ if [[ -n " $EXISTING_BRAND " ]]; then
187+ ok " OAuth consent screen already configured."
188+ BRAND_NAME=" $EXISTING_BRAND "
189+ else
190+ BRAND_BODY=$( jq -n \
191+ --arg title " $PROJECT_NAME " \
192+ --arg email " $SUPPORT_EMAIL " \
193+ ' {applicationTitle: $title, supportEmail: $email}' )
194+
195+ BRAND_RESP=$( gcp_api POST \
196+ " https://iap.googleapis.com/v1/projects/$PROJECT_NUMBER /brands" \
197+ " $BRAND_BODY " )
198+
199+ if [[ -z " $BRAND_RESP " ]]; then
200+ fail " OAuth consent screen creation failed (empty response)."
201+ fi
202+
203+ BRAND_NAME=$( echo " $BRAND_RESP " | jq -r ' .name // empty' )
204+ if [[ -z " $BRAND_NAME " ]]; then
205+ warn " Response: $BRAND_RESP "
206+ fail " OAuth consent screen creation failed."
207+ fi
208+ ok " OAuth consent screen created."
209+ fi
210+
211+ # step 4: create OAuth client
212+ info " Creating OAuth 2.0 client credentials..."
213+
214+ CLIENT_BODY=$( jq -n --arg name " py4web SSO Client" ' {displayName: $name}' )
215+
216+ CLIENT_RESP=$( gcp_api POST \
217+ " https://iap.googleapis.com/v1/${BRAND_NAME} /identityAwareProxyClients" \
218+ " $CLIENT_BODY " )
219+
220+ CLIENT_ID=$( echo " $CLIENT_RESP " | jq -r ' .name // empty' | awk -F/ ' {print $NF}' )
221+ CLIENT_SECRET=$( echo " $CLIENT_RESP " | jq -r ' .secret // empty' )
222+
223+ if [[ -z " $CLIENT_ID " || -z " $CLIENT_SECRET " ]]; then
224+ warn " Response: $CLIENT_RESP "
225+ fail " Could not create OAuth client credentials."
226+ fi
227+
228+ ok " OAuth client created via IAP API."
229+
230+ # Try to set redirect URIs (may not work via public API)
231+ REDIRECT_SET=false
232+ info " Attempting to configure redirect URIs..."
233+ # The IAP-created clients need redirect URIs set via Console
234+ warn " Redirect URIs must be configured in the Cloud Console (see instructions below)."
189235
190- # Determine the redirect URI
191- if [[ " $HOST " == localhost* || " $HOST " == 127.0.0.1* ]]; then
192- SCHEME=" http"
193236else
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"
237+ # ---- PERSONAL ACCOUNT PATH: browser-assisted ----
238+
239+ info " Personal account detected. Using browser-assisted setup."
240+ echo " "
241+
242+ # step 3: consent screen — must be done in browser
243+ CONSENT_URL=" https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID "
244+ echo " Step 1: Configure the OAuth consent screen"
245+ echo " "
246+ echo " Opening the OAuth consent screen configuration page..."
247+ echo " If it doesn't open automatically, visit:"
248+ echo " $CONSENT_URL "
249+ echo " "
250+ echo " In the consent screen form:"
251+ echo " - User Type: External (select and click Create)"
252+ echo " - App name: $PROJECT_NAME "
253+ echo " - User support email: $SUPPORT_EMAIL "
254+ echo " - Developer contact email: $SUPPORT_EMAIL "
255+ echo " - Click 'Save and Continue' through Scopes and Test Users"
256+ echo " "
257+ open_url " $CONSENT_URL "
258+
259+ read -rp " Press Enter once you've saved the consent screen... "
260+ echo " "
261+
262+ # step 4: create credentials — must be done in browser
263+ # Pre-fill as much as possible via URL parameters
264+ CRED_URL=" https://console.cloud.google.com/apis/credentials/oauthclient?project=$PROJECT_ID "
265+ echo " Step 2: Create OAuth 2.0 Client ID"
266+ echo " "
267+ echo " Opening the credential creation page..."
268+ echo " If it doesn't open automatically, visit:"
269+ echo " $CRED_URL "
270+ echo " "
271+ echo " In the form:"
272+ echo " - Application type: Web application"
273+ echo " - Name: py4web SSO Client"
274+ echo " - Authorized JavaScript origins: ${SCHEME} ://${HOST} "
275+ echo " - Authorized redirect URIs:"
276+ echo " $REDIRECT_URI "
277+ if $SCOPED ; then
278+ echo " $SCOPED_REDIRECT_URI "
279+ fi
280+ echo " - Click Create"
281+ echo " "
282+ echo " After creation, Google will show the Client ID and Secret."
283+ echo " "
284+ open_url " $CRED_URL "
285+
286+ read -rp " Paste Client ID: " CLIENT_ID
287+ read -rp " Paste Client Secret: " CLIENT_SECRET
288+
289+ if [[ -z " $CLIENT_ID " || -z " $CLIENT_SECRET " ]]; then
290+ fail " Client ID and Secret are required."
291+ fi
198292
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."
293+ ok " Credentials received."
294+ REDIRECT_SET=true # User configured redirect URIs manually
226295fi
227296
228- # ---------- step 6: output results ----------
297+ # ---------- output results ----------
229298echo " "
230299echo " =============================================="
231300echo " Google OAuth Credentials for py4web"
232301echo " =============================================="
233302echo " "
234- echo " Client ID: $CLIENT_ID "
303+ echo " Client ID: $CLIENT_ID "
235304echo " Client Secret: $CLIENT_SECRET "
236305echo " "
237306echo " Redirect URI: $REDIRECT_URI "
@@ -240,7 +309,7 @@ if $SCOPED; then
240309fi
241310echo " "
242311
243- if ! $ REDIRECT_SET; then
312+ if [[ " ${ REDIRECT_SET:- false} " != " true " ]] ; then
244313 warn " Redirect URIs must be configured manually:"
245314 echo " 1. Go to: https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID "
246315 echo " 2. Click on the OAuth 2.0 Client ID just created"
@@ -256,13 +325,12 @@ if ! $REDIRECT_SET; then
256325fi
257326
258327if [[ " $USER_TYPE " == " EXTERNAL" ]]; then
259- warn " You selected EXTERNAL user type ."
328+ warn " External consent screen: until the app is verified, only test users can log in ."
260329 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."
262330 echo " "
263331fi
264332
265- # ---------- step 7: write py4web settings ----------
333+ # ---------- write py4web settings ----------
266334echo " ----------------------------------------------"
267335echo " py4web Configuration"
268336echo " ----------------------------------------------"
@@ -274,7 +342,6 @@ echo " OAUTH2GOOGLE_CLIENT_SECRET = \"$CLIENT_SECRET\""
274342echo " "
275343
276344if $SCOPED ; then
277- # Write the scoped credentials JSON file
278345 SECRETS_FILE=" apps/${APP_NAME} /private/google_client_secrets.json"
279346 SECRETS_DIR=" $( dirname " $SECRETS_FILE " ) "
280347
@@ -301,7 +368,7 @@ if $SCOPED; then
301368 }
302369 }' > " $SECRETS_FILE "
303370
304- echo " Add to apps/${APP_NAME} /settings.py:"
371+ echo " Also add to apps/${APP_NAME} /settings.py:"
305372 echo " "
306373 echo " OAUTH2GOOGLE_SCOPED_CREDENTIALS_FILE = \" private/google_client_secrets.json\" "
307374 echo " "
0 commit comments