Skip to content

Commit c2106b6

Browse files
committed
Add webview_oauth_signin.md
1 parent 0d73d1d commit c2106b6

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

docs/webview_oauth_signin.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# WebView OAuth Sign-in
2+
3+
How to let a WebView-based app safely complete OAuth with PKCE, even though Google blocks sign-in *inside* WebViews.
4+
5+
---
6+
7+
## 1. The problem
8+
9+
Google OAuth refuses to complete inside a WebView.
10+
You’ll get errors like:
11+
12+
```
13+
403 disallowed_useragent
14+
or
15+
This browser or app may not be secure
16+
```
17+
18+
This is because embedded WebViews can intercept credentials, and Google requires that the sign-in happen in a **real browser** (like Chrome, Safari, Firefox).
19+
20+
So we must:
21+
22+
1. Start the login **from inside** the WebView app.
23+
2. Open the Google login page in the **system browser**.
24+
3. After the user finishes signing in, Google redirects to a **custom URL** (deep link or universal link).
25+
4. The app intercepts that redirect, extracts the `code` from it, and injects it back into the WebView.
26+
27+
That’s the “catch the redirect with a custom scheme or deep link” part.
28+
29+
---
30+
31+
## 2. What a deep link / custom scheme is
32+
33+
A **custom scheme** is a URL protocol that your app owns.
34+
Example:
35+
36+
```
37+
com.compassmeet:/auth
38+
```
39+
40+
or
41+
42+
```
43+
compassmeet://auth
44+
```
45+
46+
When Android (or iOS) sees a redirect to one of these URLs, it **launches your app** and passes it the URL data.
47+
48+
You register this scheme in your `AndroidManifest.xml` so Android knows which app handles it.
49+
50+
---
51+
52+
## 3. How it fits into PKCE
53+
54+
Let’s map the PKCE flow to this setup.
55+
56+
### Step 1 — Start PKCE flow inside the WebView
57+
58+
Your web code (running inside WebView) does:
59+
60+
```ts
61+
const { codeVerifier, codeChallenge } = await generatePKCE();
62+
localStorage.setItem('pkce_verifier', codeVerifier);
63+
64+
const params = new URLSearchParams({
65+
client_id: GOOGLE_CLIENT_ID,
66+
redirect_uri: 'com.compassmeet:/auth', // your deep link
67+
response_type: 'code',
68+
scope: 'openid email profile',
69+
code_challenge: codeChallenge,
70+
code_challenge_method: 'S256',
71+
});
72+
73+
window.open(`https://accounts.google.com/o/oauth2/v2/auth?${params}`, '_system');
74+
```
75+
76+
Here, `_system` (or using Capacitor Browser plugin) opens the **system browser**.
77+
78+
---
79+
80+
### Step 2 — User signs in (in the browser)
81+
82+
After login, Google redirects to your registered `redirect_uri`, e.g.:
83+
84+
```
85+
com.compassmeet:/auth?code=4/0AfJohXyZ...
86+
```
87+
88+
---
89+
90+
### Step 3 — The app intercepts that deep link
91+
92+
In your **Android app code**, you register an intent filter in `AndroidManifest.xml`:
93+
94+
```xml
95+
<intent-filter android:autoVerify="true">
96+
<action android:name="android.intent.action.VIEW" />
97+
<category android:name="android.intent.category.DEFAULT" />
98+
<category android:name="android.intent.category.BROWSABLE" />
99+
<data android:scheme="com.compassmeet" android:host="auth" />
100+
</intent-filter>
101+
```
102+
103+
Then, in your app’s main activity (Kotlin/Java), you listen for deep links:
104+
105+
```kotlin
106+
override fun onNewIntent(intent: Intent) {
107+
super.onNewIntent(intent)
108+
val data = intent.dataString
109+
if (data != null && data.startsWith("com.compassmeet:/auth")) {
110+
bridge.triggerWindowJSEvent("oauthRedirect", data)
111+
}
112+
}
113+
```
114+
115+
That line emits a custom JavaScript event inside the WebView so your web app can pick it up.
116+
117+
---
118+
119+
### Step 4 — WebView catches redirect event and exchanges the code
120+
121+
In your web app (TypeScript side):
122+
123+
```ts
124+
window.addEventListener('oauthRedirect', async (event: any) => {
125+
const url = new URL(event.detail);
126+
const code = url.searchParams.get('code');
127+
const codeVerifier = localStorage.getItem('pkce_verifier');
128+
129+
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
130+
method: 'POST',
131+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
132+
body: new URLSearchParams({
133+
client_id: GOOGLE_CLIENT_ID,
134+
code,
135+
code_verifier: codeVerifier!,
136+
redirect_uri: 'com.compassmeet:/auth',
137+
grant_type: 'authorization_code',
138+
}),
139+
});
140+
141+
const tokens = await tokenResponse.json();
142+
console.log('Tokens:', tokens);
143+
});
144+
```
145+
146+
At this point:
147+
148+
* You have your `access_token` and `id_token`.
149+
* You can sign into Firebase or use them directly.
150+
151+
---
152+
153+
## 4. Why this works and what makes it safe
154+
155+
* The login itself happens in Google’s **system browser**, not in your WebView.
156+
* The `code_verifier` ensures that only your app (which generated the challenge) can exchange the code.
157+
* The deep link ensures the token is delivered **only** to your app.
158+
* No backend is required.
159+
160+
---
161+
162+
## 5. Universal links alternative
163+
164+
If you want to use a normal HTTPS redirect (e.g. `https://www.compassmeet.com/auth/callback`), you can register it as a **universal link**:
165+
166+
* User finishes login → redirected to your HTTPS domain.
167+
* That URL is also registered to open your app (via Digital Asset Links JSON).
168+
* Android recognizes it and launches your app instead of loading the page in the browser.
169+
* The rest of the flow is the same.
170+
171+
However, universal links are more setup-heavy (require hosting a `.well-known/assetlinks.json` file).
172+
173+
---
174+
175+
## 6. Summary
176+
177+
| Step | What happens | Where |
178+
| ---- | ------------------------------------------------------------- | -------------- |
179+
| 1 | Generate PKCE challenge and open Google OAuth URL | WebView |
180+
| 2 | User signs in | System browser |
181+
| 3 | Browser redirects to deep link (e.g. `com.compassmeet:/auth`) | OS → App |
182+
| 4 | App intercepts deep link and injects it into WebView | Native layer |
183+
| 5 | WebView exchanges `code` for tokens via PKCE | Web app |

0 commit comments

Comments
 (0)