react-native-nitro-auth 0.5.5 → 0.5.7
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 +30 -19
- package/android/proguard-rules.pro +7 -1
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/PlatformAuth+Android.cpp +261 -68
- package/android/src/main/java/com/auth/AuthAdapter.kt +250 -157
- 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 +208 -66
- package/ios/PlatformAuth+iOS.mm +30 -4
- 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 +25 -19
- 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 +34 -10
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +1 -1
- package/lib/commonjs/utils/auth-error.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 +25 -19
- 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 +34 -10
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +1 -1
- package/lib/module/utils/auth-error.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.nitro.d.ts +2 -2
- package/lib/typescript/commonjs/Auth.nitro.d.ts.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/auth-error.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +2 -2
- package/lib/typescript/module/Auth.nitro.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/auth-error.d.ts.map +1 -1
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/nitro.json +4 -1
- package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -0
- package/package.json +3 -4
- package/src/Auth.nitro.ts +3 -1
- 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 +26 -19
- 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 +65 -9
- package/src/utils/auth-error.ts +2 -0
- package/src/utils/logger.ts +1 -0
|
@@ -1,3 +1,11 @@
|
|
|
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) unless the caller explicitly needs
|
|
7
|
+
// Android's account chooser semantics, which still require the legacy Google Sign-In flow.
|
|
8
|
+
|
|
1
9
|
package com.auth
|
|
2
10
|
|
|
3
11
|
import android.app.Activity
|
|
@@ -9,6 +17,7 @@ import android.os.Bundle
|
|
|
9
17
|
import android.util.Base64
|
|
10
18
|
import android.util.Log
|
|
11
19
|
import androidx.browser.customtabs.CustomTabsIntent
|
|
20
|
+
import androidx.credentials.ClearCredentialStateRequest
|
|
12
21
|
import androidx.credentials.CredentialManager
|
|
13
22
|
import androidx.credentials.GetCredentialRequest
|
|
14
23
|
import androidx.credentials.GetCredentialResponse
|
|
@@ -32,36 +41,54 @@ import java.util.UUID
|
|
|
32
41
|
|
|
33
42
|
object AuthAdapter {
|
|
34
43
|
private const val TAG = "AuthAdapter"
|
|
44
|
+
private val defaultMicrosoftScopes =
|
|
45
|
+
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
35
46
|
|
|
36
47
|
@Volatile
|
|
37
48
|
private var isInitialized = false
|
|
38
49
|
|
|
39
50
|
private var appContext: Context? = null
|
|
51
|
+
@Volatile
|
|
40
52
|
private var currentActivity: Activity? = null
|
|
41
53
|
private var googleSignInClient: GoogleSignInClient? = null
|
|
42
54
|
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
|
43
55
|
private var pendingScopes: List<String> = emptyList()
|
|
44
56
|
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
45
57
|
|
|
58
|
+
@Volatile
|
|
59
|
+
private var pendingOrigin: String = "login"
|
|
60
|
+
@Volatile
|
|
46
61
|
private var pendingPkceVerifier: String? = null
|
|
62
|
+
@Volatile
|
|
47
63
|
private var pendingState: String? = null
|
|
64
|
+
@Volatile
|
|
48
65
|
private var pendingNonce: String? = null
|
|
66
|
+
@Volatile
|
|
49
67
|
private var pendingMicrosoftTenant: String? = null
|
|
68
|
+
@Volatile
|
|
50
69
|
private var pendingMicrosoftClientId: String? = null
|
|
70
|
+
@Volatile
|
|
51
71
|
private var pendingMicrosoftB2cDomain: String? = null
|
|
72
|
+
@Volatile
|
|
73
|
+
private var microsoftAuthInProgress = false
|
|
52
74
|
|
|
75
|
+
@Volatile
|
|
53
76
|
private var inMemoryMicrosoftRefreshToken: String? = null
|
|
77
|
+
@Volatile
|
|
54
78
|
private var inMemoryMicrosoftScopes: List<String> =
|
|
55
|
-
|
|
79
|
+
defaultMicrosoftScopes
|
|
56
80
|
|
|
57
81
|
// Module-scoped coroutine scope — cancelled on module invalidation via dispose().
|
|
58
82
|
private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
59
83
|
|
|
60
84
|
@JvmStatic
|
|
61
85
|
private external fun nativeInitialize(context: Context)
|
|
86
|
+
@JvmStatic
|
|
87
|
+
private external fun nativeDispose()
|
|
62
88
|
|
|
63
89
|
@JvmStatic
|
|
64
90
|
private external fun nativeOnLoginSuccess(
|
|
91
|
+
origin: String,
|
|
65
92
|
provider: String,
|
|
66
93
|
email: String?,
|
|
67
94
|
name: String?,
|
|
@@ -74,7 +101,7 @@ object AuthAdapter {
|
|
|
74
101
|
)
|
|
75
102
|
|
|
76
103
|
@JvmStatic
|
|
77
|
-
private external fun nativeOnLoginError(error: String, underlyingError: String?)
|
|
104
|
+
private external fun nativeOnLoginError(origin: String, error: String, underlyingError: String?)
|
|
78
105
|
|
|
79
106
|
@JvmStatic
|
|
80
107
|
private external fun nativeOnRefreshSuccess(idToken: String?, accessToken: String?, expirationTime: Long?)
|
|
@@ -117,6 +144,8 @@ object AuthAdapter {
|
|
|
117
144
|
fun dispose() {
|
|
118
145
|
moduleScope.cancel()
|
|
119
146
|
moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
147
|
+
runCatching { nativeDispose() }
|
|
148
|
+
.onFailure { Log.w(TAG, "Failed to dispose NitroAuth native bridge", it) }
|
|
120
149
|
|
|
121
150
|
val app = appContext as? Application
|
|
122
151
|
lifecycleCallbacks?.let { app?.unregisterActivityLifecycleCallbacks(it) }
|
|
@@ -127,22 +156,22 @@ object AuthAdapter {
|
|
|
127
156
|
isInitialized = false
|
|
128
157
|
}
|
|
129
158
|
|
|
130
|
-
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String
|
|
159
|
+
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>, origin: String = "login") {
|
|
131
160
|
appContext ?: return
|
|
132
161
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
133
|
-
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
162
|
+
nativeOnLoginSuccess(origin, "google", account.email, account.displayName,
|
|
134
163
|
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
135
164
|
scopes.toTypedArray(), expirationTime)
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
fun onSignInError(errorCode: Int, message: String
|
|
167
|
+
fun onSignInError(errorCode: Int, message: String?, origin: String = "login") {
|
|
139
168
|
val mappedError = when (errorCode) {
|
|
140
169
|
12501 -> "cancelled"
|
|
141
170
|
7 -> "network_error"
|
|
142
171
|
8, 10 -> "configuration_error"
|
|
143
172
|
else -> "unknown"
|
|
144
173
|
}
|
|
145
|
-
nativeOnLoginError(mappedError, message)
|
|
174
|
+
nativeOnLoginError(origin, mappedError, message)
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
@JvmStatic
|
|
@@ -159,47 +188,60 @@ object AuthAdapter {
|
|
|
159
188
|
prompt: String? = null
|
|
160
189
|
) {
|
|
161
190
|
if (provider == "apple") {
|
|
162
|
-
nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
191
|
+
nativeOnLoginError("login", "unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
163
192
|
return
|
|
164
193
|
}
|
|
165
194
|
if (provider == "microsoft") {
|
|
166
|
-
loginMicrosoft(context, scopes, loginHint, tenant, prompt)
|
|
195
|
+
loginMicrosoft(context, scopes, loginHint, tenant, prompt, "login")
|
|
167
196
|
return
|
|
168
197
|
}
|
|
169
198
|
if (provider != "google") {
|
|
170
|
-
nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
|
|
199
|
+
nativeOnLoginError("login", "unsupported_provider", "Unsupported provider: $provider")
|
|
171
200
|
return
|
|
172
201
|
}
|
|
173
202
|
|
|
174
203
|
val ctx = appContext ?: context.applicationContext
|
|
175
204
|
val clientId = googleClientId ?: getClientIdFromResources(ctx)
|
|
176
205
|
if (clientId == null) {
|
|
177
|
-
nativeOnLoginError("configuration_error", "Google Client ID is required. Set it in app.json plugins.")
|
|
206
|
+
nativeOnLoginError("login", "configuration_error", "Google Client ID is required. Set it in app.json plugins.")
|
|
178
207
|
return
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
val requestedScopes = scopes?.toList() ?: listOf("email", "profile")
|
|
182
211
|
pendingScopes = requestedScopes
|
|
183
212
|
|
|
184
|
-
if (useLegacyGoogleSignIn) {
|
|
185
|
-
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
|
|
213
|
+
if (useLegacyGoogleSignIn || forceAccountPicker) {
|
|
214
|
+
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker, "login")
|
|
186
215
|
return
|
|
187
216
|
}
|
|
188
|
-
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
|
|
217
|
+
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap, "login")
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
private fun loginMicrosoft(context: Context, scopes: Array<String>?, loginHint: String?, tenant: String?, prompt: String
|
|
220
|
+
private fun loginMicrosoft(context: Context, scopes: Array<String>?, loginHint: String?, tenant: String?, prompt: String?, origin: String = "login") {
|
|
192
221
|
val ctx = appContext ?: context.applicationContext
|
|
193
222
|
val clientId = getMicrosoftClientIdFromResources(ctx)
|
|
194
223
|
if (clientId == null) {
|
|
195
|
-
nativeOnLoginError("configuration_error", "Microsoft Client ID is required. Set it in app.json plugins.")
|
|
224
|
+
nativeOnLoginError(origin, "configuration_error", "Microsoft Client ID is required. Set it in app.json plugins.")
|
|
196
225
|
return
|
|
197
226
|
}
|
|
227
|
+
pendingOrigin = origin
|
|
198
228
|
|
|
199
229
|
val effectiveTenant = tenant ?: getMicrosoftTenantFromResources(ctx) ?: "common"
|
|
200
|
-
val effectiveScopes = scopes?.toList() ?:
|
|
230
|
+
val effectiveScopes = scopes?.toList() ?: defaultMicrosoftScopes
|
|
201
231
|
val effectivePrompt = prompt ?: "select_account"
|
|
202
|
-
|
|
232
|
+
|
|
233
|
+
synchronized(this) {
|
|
234
|
+
if (microsoftAuthInProgress) {
|
|
235
|
+
nativeOnLoginError(
|
|
236
|
+
origin,
|
|
237
|
+
"operation_in_progress",
|
|
238
|
+
"Microsoft authentication already in progress",
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
microsoftAuthInProgress = true
|
|
243
|
+
pendingMicrosoftScopes = effectiveScopes
|
|
244
|
+
}
|
|
203
245
|
|
|
204
246
|
val codeVerifier = generateCodeVerifier()
|
|
205
247
|
val codeChallenge = generateCodeChallenge(codeVerifier)
|
|
@@ -241,7 +283,8 @@ object AuthAdapter {
|
|
|
241
283
|
ctx.startActivity(browserIntent)
|
|
242
284
|
}
|
|
243
285
|
} catch (e: Exception) {
|
|
244
|
-
|
|
286
|
+
clearPkceState()
|
|
287
|
+
nativeOnLoginError(origin, "unknown", e.message)
|
|
245
288
|
}
|
|
246
289
|
}
|
|
247
290
|
|
|
@@ -263,19 +306,20 @@ object AuthAdapter {
|
|
|
263
306
|
val error = uri.getQueryParameter("error")
|
|
264
307
|
val errorDescription = uri.getQueryParameter("error_description")
|
|
265
308
|
|
|
309
|
+
val origin = pendingOrigin
|
|
266
310
|
if (error != null) {
|
|
267
311
|
clearPkceState()
|
|
268
|
-
nativeOnLoginError(error, errorDescription)
|
|
312
|
+
nativeOnLoginError(origin, error, errorDescription)
|
|
269
313
|
return
|
|
270
314
|
}
|
|
271
315
|
if (state != pendingState) {
|
|
272
316
|
clearPkceState()
|
|
273
|
-
nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
|
|
317
|
+
nativeOnLoginError(origin, "invalid_state", "State mismatch - possible CSRF attack")
|
|
274
318
|
return
|
|
275
319
|
}
|
|
276
320
|
if (code == null) {
|
|
277
321
|
clearPkceState()
|
|
278
|
-
nativeOnLoginError("unknown", "No authorization code in response")
|
|
322
|
+
nativeOnLoginError(origin, "unknown", "No authorization code in response")
|
|
279
323
|
return
|
|
280
324
|
}
|
|
281
325
|
exchangeCodeForTokens(code)
|
|
@@ -286,10 +330,11 @@ object AuthAdapter {
|
|
|
286
330
|
val clientId = pendingMicrosoftClientId
|
|
287
331
|
val tenant = pendingMicrosoftTenant
|
|
288
332
|
val verifier = pendingPkceVerifier
|
|
333
|
+
val origin = pendingOrigin
|
|
289
334
|
|
|
290
335
|
if (ctx == null || clientId == null || tenant == null || verifier == null) {
|
|
291
336
|
clearPkceState()
|
|
292
|
-
nativeOnLoginError("invalid_state", "Missing PKCE state for token exchange")
|
|
337
|
+
nativeOnLoginError(origin, "invalid_state", "Missing PKCE state for token exchange")
|
|
293
338
|
return
|
|
294
339
|
}
|
|
295
340
|
|
|
@@ -301,49 +346,55 @@ object AuthAdapter {
|
|
|
301
346
|
try {
|
|
302
347
|
val url = java.net.URL(tokenUrl)
|
|
303
348
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
try {
|
|
350
|
+
connection.connectTimeout = 15_000
|
|
351
|
+
connection.readTimeout = 15_000
|
|
352
|
+
connection.requestMethod = "POST"
|
|
353
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
354
|
+
connection.doOutput = true
|
|
355
|
+
|
|
356
|
+
val postData = buildString {
|
|
357
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
358
|
+
append("&code=${java.net.URLEncoder.encode(code, "UTF-8")}")
|
|
359
|
+
append("&redirect_uri=${java.net.URLEncoder.encode(redirectUri, "UTF-8")}")
|
|
360
|
+
append("&grant_type=authorization_code")
|
|
361
|
+
append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
|
|
362
|
+
}
|
|
363
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
316
364
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
val responseCode = connection.responseCode
|
|
366
|
+
val responseBody = if (responseCode == 200) {
|
|
367
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
368
|
+
} else {
|
|
369
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
370
|
+
}
|
|
323
371
|
|
|
324
|
-
|
|
325
|
-
|
|
372
|
+
withContext(Dispatchers.Main) {
|
|
373
|
+
handleTokenResponse(responseCode, responseBody, origin)
|
|
374
|
+
}
|
|
375
|
+
} finally {
|
|
376
|
+
connection.disconnect()
|
|
326
377
|
}
|
|
327
378
|
} catch (e: Exception) {
|
|
328
379
|
withContext(Dispatchers.Main) {
|
|
329
380
|
clearPkceState()
|
|
330
|
-
nativeOnLoginError("network_error", e.message)
|
|
381
|
+
nativeOnLoginError(origin, "network_error", e.message)
|
|
331
382
|
}
|
|
332
383
|
}
|
|
333
384
|
}
|
|
334
385
|
}
|
|
335
386
|
|
|
336
|
-
private fun handleTokenResponse(responseCode: Int, responseBody: String) {
|
|
387
|
+
private fun handleTokenResponse(responseCode: Int, responseBody: String, origin: String) {
|
|
337
388
|
if (responseCode != 200) {
|
|
338
389
|
try {
|
|
339
390
|
val json = JSONObject(responseBody)
|
|
340
391
|
val error = json.optString("error", "token_error")
|
|
341
392
|
val desc = json.optString("error_description", "Failed to exchange code for tokens")
|
|
342
393
|
clearPkceState()
|
|
343
|
-
nativeOnLoginError(error, desc)
|
|
394
|
+
nativeOnLoginError(origin, error, desc)
|
|
344
395
|
} catch (e: Exception) {
|
|
345
396
|
clearPkceState()
|
|
346
|
-
nativeOnLoginError("token_error", "Failed to exchange code for tokens")
|
|
397
|
+
nativeOnLoginError(origin, "token_error", "Failed to exchange code for tokens")
|
|
347
398
|
}
|
|
348
399
|
return
|
|
349
400
|
}
|
|
@@ -358,43 +409,45 @@ object AuthAdapter {
|
|
|
358
409
|
|
|
359
410
|
if (idToken.isEmpty()) {
|
|
360
411
|
clearPkceState()
|
|
361
|
-
nativeOnLoginError("no_id_token", "No id_token in token response")
|
|
412
|
+
nativeOnLoginError(origin, "no_id_token", "No id_token in token response")
|
|
362
413
|
return
|
|
363
414
|
}
|
|
364
415
|
|
|
365
416
|
val claims = decodeJwt(idToken)
|
|
366
417
|
if (claims["nonce"] != pendingNonce) {
|
|
367
418
|
clearPkceState()
|
|
368
|
-
nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
419
|
+
nativeOnLoginError(origin, "invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
369
420
|
return
|
|
370
421
|
}
|
|
371
422
|
|
|
372
423
|
val email = claims["preferred_username"] ?: claims["email"]
|
|
373
424
|
val name = claims["name"]
|
|
425
|
+
val grantedScopes = pendingMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
|
|
374
426
|
|
|
375
427
|
if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
|
|
376
|
-
inMemoryMicrosoftScopes =
|
|
377
|
-
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
378
|
-
}
|
|
428
|
+
inMemoryMicrosoftScopes = grantedScopes
|
|
379
429
|
|
|
380
430
|
clearPkceState()
|
|
381
431
|
nativeOnLoginSuccess(
|
|
382
|
-
"microsoft", email, name, null, idToken, accessToken, null,
|
|
383
|
-
|
|
432
|
+
origin, "microsoft", email, name, null, idToken, accessToken, null,
|
|
433
|
+
grantedScopes.toTypedArray(), expirationTime
|
|
384
434
|
)
|
|
385
435
|
} catch (e: Exception) {
|
|
386
436
|
clearPkceState()
|
|
387
|
-
nativeOnLoginError("parse_error", e.message)
|
|
437
|
+
nativeOnLoginError(origin, "parse_error", e.message)
|
|
388
438
|
}
|
|
389
439
|
}
|
|
390
440
|
|
|
391
441
|
private fun clearPkceState() {
|
|
442
|
+
pendingOrigin = "login"
|
|
392
443
|
pendingPkceVerifier = null
|
|
393
444
|
pendingState = null
|
|
394
445
|
pendingNonce = null
|
|
395
446
|
pendingMicrosoftTenant = null
|
|
396
447
|
pendingMicrosoftClientId = null
|
|
397
448
|
pendingMicrosoftB2cDomain = null
|
|
449
|
+
pendingMicrosoftScopes = emptyList()
|
|
450
|
+
microsoftAuthInProgress = false
|
|
398
451
|
}
|
|
399
452
|
|
|
400
453
|
private fun decodeJwt(token: String): Map<String, String> {
|
|
@@ -410,6 +463,7 @@ object AuthAdapter {
|
|
|
410
463
|
}
|
|
411
464
|
result
|
|
412
465
|
} catch (e: Exception) {
|
|
466
|
+
Log.w(TAG, "Failed to decode JWT: ${e.message}")
|
|
413
467
|
emptyMap()
|
|
414
468
|
}
|
|
415
469
|
}
|
|
@@ -452,12 +506,13 @@ object AuthAdapter {
|
|
|
452
506
|
scopes: List<String>,
|
|
453
507
|
loginHint: String?,
|
|
454
508
|
forceAccountPicker: Boolean,
|
|
455
|
-
useOneTap: Boolean
|
|
509
|
+
useOneTap: Boolean,
|
|
510
|
+
origin: String = "login"
|
|
456
511
|
) {
|
|
457
512
|
val activity = currentActivity ?: context as? Activity
|
|
458
513
|
if (activity == null) {
|
|
459
514
|
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
460
|
-
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
515
|
+
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker, origin)
|
|
461
516
|
}
|
|
462
517
|
|
|
463
518
|
val credentialManager = CredentialManager.create(activity)
|
|
@@ -474,31 +529,31 @@ object AuthAdapter {
|
|
|
474
529
|
moduleScope.launch(Dispatchers.Main) {
|
|
475
530
|
try {
|
|
476
531
|
val result = credentialManager.getCredential(context = activity, request = request)
|
|
477
|
-
handleCredentialResponse(result, scopes)
|
|
532
|
+
handleCredentialResponse(result, scopes, origin)
|
|
478
533
|
} catch (e: Exception) {
|
|
479
534
|
Log.w(TAG, "One-Tap failed, falling back to legacy: ${e.message}")
|
|
480
|
-
loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
535
|
+
loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker, origin)
|
|
481
536
|
}
|
|
482
537
|
}
|
|
483
538
|
}
|
|
484
539
|
|
|
485
|
-
@Suppress("DEPRECATION")
|
|
486
540
|
private fun loginLegacy(
|
|
487
541
|
context: Context,
|
|
488
542
|
clientId: String,
|
|
489
543
|
scopes: List<String>,
|
|
490
544
|
loginHint: String?,
|
|
491
|
-
forceAccountPicker: Boolean
|
|
545
|
+
forceAccountPicker: Boolean,
|
|
546
|
+
origin: String = "login"
|
|
492
547
|
) {
|
|
493
548
|
val ctx = appContext ?: context.applicationContext
|
|
494
549
|
val intent = GoogleSignInActivity.createIntent(
|
|
495
|
-
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
|
|
550
|
+
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker, origin
|
|
496
551
|
)
|
|
497
552
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
498
553
|
ctx.startActivity(intent)
|
|
499
554
|
}
|
|
500
555
|
|
|
501
|
-
private fun handleCredentialResponse(response: GetCredentialResponse, scopes: List<String
|
|
556
|
+
private fun handleCredentialResponse(response: GetCredentialResponse, scopes: List<String>, origin: String) {
|
|
502
557
|
val credential = response.credential
|
|
503
558
|
val googleIdTokenCredential = try {
|
|
504
559
|
if (credential is GoogleIdTokenCredential) {
|
|
@@ -516,7 +571,7 @@ object AuthAdapter {
|
|
|
516
571
|
if (googleIdTokenCredential != null) {
|
|
517
572
|
val expirationTime = getJwtExpirationTimeMs(googleIdTokenCredential.idToken)
|
|
518
573
|
nativeOnLoginSuccess(
|
|
519
|
-
"google",
|
|
574
|
+
origin, "google",
|
|
520
575
|
googleIdTokenCredential.id,
|
|
521
576
|
googleIdTokenCredential.displayName,
|
|
522
577
|
googleIdTokenCredential.profilePictureUri?.toString(),
|
|
@@ -527,13 +582,12 @@ object AuthAdapter {
|
|
|
527
582
|
)
|
|
528
583
|
} else {
|
|
529
584
|
Log.w(TAG, "Unsupported credential type: ${credential.type}")
|
|
530
|
-
nativeOnLoginError("unknown", "Unsupported credential type: ${credential.type}")
|
|
585
|
+
nativeOnLoginError(origin, "unknown", "Unsupported credential type: ${credential.type}")
|
|
531
586
|
}
|
|
532
587
|
}
|
|
533
588
|
|
|
534
589
|
// requestScopesSync uses the legacy GoogleSignIn API to check the last signed-in account
|
|
535
590
|
// because Credential Manager has no equivalent for querying existing account state.
|
|
536
|
-
@Suppress("DEPRECATION")
|
|
537
591
|
@JvmStatic
|
|
538
592
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
539
593
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -541,31 +595,30 @@ object AuthAdapter {
|
|
|
541
595
|
if (account != null) {
|
|
542
596
|
val newScopes = scopes.map { Scope(it) }
|
|
543
597
|
if (GoogleSignIn.hasPermissions(account, *newScopes.toTypedArray())) {
|
|
544
|
-
onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct())
|
|
598
|
+
onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct(), "scopes")
|
|
545
599
|
return
|
|
546
600
|
}
|
|
547
601
|
val clientId = getClientIdFromResources(ctx)
|
|
548
602
|
if (clientId == null) {
|
|
549
|
-
nativeOnLoginError("configuration_error", "Google Client ID not configured")
|
|
603
|
+
nativeOnLoginError("scopes", "configuration_error", "Google Client ID not configured")
|
|
550
604
|
return
|
|
551
605
|
}
|
|
552
606
|
val allScopes = (pendingScopes + scopes.toList()).distinct()
|
|
553
|
-
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email)
|
|
607
|
+
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email, origin = "scopes")
|
|
554
608
|
ctx.startActivity(intent)
|
|
555
609
|
return
|
|
556
610
|
}
|
|
557
611
|
if (inMemoryMicrosoftRefreshToken != null) {
|
|
558
612
|
val mergedScopes = (inMemoryMicrosoftScopes + scopes.toList()).distinct()
|
|
559
613
|
val tenant = getMicrosoftTenantFromResources(ctx)
|
|
560
|
-
loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null)
|
|
614
|
+
loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null, "scopes")
|
|
561
615
|
return
|
|
562
616
|
}
|
|
563
|
-
nativeOnLoginError("
|
|
617
|
+
nativeOnLoginError("scopes", "not_signed_in", "No user logged in")
|
|
564
618
|
}
|
|
565
619
|
|
|
566
620
|
// refreshTokenSync uses the legacy silentSignIn because AuthorizationClient (the recommended
|
|
567
621
|
// replacement) requires an Activity context which is not always available at refresh time.
|
|
568
|
-
@Suppress("DEPRECATION")
|
|
569
622
|
@JvmStatic
|
|
570
623
|
fun refreshTokenSync(context: Context) {
|
|
571
624
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -599,33 +652,40 @@ object AuthAdapter {
|
|
|
599
652
|
refreshMicrosoftTokenForRefresh(ctx, refreshToken)
|
|
600
653
|
return
|
|
601
654
|
}
|
|
602
|
-
nativeOnRefreshError("
|
|
655
|
+
nativeOnRefreshError("not_signed_in", "No user logged in")
|
|
603
656
|
}
|
|
604
657
|
|
|
605
658
|
@JvmStatic
|
|
606
659
|
fun hasPlayServices(context: Context): Boolean {
|
|
607
|
-
val ctx = context.applicationContext ?:
|
|
660
|
+
val ctx = appContext ?: context.applicationContext ?: return false
|
|
608
661
|
return GoogleApiAvailability.getInstance()
|
|
609
662
|
.isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
|
|
610
663
|
}
|
|
611
664
|
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
@Suppress("DEPRECATION")
|
|
665
|
+
// revokeAccessSync uses the legacy GoogleSignIn client because Credential Manager has no
|
|
666
|
+
// equivalent revoke API for the Google ID token flow.
|
|
615
667
|
@JvmStatic
|
|
616
668
|
fun logoutSync(context: Context) {
|
|
617
669
|
val ctx = appContext ?: context.applicationContext
|
|
670
|
+
// Clear Credential Manager state (covers One-Tap / passkey credentials).
|
|
671
|
+
moduleScope.launch {
|
|
672
|
+
try {
|
|
673
|
+
CredentialManager.create(ctx).clearCredentialState(ClearCredentialStateRequest())
|
|
674
|
+
} catch (e: Exception) {
|
|
675
|
+
Log.w(TAG, "clearCredentialState failed: ${e.message}")
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Also clear legacy GMS sign-in state so getLastSignedInAccount returns null.
|
|
618
679
|
val clientId = getClientIdFromResources(ctx)
|
|
619
680
|
if (clientId != null) {
|
|
620
681
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
621
|
-
.requestIdToken(clientId).
|
|
682
|
+
.requestIdToken(clientId).requestEmail().build()
|
|
622
683
|
GoogleSignIn.getClient(ctx, gso).signOut()
|
|
623
684
|
}
|
|
624
685
|
inMemoryMicrosoftRefreshToken = null
|
|
625
|
-
inMemoryMicrosoftScopes =
|
|
686
|
+
inMemoryMicrosoftScopes = defaultMicrosoftScopes
|
|
626
687
|
}
|
|
627
688
|
|
|
628
|
-
@Suppress("DEPRECATION")
|
|
629
689
|
@JvmStatic
|
|
630
690
|
fun revokeAccessSync(context: Context) {
|
|
631
691
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -636,7 +696,7 @@ object AuthAdapter {
|
|
|
636
696
|
GoogleSignIn.getClient(ctx, gso).revokeAccess()
|
|
637
697
|
}
|
|
638
698
|
inMemoryMicrosoftRefreshToken = null
|
|
639
|
-
inMemoryMicrosoftScopes =
|
|
699
|
+
inMemoryMicrosoftScopes = defaultMicrosoftScopes
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
private fun getClientIdFromResources(context: Context): String? {
|
|
@@ -646,12 +706,12 @@ object AuthAdapter {
|
|
|
646
706
|
|
|
647
707
|
@JvmStatic
|
|
648
708
|
fun restoreSession(context: Context) {
|
|
649
|
-
val ctx = context.applicationContext ?:
|
|
709
|
+
val ctx = appContext ?: context.applicationContext ?: return
|
|
650
710
|
@Suppress("DEPRECATION")
|
|
651
711
|
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
652
712
|
if (account != null) {
|
|
653
713
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
654
|
-
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
714
|
+
nativeOnLoginSuccess("silent", "google", account.email, account.displayName,
|
|
655
715
|
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
656
716
|
account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
|
|
657
717
|
} else {
|
|
@@ -659,7 +719,7 @@ object AuthAdapter {
|
|
|
659
719
|
if (refreshToken != null) {
|
|
660
720
|
refreshMicrosoftToken(ctx, refreshToken)
|
|
661
721
|
} else {
|
|
662
|
-
nativeOnLoginError("
|
|
722
|
+
nativeOnLoginError("silent", "not_signed_in", "No session")
|
|
663
723
|
}
|
|
664
724
|
}
|
|
665
725
|
}
|
|
@@ -668,9 +728,10 @@ object AuthAdapter {
|
|
|
668
728
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
669
729
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
670
730
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
731
|
+
val effectiveScopes = inMemoryMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
|
|
671
732
|
|
|
672
733
|
if (clientId == null) {
|
|
673
|
-
nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
|
|
734
|
+
nativeOnLoginError("silent", "configuration_error", "Microsoft Client ID is required for refresh")
|
|
674
735
|
return
|
|
675
736
|
}
|
|
676
737
|
|
|
@@ -681,50 +742,65 @@ object AuthAdapter {
|
|
|
681
742
|
try {
|
|
682
743
|
val url = java.net.URL(tokenUrl)
|
|
683
744
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
745
|
+
try {
|
|
746
|
+
connection.connectTimeout = 15_000
|
|
747
|
+
connection.readTimeout = 15_000
|
|
748
|
+
connection.requestMethod = "POST"
|
|
749
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
750
|
+
connection.doOutput = true
|
|
751
|
+
|
|
752
|
+
val postData = buildString {
|
|
753
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
754
|
+
append("&grant_type=refresh_token")
|
|
755
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
756
|
+
}
|
|
757
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
694
758
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
759
|
+
val responseCode = connection.responseCode
|
|
760
|
+
val responseBody = if (responseCode == 200) {
|
|
761
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
762
|
+
} else {
|
|
763
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
764
|
+
}
|
|
701
765
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
766
|
+
withContext(Dispatchers.Main) {
|
|
767
|
+
if (responseCode == 200) {
|
|
768
|
+
val json = JSONObject(responseBody)
|
|
769
|
+
val newIdToken = json.optString("id_token")
|
|
770
|
+
val newAccessToken = json.optString("access_token")
|
|
771
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
772
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
773
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
774
|
+
val claims = decodeJwt(newIdToken)
|
|
775
|
+
|
|
776
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
777
|
+
inMemoryMicrosoftScopes = effectiveScopes
|
|
778
|
+
|
|
779
|
+
nativeOnLoginSuccess("silent", "microsoft",
|
|
780
|
+
claims["preferred_username"] ?: claims["email"],
|
|
781
|
+
claims["name"], null,
|
|
782
|
+
newIdToken, newAccessToken, null, effectiveScopes.toTypedArray(), expirationTime)
|
|
783
|
+
} else {
|
|
784
|
+
if (responseCode in 400..499) {
|
|
785
|
+
inMemoryMicrosoftRefreshToken = null // Token is invalid, clear it
|
|
786
|
+
}
|
|
787
|
+
val mappedError = try {
|
|
788
|
+
val json = org.json.JSONObject(responseBody)
|
|
789
|
+
val errorCode = json.optString("error", "token_error")
|
|
790
|
+
val errorDesc = json.optString("error_description", "Token refresh failed")
|
|
791
|
+
Pair(errorCode, errorDesc)
|
|
792
|
+
} catch (e: Exception) {
|
|
793
|
+
Pair("token_error", "Token refresh failed")
|
|
794
|
+
}
|
|
795
|
+
nativeOnLoginError("silent", mappedError.first, mappedError.second)
|
|
715
796
|
}
|
|
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
797
|
}
|
|
798
|
+
} finally {
|
|
799
|
+
connection.disconnect()
|
|
724
800
|
}
|
|
725
801
|
} catch (e: Exception) {
|
|
726
802
|
withContext(Dispatchers.Main) {
|
|
727
|
-
nativeOnLoginError("network_error", e.message)
|
|
803
|
+
nativeOnLoginError("silent", "network_error", e.message)
|
|
728
804
|
}
|
|
729
805
|
}
|
|
730
806
|
}
|
|
@@ -734,6 +810,7 @@ object AuthAdapter {
|
|
|
734
810
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
735
811
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
736
812
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
813
|
+
val effectiveScopes = inMemoryMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
|
|
737
814
|
|
|
738
815
|
if (clientId == null) {
|
|
739
816
|
nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
|
|
@@ -747,46 +824,62 @@ object AuthAdapter {
|
|
|
747
824
|
try {
|
|
748
825
|
val url = java.net.URL(tokenUrl)
|
|
749
826
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
827
|
+
try {
|
|
828
|
+
connection.connectTimeout = 15_000
|
|
829
|
+
connection.readTimeout = 15_000
|
|
830
|
+
connection.requestMethod = "POST"
|
|
831
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
832
|
+
connection.doOutput = true
|
|
833
|
+
|
|
834
|
+
val postData = buildString {
|
|
835
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
836
|
+
append("&grant_type=refresh_token")
|
|
837
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
838
|
+
}
|
|
839
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
760
840
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
841
|
+
val responseCode = connection.responseCode
|
|
842
|
+
val responseBody = if (responseCode == 200) {
|
|
843
|
+
connection.inputStream.bufferedReader().use { it.readText() }
|
|
844
|
+
} else {
|
|
845
|
+
connection.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
|
|
846
|
+
}
|
|
767
847
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
848
|
+
withContext(Dispatchers.Main) {
|
|
849
|
+
if (responseCode == 200) {
|
|
850
|
+
val json = JSONObject(responseBody)
|
|
851
|
+
val newIdToken = json.optString("id_token")
|
|
852
|
+
val newAccessToken = json.optString("access_token")
|
|
853
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
854
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
855
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
856
|
+
|
|
857
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
858
|
+
inMemoryMicrosoftScopes = effectiveScopes
|
|
859
|
+
|
|
860
|
+
nativeOnRefreshSuccess(
|
|
861
|
+
newIdToken.ifEmpty { null },
|
|
862
|
+
newAccessToken.ifEmpty { null },
|
|
863
|
+
expirationTime
|
|
864
|
+
)
|
|
865
|
+
} else {
|
|
866
|
+
if (responseCode in 400..499) {
|
|
867
|
+
inMemoryMicrosoftRefreshToken = null
|
|
868
|
+
}
|
|
869
|
+
val errorBody = responseBody
|
|
870
|
+
val mappedError = try {
|
|
871
|
+
val json = org.json.JSONObject(errorBody)
|
|
872
|
+
val errorCode = json.optString("error", "token_error")
|
|
873
|
+
val errorDesc = json.optString("error_description", "Token refresh failed")
|
|
874
|
+
Pair(errorCode, errorDesc)
|
|
875
|
+
} catch (e: Exception) {
|
|
876
|
+
Pair("token_error", "Token refresh failed")
|
|
877
|
+
}
|
|
878
|
+
nativeOnRefreshError(mappedError.first, mappedError.second)
|
|
780
879
|
}
|
|
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
880
|
}
|
|
881
|
+
} finally {
|
|
882
|
+
connection.disconnect()
|
|
790
883
|
}
|
|
791
884
|
} catch (e: Exception) {
|
|
792
885
|
withContext(Dispatchers.Main) {
|