react-native-nitro-auth 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -17
- package/android/proguard-rules.pro +7 -1
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/PlatformAuth+Android.cpp +260 -67
- package/android/src/main/java/com/auth/AuthAdapter.kt +217 -146
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +9 -5
- package/cpp/HybridAuth.cpp +79 -64
- package/cpp/HybridAuth.hpp +9 -7
- package/cpp/JSONSerializer.hpp +3 -0
- package/ios/AuthAdapter.swift +173 -60
- package/ios/PlatformAuth+iOS.mm +10 -3
- package/lib/commonjs/Auth.web.js +50 -10
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.web.js +30 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/service.js +9 -7
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +65 -13
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +19 -14
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +16 -10
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +13 -5
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/logger.js +1 -0
- package/lib/commonjs/utils/logger.js.map +1 -1
- package/lib/module/Auth.web.js +50 -10
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/global.d.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/service.js +9 -7
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +65 -13
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +19 -14
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +16 -10
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js +13 -5
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/logger.js +1 -0
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +5 -1
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +2 -18
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +5 -1
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +2 -18
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/package.json +2 -4
- package/src/Auth.web.ts +77 -11
- package/src/global.d.ts +0 -11
- package/src/index.ts +5 -1
- package/src/index.web.ts +6 -1
- package/src/service.ts +9 -7
- package/src/service.web.ts +84 -15
- package/src/ui/social-button.tsx +21 -9
- package/src/ui/social-button.web.tsx +17 -4
- package/src/use-auth.ts +35 -8
- package/src/utils/logger.ts +1 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
@file:Suppress("DEPRECATION")
|
|
2
|
+
// The legacy com.google.android.gms.auth.api.signin.* API is used intentionally for:
|
|
3
|
+
// • getLastSignedInAccount – persists session across app restarts via GMS store; no drop-in replacement
|
|
4
|
+
// • silentSignIn – AuthorizationClient.authorize() still requires an Activity for interactive fallback
|
|
5
|
+
// • revokeAccess – no equivalent in Credential Manager or Identity.getAuthorizationClient()
|
|
6
|
+
// All modern entry-points use Credential Manager (One-Tap). Legacy is a documented fallback only.
|
|
7
|
+
|
|
1
8
|
package com.auth
|
|
2
9
|
|
|
3
10
|
import android.app.Activity
|
|
@@ -9,6 +16,7 @@ import android.os.Bundle
|
|
|
9
16
|
import android.util.Base64
|
|
10
17
|
import android.util.Log
|
|
11
18
|
import androidx.browser.customtabs.CustomTabsIntent
|
|
19
|
+
import androidx.credentials.ClearCredentialStateRequest
|
|
12
20
|
import androidx.credentials.CredentialManager
|
|
13
21
|
import androidx.credentials.GetCredentialRequest
|
|
14
22
|
import androidx.credentials.GetCredentialResponse
|
|
@@ -43,14 +51,24 @@ object AuthAdapter {
|
|
|
43
51
|
private var pendingScopes: List<String> = emptyList()
|
|
44
52
|
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
45
53
|
|
|
54
|
+
@Volatile
|
|
55
|
+
private var pendingOrigin: String = "login"
|
|
56
|
+
@Volatile
|
|
46
57
|
private var pendingPkceVerifier: String? = null
|
|
58
|
+
@Volatile
|
|
47
59
|
private var pendingState: String? = null
|
|
60
|
+
@Volatile
|
|
48
61
|
private var pendingNonce: String? = null
|
|
62
|
+
@Volatile
|
|
49
63
|
private var pendingMicrosoftTenant: String? = null
|
|
64
|
+
@Volatile
|
|
50
65
|
private var pendingMicrosoftClientId: String? = null
|
|
66
|
+
@Volatile
|
|
51
67
|
private var pendingMicrosoftB2cDomain: String? = null
|
|
52
68
|
|
|
69
|
+
@Volatile
|
|
53
70
|
private var inMemoryMicrosoftRefreshToken: String? = null
|
|
71
|
+
@Volatile
|
|
54
72
|
private var inMemoryMicrosoftScopes: List<String> =
|
|
55
73
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
56
74
|
|
|
@@ -62,6 +80,7 @@ object AuthAdapter {
|
|
|
62
80
|
|
|
63
81
|
@JvmStatic
|
|
64
82
|
private external fun nativeOnLoginSuccess(
|
|
83
|
+
origin: String,
|
|
65
84
|
provider: String,
|
|
66
85
|
email: String?,
|
|
67
86
|
name: String?,
|
|
@@ -74,7 +93,7 @@ object AuthAdapter {
|
|
|
74
93
|
)
|
|
75
94
|
|
|
76
95
|
@JvmStatic
|
|
77
|
-
private external fun nativeOnLoginError(error: String, underlyingError: String?)
|
|
96
|
+
private external fun nativeOnLoginError(origin: String, error: String, underlyingError: String?)
|
|
78
97
|
|
|
79
98
|
@JvmStatic
|
|
80
99
|
private external fun nativeOnRefreshSuccess(idToken: String?, accessToken: String?, expirationTime: Long?)
|
|
@@ -127,22 +146,22 @@ object AuthAdapter {
|
|
|
127
146
|
isInitialized = false
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String
|
|
149
|
+
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>, origin: String = "login") {
|
|
131
150
|
appContext ?: return
|
|
132
151
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
133
|
-
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
152
|
+
nativeOnLoginSuccess(origin, "google", account.email, account.displayName,
|
|
134
153
|
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
135
154
|
scopes.toTypedArray(), expirationTime)
|
|
136
155
|
}
|
|
137
156
|
|
|
138
|
-
fun onSignInError(errorCode: Int, message: String
|
|
157
|
+
fun onSignInError(errorCode: Int, message: String?, origin: String = "login") {
|
|
139
158
|
val mappedError = when (errorCode) {
|
|
140
159
|
12501 -> "cancelled"
|
|
141
160
|
7 -> "network_error"
|
|
142
161
|
8, 10 -> "configuration_error"
|
|
143
162
|
else -> "unknown"
|
|
144
163
|
}
|
|
145
|
-
nativeOnLoginError(mappedError, message)
|
|
164
|
+
nativeOnLoginError(origin, mappedError, message)
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
@JvmStatic
|
|
@@ -159,22 +178,22 @@ object AuthAdapter {
|
|
|
159
178
|
prompt: String? = null
|
|
160
179
|
) {
|
|
161
180
|
if (provider == "apple") {
|
|
162
|
-
nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
181
|
+
nativeOnLoginError("login", "unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
163
182
|
return
|
|
164
183
|
}
|
|
165
184
|
if (provider == "microsoft") {
|
|
166
|
-
loginMicrosoft(context, scopes, loginHint, tenant, prompt)
|
|
185
|
+
loginMicrosoft(context, scopes, loginHint, tenant, prompt, "login")
|
|
167
186
|
return
|
|
168
187
|
}
|
|
169
188
|
if (provider != "google") {
|
|
170
|
-
nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
|
|
189
|
+
nativeOnLoginError("login", "unsupported_provider", "Unsupported provider: $provider")
|
|
171
190
|
return
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
val ctx = appContext ?: context.applicationContext
|
|
175
194
|
val clientId = googleClientId ?: getClientIdFromResources(ctx)
|
|
176
195
|
if (clientId == null) {
|
|
177
|
-
nativeOnLoginError("configuration_error", "Google Client ID is required. Set it in app.json plugins.")
|
|
196
|
+
nativeOnLoginError("login", "configuration_error", "Google Client ID is required. Set it in app.json plugins.")
|
|
178
197
|
return
|
|
179
198
|
}
|
|
180
199
|
|
|
@@ -182,19 +201,20 @@ object AuthAdapter {
|
|
|
182
201
|
pendingScopes = requestedScopes
|
|
183
202
|
|
|
184
203
|
if (useLegacyGoogleSignIn) {
|
|
185
|
-
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
|
|
204
|
+
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker, "login")
|
|
186
205
|
return
|
|
187
206
|
}
|
|
188
|
-
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
|
|
207
|
+
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap, "login")
|
|
189
208
|
}
|
|
190
209
|
|
|
191
|
-
private fun loginMicrosoft(context: Context, scopes: Array<String>?, loginHint: String?, tenant: String?, prompt: String
|
|
210
|
+
private fun loginMicrosoft(context: Context, scopes: Array<String>?, loginHint: String?, tenant: String?, prompt: String?, origin: String = "login") {
|
|
192
211
|
val ctx = appContext ?: context.applicationContext
|
|
193
212
|
val clientId = getMicrosoftClientIdFromResources(ctx)
|
|
194
213
|
if (clientId == null) {
|
|
195
|
-
nativeOnLoginError("configuration_error", "Microsoft Client ID is required. Set it in app.json plugins.")
|
|
214
|
+
nativeOnLoginError(origin, "configuration_error", "Microsoft Client ID is required. Set it in app.json plugins.")
|
|
196
215
|
return
|
|
197
216
|
}
|
|
217
|
+
pendingOrigin = origin
|
|
198
218
|
|
|
199
219
|
val effectiveTenant = tenant ?: getMicrosoftTenantFromResources(ctx) ?: "common"
|
|
200
220
|
val effectiveScopes = scopes?.toList() ?: listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
@@ -241,7 +261,7 @@ object AuthAdapter {
|
|
|
241
261
|
ctx.startActivity(browserIntent)
|
|
242
262
|
}
|
|
243
263
|
} catch (e: Exception) {
|
|
244
|
-
nativeOnLoginError("unknown", e.message)
|
|
264
|
+
nativeOnLoginError(origin, "unknown", e.message)
|
|
245
265
|
}
|
|
246
266
|
}
|
|
247
267
|
|
|
@@ -263,19 +283,20 @@ object AuthAdapter {
|
|
|
263
283
|
val error = uri.getQueryParameter("error")
|
|
264
284
|
val errorDescription = uri.getQueryParameter("error_description")
|
|
265
285
|
|
|
286
|
+
val origin = pendingOrigin
|
|
266
287
|
if (error != null) {
|
|
267
288
|
clearPkceState()
|
|
268
|
-
nativeOnLoginError(error, errorDescription)
|
|
289
|
+
nativeOnLoginError(origin, error, errorDescription)
|
|
269
290
|
return
|
|
270
291
|
}
|
|
271
292
|
if (state != pendingState) {
|
|
272
293
|
clearPkceState()
|
|
273
|
-
nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
|
|
294
|
+
nativeOnLoginError(origin, "invalid_state", "State mismatch - possible CSRF attack")
|
|
274
295
|
return
|
|
275
296
|
}
|
|
276
297
|
if (code == null) {
|
|
277
298
|
clearPkceState()
|
|
278
|
-
nativeOnLoginError("unknown", "No authorization code in response")
|
|
299
|
+
nativeOnLoginError(origin, "unknown", "No authorization code in response")
|
|
279
300
|
return
|
|
280
301
|
}
|
|
281
302
|
exchangeCodeForTokens(code)
|
|
@@ -286,10 +307,11 @@ object AuthAdapter {
|
|
|
286
307
|
val clientId = pendingMicrosoftClientId
|
|
287
308
|
val tenant = pendingMicrosoftTenant
|
|
288
309
|
val verifier = pendingPkceVerifier
|
|
310
|
+
val origin = pendingOrigin
|
|
289
311
|
|
|
290
312
|
if (ctx == null || clientId == null || tenant == null || verifier == null) {
|
|
291
313
|
clearPkceState()
|
|
292
|
-
nativeOnLoginError("invalid_state", "Missing PKCE state for token exchange")
|
|
314
|
+
nativeOnLoginError(origin, "invalid_state", "Missing PKCE state for token exchange")
|
|
293
315
|
return
|
|
294
316
|
}
|
|
295
317
|
|
|
@@ -301,49 +323,55 @@ object AuthAdapter {
|
|
|
301
323
|
try {
|
|
302
324
|
val url = java.net.URL(tokenUrl)
|
|
303
325
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
326
|
+
try {
|
|
327
|
+
connection.connectTimeout = 15_000
|
|
328
|
+
connection.readTimeout = 15_000
|
|
329
|
+
connection.requestMethod = "POST"
|
|
330
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
331
|
+
connection.doOutput = true
|
|
332
|
+
|
|
333
|
+
val postData = buildString {
|
|
334
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
335
|
+
append("&code=${java.net.URLEncoder.encode(code, "UTF-8")}")
|
|
336
|
+
append("&redirect_uri=${java.net.URLEncoder.encode(redirectUri, "UTF-8")}")
|
|
337
|
+
append("&grant_type=authorization_code")
|
|
338
|
+
append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
|
|
339
|
+
}
|
|
340
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
316
341
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
342
|
+
val responseCode = connection.responseCode
|
|
343
|
+
val responseBody = if (responseCode == 200) {
|
|
344
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
345
|
+
} else {
|
|
346
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
347
|
+
}
|
|
323
348
|
|
|
324
|
-
|
|
325
|
-
|
|
349
|
+
withContext(Dispatchers.Main) {
|
|
350
|
+
handleTokenResponse(responseCode, responseBody, origin)
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
connection.disconnect()
|
|
326
354
|
}
|
|
327
355
|
} catch (e: Exception) {
|
|
328
356
|
withContext(Dispatchers.Main) {
|
|
329
357
|
clearPkceState()
|
|
330
|
-
nativeOnLoginError("network_error", e.message)
|
|
358
|
+
nativeOnLoginError(origin, "network_error", e.message)
|
|
331
359
|
}
|
|
332
360
|
}
|
|
333
361
|
}
|
|
334
362
|
}
|
|
335
363
|
|
|
336
|
-
private fun handleTokenResponse(responseCode: Int, responseBody: String) {
|
|
364
|
+
private fun handleTokenResponse(responseCode: Int, responseBody: String, origin: String) {
|
|
337
365
|
if (responseCode != 200) {
|
|
338
366
|
try {
|
|
339
367
|
val json = JSONObject(responseBody)
|
|
340
368
|
val error = json.optString("error", "token_error")
|
|
341
369
|
val desc = json.optString("error_description", "Failed to exchange code for tokens")
|
|
342
370
|
clearPkceState()
|
|
343
|
-
nativeOnLoginError(error, desc)
|
|
371
|
+
nativeOnLoginError(origin, error, desc)
|
|
344
372
|
} catch (e: Exception) {
|
|
345
373
|
clearPkceState()
|
|
346
|
-
nativeOnLoginError("token_error", "Failed to exchange code for tokens")
|
|
374
|
+
nativeOnLoginError(origin, "token_error", "Failed to exchange code for tokens")
|
|
347
375
|
}
|
|
348
376
|
return
|
|
349
377
|
}
|
|
@@ -358,14 +386,14 @@ object AuthAdapter {
|
|
|
358
386
|
|
|
359
387
|
if (idToken.isEmpty()) {
|
|
360
388
|
clearPkceState()
|
|
361
|
-
nativeOnLoginError("no_id_token", "No id_token in token response")
|
|
389
|
+
nativeOnLoginError(origin, "no_id_token", "No id_token in token response")
|
|
362
390
|
return
|
|
363
391
|
}
|
|
364
392
|
|
|
365
393
|
val claims = decodeJwt(idToken)
|
|
366
394
|
if (claims["nonce"] != pendingNonce) {
|
|
367
395
|
clearPkceState()
|
|
368
|
-
nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
396
|
+
nativeOnLoginError(origin, "invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
369
397
|
return
|
|
370
398
|
}
|
|
371
399
|
|
|
@@ -379,16 +407,17 @@ object AuthAdapter {
|
|
|
379
407
|
|
|
380
408
|
clearPkceState()
|
|
381
409
|
nativeOnLoginSuccess(
|
|
382
|
-
"microsoft", email, name, null, idToken, accessToken, null,
|
|
410
|
+
origin, "microsoft", email, name, null, idToken, accessToken, null,
|
|
383
411
|
pendingMicrosoftScopes.toTypedArray(), expirationTime
|
|
384
412
|
)
|
|
385
413
|
} catch (e: Exception) {
|
|
386
414
|
clearPkceState()
|
|
387
|
-
nativeOnLoginError("parse_error", e.message)
|
|
415
|
+
nativeOnLoginError(origin, "parse_error", e.message)
|
|
388
416
|
}
|
|
389
417
|
}
|
|
390
418
|
|
|
391
419
|
private fun clearPkceState() {
|
|
420
|
+
pendingOrigin = "login"
|
|
392
421
|
pendingPkceVerifier = null
|
|
393
422
|
pendingState = null
|
|
394
423
|
pendingNonce = null
|
|
@@ -410,6 +439,7 @@ object AuthAdapter {
|
|
|
410
439
|
}
|
|
411
440
|
result
|
|
412
441
|
} catch (e: Exception) {
|
|
442
|
+
Log.w(TAG, "Failed to decode JWT: ${e.message}")
|
|
413
443
|
emptyMap()
|
|
414
444
|
}
|
|
415
445
|
}
|
|
@@ -452,12 +482,13 @@ object AuthAdapter {
|
|
|
452
482
|
scopes: List<String>,
|
|
453
483
|
loginHint: String?,
|
|
454
484
|
forceAccountPicker: Boolean,
|
|
455
|
-
useOneTap: Boolean
|
|
485
|
+
useOneTap: Boolean,
|
|
486
|
+
origin: String = "login"
|
|
456
487
|
) {
|
|
457
488
|
val activity = currentActivity ?: context as? Activity
|
|
458
489
|
if (activity == null) {
|
|
459
490
|
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
460
|
-
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
491
|
+
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker, origin)
|
|
461
492
|
}
|
|
462
493
|
|
|
463
494
|
val credentialManager = CredentialManager.create(activity)
|
|
@@ -474,31 +505,31 @@ object AuthAdapter {
|
|
|
474
505
|
moduleScope.launch(Dispatchers.Main) {
|
|
475
506
|
try {
|
|
476
507
|
val result = credentialManager.getCredential(context = activity, request = request)
|
|
477
|
-
handleCredentialResponse(result, scopes)
|
|
508
|
+
handleCredentialResponse(result, scopes, origin)
|
|
478
509
|
} catch (e: Exception) {
|
|
479
510
|
Log.w(TAG, "One-Tap failed, falling back to legacy: ${e.message}")
|
|
480
|
-
loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
511
|
+
loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker, origin)
|
|
481
512
|
}
|
|
482
513
|
}
|
|
483
514
|
}
|
|
484
515
|
|
|
485
|
-
@Suppress("DEPRECATION")
|
|
486
516
|
private fun loginLegacy(
|
|
487
517
|
context: Context,
|
|
488
518
|
clientId: String,
|
|
489
519
|
scopes: List<String>,
|
|
490
520
|
loginHint: String?,
|
|
491
|
-
forceAccountPicker: Boolean
|
|
521
|
+
forceAccountPicker: Boolean,
|
|
522
|
+
origin: String = "login"
|
|
492
523
|
) {
|
|
493
524
|
val ctx = appContext ?: context.applicationContext
|
|
494
525
|
val intent = GoogleSignInActivity.createIntent(
|
|
495
|
-
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
|
|
526
|
+
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker, origin
|
|
496
527
|
)
|
|
497
528
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
498
529
|
ctx.startActivity(intent)
|
|
499
530
|
}
|
|
500
531
|
|
|
501
|
-
private fun handleCredentialResponse(response: GetCredentialResponse, scopes: List<String
|
|
532
|
+
private fun handleCredentialResponse(response: GetCredentialResponse, scopes: List<String>, origin: String) {
|
|
502
533
|
val credential = response.credential
|
|
503
534
|
val googleIdTokenCredential = try {
|
|
504
535
|
if (credential is GoogleIdTokenCredential) {
|
|
@@ -516,7 +547,7 @@ object AuthAdapter {
|
|
|
516
547
|
if (googleIdTokenCredential != null) {
|
|
517
548
|
val expirationTime = getJwtExpirationTimeMs(googleIdTokenCredential.idToken)
|
|
518
549
|
nativeOnLoginSuccess(
|
|
519
|
-
"google",
|
|
550
|
+
origin, "google",
|
|
520
551
|
googleIdTokenCredential.id,
|
|
521
552
|
googleIdTokenCredential.displayName,
|
|
522
553
|
googleIdTokenCredential.profilePictureUri?.toString(),
|
|
@@ -527,13 +558,12 @@ object AuthAdapter {
|
|
|
527
558
|
)
|
|
528
559
|
} else {
|
|
529
560
|
Log.w(TAG, "Unsupported credential type: ${credential.type}")
|
|
530
|
-
nativeOnLoginError("unknown", "Unsupported credential type: ${credential.type}")
|
|
561
|
+
nativeOnLoginError(origin, "unknown", "Unsupported credential type: ${credential.type}")
|
|
531
562
|
}
|
|
532
563
|
}
|
|
533
564
|
|
|
534
565
|
// requestScopesSync uses the legacy GoogleSignIn API to check the last signed-in account
|
|
535
566
|
// because Credential Manager has no equivalent for querying existing account state.
|
|
536
|
-
@Suppress("DEPRECATION")
|
|
537
567
|
@JvmStatic
|
|
538
568
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
539
569
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -541,31 +571,30 @@ object AuthAdapter {
|
|
|
541
571
|
if (account != null) {
|
|
542
572
|
val newScopes = scopes.map { Scope(it) }
|
|
543
573
|
if (GoogleSignIn.hasPermissions(account, *newScopes.toTypedArray())) {
|
|
544
|
-
onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct())
|
|
574
|
+
onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct(), "scopes")
|
|
545
575
|
return
|
|
546
576
|
}
|
|
547
577
|
val clientId = getClientIdFromResources(ctx)
|
|
548
578
|
if (clientId == null) {
|
|
549
|
-
nativeOnLoginError("configuration_error", "Google Client ID not configured")
|
|
579
|
+
nativeOnLoginError("scopes", "configuration_error", "Google Client ID not configured")
|
|
550
580
|
return
|
|
551
581
|
}
|
|
552
582
|
val allScopes = (pendingScopes + scopes.toList()).distinct()
|
|
553
|
-
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email)
|
|
583
|
+
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email, origin = "scopes")
|
|
554
584
|
ctx.startActivity(intent)
|
|
555
585
|
return
|
|
556
586
|
}
|
|
557
587
|
if (inMemoryMicrosoftRefreshToken != null) {
|
|
558
588
|
val mergedScopes = (inMemoryMicrosoftScopes + scopes.toList()).distinct()
|
|
559
589
|
val tenant = getMicrosoftTenantFromResources(ctx)
|
|
560
|
-
loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null)
|
|
590
|
+
loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null, "scopes")
|
|
561
591
|
return
|
|
562
592
|
}
|
|
563
|
-
nativeOnLoginError("unknown", "No user logged in")
|
|
593
|
+
nativeOnLoginError("scopes", "unknown", "No user logged in")
|
|
564
594
|
}
|
|
565
595
|
|
|
566
596
|
// refreshTokenSync uses the legacy silentSignIn because AuthorizationClient (the recommended
|
|
567
597
|
// replacement) requires an Activity context which is not always available at refresh time.
|
|
568
|
-
@Suppress("DEPRECATION")
|
|
569
598
|
@JvmStatic
|
|
570
599
|
fun refreshTokenSync(context: Context) {
|
|
571
600
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -604,28 +633,35 @@ object AuthAdapter {
|
|
|
604
633
|
|
|
605
634
|
@JvmStatic
|
|
606
635
|
fun hasPlayServices(context: Context): Boolean {
|
|
607
|
-
val ctx = context.applicationContext ?:
|
|
636
|
+
val ctx = appContext ?: context.applicationContext ?: return false
|
|
608
637
|
return GoogleApiAvailability.getInstance()
|
|
609
638
|
.isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
|
|
610
639
|
}
|
|
611
640
|
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
@Suppress("DEPRECATION")
|
|
641
|
+
// revokeAccessSync uses the legacy GoogleSignIn client because Credential Manager has no
|
|
642
|
+
// equivalent revoke API for the Google ID token flow.
|
|
615
643
|
@JvmStatic
|
|
616
644
|
fun logoutSync(context: Context) {
|
|
617
645
|
val ctx = appContext ?: context.applicationContext
|
|
646
|
+
// Clear Credential Manager state (covers One-Tap / passkey credentials).
|
|
647
|
+
moduleScope.launch {
|
|
648
|
+
try {
|
|
649
|
+
CredentialManager.create(ctx).clearCredentialState(ClearCredentialStateRequest())
|
|
650
|
+
} catch (e: Exception) {
|
|
651
|
+
Log.w(TAG, "clearCredentialState failed: ${e.message}")
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Also clear legacy GMS sign-in state so getLastSignedInAccount returns null.
|
|
618
655
|
val clientId = getClientIdFromResources(ctx)
|
|
619
656
|
if (clientId != null) {
|
|
620
657
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
621
|
-
.requestIdToken(clientId).
|
|
658
|
+
.requestIdToken(clientId).requestEmail().build()
|
|
622
659
|
GoogleSignIn.getClient(ctx, gso).signOut()
|
|
623
660
|
}
|
|
624
661
|
inMemoryMicrosoftRefreshToken = null
|
|
625
662
|
inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
626
663
|
}
|
|
627
664
|
|
|
628
|
-
@Suppress("DEPRECATION")
|
|
629
665
|
@JvmStatic
|
|
630
666
|
fun revokeAccessSync(context: Context) {
|
|
631
667
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -646,12 +682,12 @@ object AuthAdapter {
|
|
|
646
682
|
|
|
647
683
|
@JvmStatic
|
|
648
684
|
fun restoreSession(context: Context) {
|
|
649
|
-
val ctx = context.applicationContext ?:
|
|
685
|
+
val ctx = appContext ?: context.applicationContext ?: return
|
|
650
686
|
@Suppress("DEPRECATION")
|
|
651
687
|
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
652
688
|
if (account != null) {
|
|
653
689
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
654
|
-
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
690
|
+
nativeOnLoginSuccess("silent", "google", account.email, account.displayName,
|
|
655
691
|
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
656
692
|
account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
|
|
657
693
|
} else {
|
|
@@ -659,7 +695,7 @@ object AuthAdapter {
|
|
|
659
695
|
if (refreshToken != null) {
|
|
660
696
|
refreshMicrosoftToken(ctx, refreshToken)
|
|
661
697
|
} else {
|
|
662
|
-
nativeOnLoginError("unknown", "No session")
|
|
698
|
+
nativeOnLoginError("silent", "unknown", "No session")
|
|
663
699
|
}
|
|
664
700
|
}
|
|
665
701
|
}
|
|
@@ -670,7 +706,7 @@ object AuthAdapter {
|
|
|
670
706
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
671
707
|
|
|
672
708
|
if (clientId == null) {
|
|
673
|
-
nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
|
|
709
|
+
nativeOnLoginError("silent", "configuration_error", "Microsoft Client ID is required for refresh")
|
|
674
710
|
return
|
|
675
711
|
}
|
|
676
712
|
|
|
@@ -681,50 +717,67 @@ object AuthAdapter {
|
|
|
681
717
|
try {
|
|
682
718
|
val url = java.net.URL(tokenUrl)
|
|
683
719
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
720
|
+
try {
|
|
721
|
+
connection.connectTimeout = 15_000
|
|
722
|
+
connection.readTimeout = 15_000
|
|
723
|
+
connection.requestMethod = "POST"
|
|
724
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
725
|
+
connection.doOutput = true
|
|
726
|
+
|
|
727
|
+
val postData = buildString {
|
|
728
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
729
|
+
append("&grant_type=refresh_token")
|
|
730
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
731
|
+
}
|
|
732
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
694
733
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
734
|
+
val responseCode = connection.responseCode
|
|
735
|
+
val responseBody = if (responseCode == 200) {
|
|
736
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
737
|
+
} else {
|
|
738
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
739
|
+
}
|
|
701
740
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
741
|
+
withContext(Dispatchers.Main) {
|
|
742
|
+
if (responseCode == 200) {
|
|
743
|
+
val json = JSONObject(responseBody)
|
|
744
|
+
val newIdToken = json.optString("id_token")
|
|
745
|
+
val newAccessToken = json.optString("access_token")
|
|
746
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
747
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
748
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
749
|
+
val claims = decodeJwt(newIdToken)
|
|
750
|
+
|
|
751
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
752
|
+
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
753
|
+
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
nativeOnLoginSuccess("silent", "microsoft",
|
|
757
|
+
claims["preferred_username"] ?: claims["email"],
|
|
758
|
+
claims["name"], null,
|
|
759
|
+
newIdToken, newAccessToken, null, null, expirationTime)
|
|
760
|
+
} else {
|
|
761
|
+
if (responseCode in 400..499) {
|
|
762
|
+
inMemoryMicrosoftRefreshToken = null // Token is invalid, clear it
|
|
763
|
+
}
|
|
764
|
+
val mappedError = try {
|
|
765
|
+
val json = org.json.JSONObject(responseBody)
|
|
766
|
+
val errorCode = json.optString("error", "token_error")
|
|
767
|
+
val errorDesc = json.optString("error_description", "Token refresh failed")
|
|
768
|
+
Pair(errorCode, errorDesc)
|
|
769
|
+
} catch (e: Exception) {
|
|
770
|
+
Pair("token_error", "Token refresh failed")
|
|
771
|
+
}
|
|
772
|
+
nativeOnLoginError("silent", mappedError.first, mappedError.second)
|
|
715
773
|
}
|
|
716
|
-
|
|
717
|
-
nativeOnLoginSuccess("microsoft",
|
|
718
|
-
claims["preferred_username"] ?: claims["email"],
|
|
719
|
-
claims["name"], null,
|
|
720
|
-
newIdToken, newAccessToken, null, null, expirationTime)
|
|
721
|
-
} else {
|
|
722
|
-
nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
|
|
723
774
|
}
|
|
775
|
+
} finally {
|
|
776
|
+
connection.disconnect()
|
|
724
777
|
}
|
|
725
778
|
} catch (e: Exception) {
|
|
726
779
|
withContext(Dispatchers.Main) {
|
|
727
|
-
nativeOnLoginError("network_error", e.message)
|
|
780
|
+
nativeOnLoginError("silent", "network_error", e.message)
|
|
728
781
|
}
|
|
729
782
|
}
|
|
730
783
|
}
|
|
@@ -747,46 +800,64 @@ object AuthAdapter {
|
|
|
747
800
|
try {
|
|
748
801
|
val url = java.net.URL(tokenUrl)
|
|
749
802
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
803
|
+
try {
|
|
804
|
+
connection.connectTimeout = 15_000
|
|
805
|
+
connection.readTimeout = 15_000
|
|
806
|
+
connection.requestMethod = "POST"
|
|
807
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
808
|
+
connection.doOutput = true
|
|
809
|
+
|
|
810
|
+
val postData = buildString {
|
|
811
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
812
|
+
append("&grant_type=refresh_token")
|
|
813
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
814
|
+
}
|
|
815
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
760
816
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
817
|
+
val responseCode = connection.responseCode
|
|
818
|
+
val responseBody = if (responseCode == 200) {
|
|
819
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
820
|
+
} else {
|
|
821
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
822
|
+
}
|
|
767
823
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
824
|
+
withContext(Dispatchers.Main) {
|
|
825
|
+
if (responseCode == 200) {
|
|
826
|
+
val json = JSONObject(responseBody)
|
|
827
|
+
val newIdToken = json.optString("id_token")
|
|
828
|
+
val newAccessToken = json.optString("access_token")
|
|
829
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
830
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
831
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
832
|
+
|
|
833
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
834
|
+
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
835
|
+
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
nativeOnRefreshSuccess(
|
|
839
|
+
newIdToken.ifEmpty { null },
|
|
840
|
+
newAccessToken.ifEmpty { null },
|
|
841
|
+
expirationTime
|
|
842
|
+
)
|
|
843
|
+
} else {
|
|
844
|
+
if (responseCode in 400..499) {
|
|
845
|
+
inMemoryMicrosoftRefreshToken = null
|
|
846
|
+
}
|
|
847
|
+
val errorBody = responseBody
|
|
848
|
+
val mappedError = try {
|
|
849
|
+
val json = org.json.JSONObject(errorBody)
|
|
850
|
+
val errorCode = json.optString("error", "token_error")
|
|
851
|
+
val errorDesc = json.optString("error_description", "Token refresh failed")
|
|
852
|
+
Pair(errorCode, errorDesc)
|
|
853
|
+
} catch (e: Exception) {
|
|
854
|
+
Pair("token_error", "Token refresh failed")
|
|
855
|
+
}
|
|
856
|
+
nativeOnRefreshError(mappedError.first, mappedError.second)
|
|
780
857
|
}
|
|
781
|
-
|
|
782
|
-
nativeOnRefreshSuccess(
|
|
783
|
-
newIdToken.ifEmpty { null },
|
|
784
|
-
newAccessToken.ifEmpty { null },
|
|
785
|
-
expirationTime
|
|
786
|
-
)
|
|
787
|
-
} else {
|
|
788
|
-
nativeOnRefreshError("refresh_failed", "Microsoft token refresh failed")
|
|
789
858
|
}
|
|
859
|
+
} finally {
|
|
860
|
+
connection.disconnect()
|
|
790
861
|
}
|
|
791
862
|
} catch (e: Exception) {
|
|
792
863
|
withContext(Dispatchers.Main) {
|