react-native-nitro-auth 0.4.0 → 0.5.1
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 +288 -28
- package/android/build.gradle +8 -2
- package/android/gradle.properties +2 -2
- package/android/src/main/cpp/JniOnLoad.cpp +1 -0
- package/android/src/main/cpp/PlatformAuth+Android.cpp +37 -4
- package/android/src/main/java/com/auth/AuthAdapter.kt +626 -78
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +3 -4
- package/android/src/main/java/com/auth/MicrosoftAuthActivity.kt +25 -0
- package/android/src/main/java/com/auth/NitroAuthPackage.kt +2 -0
- package/app.plugin.js +113 -3
- package/cpp/AuthCache.cpp +72 -19
- package/ios/AuthAdapter.swift +457 -52
- package/ios/KeychainStore.swift +43 -0
- package/ios/PlatformAuth+iOS.mm +29 -3
- package/lib/commonjs/Auth.web.js +246 -7
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +7 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +0 -8
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +12 -2
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +12 -2
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/logger.js +1 -1
- package/lib/commonjs/utils/logger.js.map +1 -1
- package/lib/module/Auth.web.js +246 -7
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +0 -8
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +12 -2
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +12 -2
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/logger.js +1 -1
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/commonjs/Auth.nitro.d.ts +9 -2
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +7 -0
- 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/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +2 -6
- 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 +12 -8
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/logger.d.ts +5 -5
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +9 -2
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +7 -0
- 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/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +2 -6
- 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 +12 -8
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/logger.d.ts +5 -5
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/AuthProvider.hpp +4 -0
- package/nitrogen/generated/shared/c++/LoginOptions.hpp +17 -3
- package/nitrogen/generated/shared/c++/MicrosoftPrompt.hpp +84 -0
- package/package.json +6 -4
- package/react-native-nitro-auth.podspec +4 -2
- package/src/Auth.nitro.ts +15 -1
- package/src/Auth.web.ts +350 -7
- package/src/index.ts +1 -1
- package/src/service.ts +4 -3
- package/src/service.web.ts +3 -12
- package/src/ui/social-button.tsx +10 -2
- package/src/ui/social-button.web.tsx +10 -2
- package/src/use-auth.ts +12 -1
- package/src/utils/logger.ts +5 -5
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
@file:Suppress("DEPRECATION")
|
|
2
|
+
|
|
1
3
|
package com.auth
|
|
2
4
|
|
|
3
5
|
import android.content.Context
|
|
6
|
+
import android.content.SharedPreferences
|
|
4
7
|
import android.os.Bundle
|
|
8
|
+
import android.os.Build
|
|
5
9
|
import android.util.Log
|
|
6
10
|
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
|
7
11
|
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
|
@@ -20,15 +24,74 @@ import kotlinx.coroutines.Dispatchers
|
|
|
20
24
|
import kotlinx.coroutines.launch
|
|
21
25
|
import android.app.Activity
|
|
22
26
|
import android.app.Application
|
|
27
|
+
import android.content.Intent
|
|
28
|
+
import android.net.Uri
|
|
29
|
+
import androidx.browser.customtabs.CustomTabsIntent
|
|
30
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
31
|
+
import androidx.security.crypto.MasterKeys
|
|
32
|
+
import java.util.UUID
|
|
33
|
+
import org.json.JSONArray
|
|
34
|
+
import org.json.JSONObject
|
|
35
|
+
import android.util.Base64
|
|
23
36
|
|
|
24
37
|
object AuthAdapter {
|
|
25
38
|
private const val TAG = "AuthAdapter"
|
|
26
39
|
private const val PREF_NAME = "nitro_auth"
|
|
40
|
+
private const val SECURE_PREF_NAME = "nitro_auth_secure"
|
|
27
41
|
|
|
28
42
|
private var appContext: Context? = null
|
|
29
43
|
private var currentActivity: Activity? = null
|
|
30
44
|
private var googleSignInClient: GoogleSignInClient? = null
|
|
31
45
|
private var pendingScopes: List<String> = emptyList()
|
|
46
|
+
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
47
|
+
|
|
48
|
+
private var pendingPkceVerifier: String? = null
|
|
49
|
+
private var pendingState: String? = null
|
|
50
|
+
private var pendingNonce: String? = null
|
|
51
|
+
private var pendingMicrosoftTenant: String? = null
|
|
52
|
+
private var pendingMicrosoftClientId: String? = null
|
|
53
|
+
private var pendingMicrosoftB2cDomain: String? = null
|
|
54
|
+
|
|
55
|
+
private fun getPrefs(context: Context): SharedPreferences {
|
|
56
|
+
return try {
|
|
57
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
58
|
+
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
|
59
|
+
val securePrefs = EncryptedSharedPreferences.create(
|
|
60
|
+
SECURE_PREF_NAME,
|
|
61
|
+
masterKeyAlias,
|
|
62
|
+
context,
|
|
63
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
64
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
65
|
+
)
|
|
66
|
+
migrateLegacyPrefsIfNeeded(context, securePrefs)
|
|
67
|
+
securePrefs
|
|
68
|
+
} else {
|
|
69
|
+
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
|
70
|
+
}
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
Log.w(TAG, "Failed to initialize encrypted storage, falling back to SharedPreferences", e)
|
|
73
|
+
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private fun migrateLegacyPrefsIfNeeded(context: Context, securePrefs: SharedPreferences) {
|
|
78
|
+
val legacyPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
|
79
|
+
if (legacyPrefs.all.isEmpty() || securePrefs.all.isNotEmpty()) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
val editor = securePrefs.edit()
|
|
83
|
+
for ((key, value) in legacyPrefs.all) {
|
|
84
|
+
when (value) {
|
|
85
|
+
is String -> editor.putString(key, value)
|
|
86
|
+
is Boolean -> editor.putBoolean(key, value)
|
|
87
|
+
is Int -> editor.putInt(key, value)
|
|
88
|
+
is Long -> editor.putLong(key, value)
|
|
89
|
+
is Float -> editor.putFloat(key, value)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
editor.apply()
|
|
93
|
+
legacyPrefs.edit().clear().apply()
|
|
94
|
+
}
|
|
32
95
|
|
|
33
96
|
@JvmStatic
|
|
34
97
|
private external fun nativeInitialize(context: Context)
|
|
@@ -96,12 +159,28 @@ object AuthAdapter {
|
|
|
96
159
|
}
|
|
97
160
|
|
|
98
161
|
@JvmStatic
|
|
99
|
-
fun loginSync(
|
|
162
|
+
fun loginSync(
|
|
163
|
+
context: Context,
|
|
164
|
+
provider: String,
|
|
165
|
+
googleClientId: String?,
|
|
166
|
+
scopes: Array<String>?,
|
|
167
|
+
loginHint: String?,
|
|
168
|
+
useOneTap: Boolean,
|
|
169
|
+
forceAccountPicker: Boolean = false,
|
|
170
|
+
useLegacyGoogleSignIn: Boolean = false,
|
|
171
|
+
tenant: String? = null,
|
|
172
|
+
prompt: String? = null
|
|
173
|
+
) {
|
|
100
174
|
if (provider == "apple") {
|
|
101
175
|
nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
102
176
|
return
|
|
103
177
|
}
|
|
104
178
|
|
|
179
|
+
if (provider == "microsoft") {
|
|
180
|
+
loginMicrosoft(context, scopes, loginHint, tenant, prompt)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
105
184
|
if (provider != "google") {
|
|
106
185
|
nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
|
|
107
186
|
return
|
|
@@ -117,27 +196,311 @@ object AuthAdapter {
|
|
|
117
196
|
val requestedScopes = scopes?.toList() ?: listOf("email", "profile")
|
|
118
197
|
pendingScopes = requestedScopes
|
|
119
198
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
199
|
+
if (useLegacyGoogleSignIn) {
|
|
200
|
+
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private fun loginMicrosoft(context: Context, scopes: Array<String>?, loginHint: String?, tenant: String?, prompt: String?) {
|
|
208
|
+
val ctx = appContext ?: context.applicationContext
|
|
209
|
+
val clientId = getMicrosoftClientIdFromResources(ctx)
|
|
210
|
+
if (clientId == null) {
|
|
211
|
+
nativeOnLoginError("configuration_error", "Microsoft Client ID is required. Set it in app.json plugins.")
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
val effectiveTenant = tenant ?: getMicrosoftTenantFromResources(ctx) ?: "common"
|
|
216
|
+
val effectiveScopes = scopes?.toList() ?: listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
217
|
+
val effectivePrompt = prompt ?: "select_account"
|
|
218
|
+
pendingMicrosoftScopes = effectiveScopes
|
|
219
|
+
|
|
220
|
+
val codeVerifier = generateCodeVerifier()
|
|
221
|
+
val codeChallenge = generateCodeChallenge(codeVerifier)
|
|
222
|
+
val state = UUID.randomUUID().toString()
|
|
223
|
+
val nonce = UUID.randomUUID().toString()
|
|
224
|
+
pendingPkceVerifier = codeVerifier
|
|
225
|
+
pendingState = state
|
|
226
|
+
pendingNonce = nonce
|
|
227
|
+
pendingMicrosoftTenant = effectiveTenant
|
|
228
|
+
pendingMicrosoftClientId = clientId
|
|
229
|
+
|
|
230
|
+
val b2cDomain = getMicrosoftB2cDomainFromResources(ctx)
|
|
231
|
+
pendingMicrosoftB2cDomain = b2cDomain
|
|
232
|
+
val authBaseUrl = getMicrosoftAuthBaseUrl(effectiveTenant, b2cDomain)
|
|
233
|
+
val redirectUri = "msauth://${ctx.packageName}/${clientId}"
|
|
234
|
+
|
|
235
|
+
val authUrl = Uri.parse("${authBaseUrl}oauth2/v2.0/authorize").buildUpon()
|
|
236
|
+
.appendQueryParameter("client_id", clientId)
|
|
237
|
+
.appendQueryParameter("redirect_uri", redirectUri)
|
|
238
|
+
.appendQueryParameter("response_type", "code")
|
|
239
|
+
.appendQueryParameter("response_mode", "query")
|
|
240
|
+
.appendQueryParameter("scope", effectiveScopes.joinToString(" "))
|
|
241
|
+
.appendQueryParameter("state", state)
|
|
242
|
+
.appendQueryParameter("nonce", nonce)
|
|
243
|
+
.appendQueryParameter("code_challenge", codeChallenge)
|
|
244
|
+
.appendQueryParameter("code_challenge_method", "S256")
|
|
245
|
+
.appendQueryParameter("prompt", effectivePrompt)
|
|
246
|
+
.apply { if (loginHint != null) appendQueryParameter("login_hint", loginHint) }
|
|
247
|
+
.build()
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
val activity = currentActivity
|
|
251
|
+
if (activity != null) {
|
|
252
|
+
val customTabsIntent = CustomTabsIntent.Builder().build()
|
|
253
|
+
customTabsIntent.launchUrl(activity, authUrl)
|
|
254
|
+
} else {
|
|
255
|
+
val browserIntent = Intent(Intent.ACTION_VIEW, authUrl)
|
|
256
|
+
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
257
|
+
ctx.startActivity(browserIntent)
|
|
258
|
+
}
|
|
259
|
+
} catch (e: Exception) {
|
|
260
|
+
nativeOnLoginError("unknown", e.message)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun generateCodeVerifier(): String {
|
|
265
|
+
val bytes = ByteArray(32)
|
|
266
|
+
java.security.SecureRandom().nextBytes(bytes)
|
|
267
|
+
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun generateCodeChallenge(verifier: String): String {
|
|
271
|
+
val bytes = java.security.MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
|
|
272
|
+
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@JvmStatic
|
|
276
|
+
fun handleMicrosoftRedirect(uri: Uri) {
|
|
277
|
+
val code = uri.getQueryParameter("code")
|
|
278
|
+
val state = uri.getQueryParameter("state")
|
|
279
|
+
val error = uri.getQueryParameter("error")
|
|
280
|
+
val errorDescription = uri.getQueryParameter("error_description")
|
|
281
|
+
|
|
282
|
+
if (error != null) {
|
|
283
|
+
clearPkceState()
|
|
284
|
+
nativeOnLoginError(error, errorDescription)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (state != pendingState) {
|
|
289
|
+
clearPkceState()
|
|
290
|
+
nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (code == null) {
|
|
295
|
+
clearPkceState()
|
|
296
|
+
nativeOnLoginError("unknown", "No authorization code in response")
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
exchangeCodeForTokens(code)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private fun exchangeCodeForTokens(code: String) {
|
|
304
|
+
val ctx = appContext
|
|
305
|
+
val clientId = pendingMicrosoftClientId
|
|
306
|
+
val tenant = pendingMicrosoftTenant
|
|
307
|
+
val verifier = pendingPkceVerifier
|
|
308
|
+
|
|
309
|
+
if (ctx == null || clientId == null || tenant == null || verifier == null) {
|
|
310
|
+
clearPkceState()
|
|
311
|
+
nativeOnLoginError("invalid_state", "Missing PKCE state for token exchange")
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
val redirectUri = "msauth://${ctx.packageName}/${clientId}"
|
|
316
|
+
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, pendingMicrosoftB2cDomain)
|
|
317
|
+
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
318
|
+
|
|
319
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
320
|
+
try {
|
|
321
|
+
val url = java.net.URL(tokenUrl)
|
|
322
|
+
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
323
|
+
connection.requestMethod = "POST"
|
|
324
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
325
|
+
connection.doOutput = true
|
|
326
|
+
|
|
327
|
+
val postData = buildString {
|
|
328
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
329
|
+
append("&code=${java.net.URLEncoder.encode(code, "UTF-8")}")
|
|
330
|
+
append("&redirect_uri=${java.net.URLEncoder.encode(redirectUri, "UTF-8")}")
|
|
331
|
+
append("&grant_type=authorization_code")
|
|
332
|
+
append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
336
|
+
|
|
337
|
+
val responseCode = connection.responseCode
|
|
338
|
+
val responseBody = if (responseCode == 200) {
|
|
339
|
+
connection.inputStream.bufferedReader().readText()
|
|
340
|
+
} else {
|
|
341
|
+
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
345
|
+
handleTokenResponse(responseCode, responseBody)
|
|
346
|
+
}
|
|
347
|
+
} catch (e: Exception) {
|
|
348
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
349
|
+
clearPkceState()
|
|
350
|
+
nativeOnLoginError("network_error", e.message)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun handleTokenResponse(responseCode: Int, responseBody: String) {
|
|
357
|
+
if (responseCode != 200) {
|
|
358
|
+
try {
|
|
359
|
+
val json = JSONObject(responseBody)
|
|
360
|
+
val error = json.optString("error", "token_error")
|
|
361
|
+
val desc = json.optString("error_description", "Failed to exchange code for tokens")
|
|
362
|
+
clearPkceState()
|
|
363
|
+
nativeOnLoginError(error, desc)
|
|
364
|
+
} catch (e: Exception) {
|
|
365
|
+
clearPkceState()
|
|
366
|
+
nativeOnLoginError("token_error", "Failed to exchange code for tokens")
|
|
367
|
+
}
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
val json = JSONObject(responseBody)
|
|
373
|
+
val idToken = json.optString("id_token")
|
|
374
|
+
val accessToken = json.optString("access_token")
|
|
375
|
+
val refreshToken = json.optString("refresh_token")
|
|
376
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
377
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
378
|
+
|
|
379
|
+
if (idToken.isEmpty()) {
|
|
380
|
+
clearPkceState()
|
|
381
|
+
nativeOnLoginError("no_id_token", "No id_token in token response")
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
val claims = decodeJwt(idToken)
|
|
386
|
+
val tokenNonce = claims["nonce"]
|
|
387
|
+
if (tokenNonce != pendingNonce) {
|
|
388
|
+
clearPkceState()
|
|
389
|
+
nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
val email = claims["preferred_username"] ?: claims["email"]
|
|
394
|
+
val name = claims["name"]
|
|
395
|
+
|
|
396
|
+
val ctx = appContext
|
|
397
|
+
if (ctx != null) {
|
|
398
|
+
saveUser(ctx, "microsoft", email, name, null, idToken, refreshToken, pendingMicrosoftScopes)
|
|
399
|
+
if (refreshToken.isNotEmpty()) {
|
|
400
|
+
saveMicrosoftRefreshToken(ctx, refreshToken)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
clearPkceState()
|
|
405
|
+
nativeOnLoginSuccess(
|
|
406
|
+
"microsoft",
|
|
407
|
+
email,
|
|
408
|
+
name,
|
|
409
|
+
null,
|
|
410
|
+
idToken,
|
|
411
|
+
accessToken,
|
|
412
|
+
null,
|
|
413
|
+
pendingMicrosoftScopes.toTypedArray(),
|
|
414
|
+
expirationTime
|
|
415
|
+
)
|
|
416
|
+
} catch (e: Exception) {
|
|
417
|
+
clearPkceState()
|
|
418
|
+
nativeOnLoginError("parse_error", e.message)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private fun saveMicrosoftRefreshToken(context: Context, refreshToken: String) {
|
|
423
|
+
val prefs = getPrefs(context)
|
|
424
|
+
prefs.edit().putString("microsoft_refresh_token", refreshToken).apply()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private fun getMicrosoftRefreshToken(context: Context): String? {
|
|
428
|
+
val prefs = getPrefs(context)
|
|
429
|
+
return prefs.getString("microsoft_refresh_token", null)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private fun clearPkceState() {
|
|
433
|
+
pendingPkceVerifier = null
|
|
434
|
+
pendingState = null
|
|
435
|
+
pendingNonce = null
|
|
436
|
+
pendingMicrosoftTenant = null
|
|
437
|
+
pendingMicrosoftClientId = null
|
|
438
|
+
pendingMicrosoftB2cDomain = null
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private fun decodeJwt(token: String): Map<String, String> {
|
|
442
|
+
return try {
|
|
443
|
+
val parts = token.split(".")
|
|
444
|
+
if (parts.size < 2) return emptyMap()
|
|
445
|
+
val payload = String(Base64.decode(parts[1], Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP))
|
|
446
|
+
val json = JSONObject(payload)
|
|
447
|
+
val result = mutableMapOf<String, String>()
|
|
448
|
+
json.keys().forEach { key ->
|
|
449
|
+
val value = json.optString(key)
|
|
450
|
+
if (value.isNotEmpty()) result[key] = value
|
|
451
|
+
}
|
|
452
|
+
result
|
|
453
|
+
} catch (e: Exception) {
|
|
454
|
+
emptyMap()
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun getMicrosoftClientIdFromResources(context: Context): String? {
|
|
459
|
+
val resId = context.resources.getIdentifier("nitro_auth_microsoft_client_id", "string", context.packageName)
|
|
460
|
+
return if (resId != 0) context.getString(resId) else null
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private fun getMicrosoftTenantFromResources(context: Context): String? {
|
|
464
|
+
val resId = context.resources.getIdentifier("nitro_auth_microsoft_tenant", "string", context.packageName)
|
|
465
|
+
return if (resId != 0) context.getString(resId) else null
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private fun getMicrosoftB2cDomainFromResources(context: Context): String? {
|
|
469
|
+
val resId = context.resources.getIdentifier("nitro_auth_microsoft_b2c_domain", "string", context.packageName)
|
|
470
|
+
return if (resId != 0) context.getString(resId) else null
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private fun getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?): String {
|
|
474
|
+
if (tenant.startsWith("https://")) {
|
|
475
|
+
return if (tenant.endsWith("/")) tenant else "$tenant/"
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return if (b2cDomain != null) {
|
|
479
|
+
"https://$b2cDomain/tfp/$tenant/"
|
|
122
480
|
} else {
|
|
123
|
-
|
|
124
|
-
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
125
|
-
ctx.startActivity(intent)
|
|
481
|
+
"https://login.microsoftonline.com/$tenant/"
|
|
126
482
|
}
|
|
127
483
|
}
|
|
128
484
|
|
|
129
|
-
private fun loginOneTap(
|
|
485
|
+
private fun loginOneTap(
|
|
486
|
+
context: Context,
|
|
487
|
+
clientId: String,
|
|
488
|
+
scopes: List<String>,
|
|
489
|
+
loginHint: String?,
|
|
490
|
+
forceAccountPicker: Boolean,
|
|
491
|
+
useOneTap: Boolean
|
|
492
|
+
) {
|
|
130
493
|
val activity = currentActivity ?: context as? Activity
|
|
131
494
|
if (activity == null) {
|
|
132
495
|
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
133
|
-
return loginLegacy(context, clientId, scopes)
|
|
496
|
+
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
134
497
|
}
|
|
135
498
|
|
|
136
499
|
val credentialManager = CredentialManager.create(activity)
|
|
137
500
|
val googleIdOption = GetGoogleIdOption.Builder()
|
|
138
501
|
.setFilterByAuthorizedAccounts(false)
|
|
139
502
|
.setServerClientId(clientId)
|
|
140
|
-
.setAutoSelectEnabled(
|
|
503
|
+
.setAutoSelectEnabled(useOneTap && !forceAccountPicker)
|
|
141
504
|
.build()
|
|
142
505
|
|
|
143
506
|
val request = GetCredentialRequest.Builder()
|
|
@@ -150,14 +513,26 @@ object AuthAdapter {
|
|
|
150
513
|
handleCredentialResponse(result, scopes)
|
|
151
514
|
} catch (e: Exception) {
|
|
152
515
|
Log.w(TAG, "One-Tap failed, falling back to legacy: ${e.message}")
|
|
153
|
-
loginLegacy(context, clientId, scopes)
|
|
516
|
+
loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
154
517
|
}
|
|
155
518
|
}
|
|
156
519
|
}
|
|
157
520
|
|
|
158
|
-
private fun loginLegacy(
|
|
521
|
+
private fun loginLegacy(
|
|
522
|
+
context: Context,
|
|
523
|
+
clientId: String,
|
|
524
|
+
scopes: List<String>,
|
|
525
|
+
loginHint: String?,
|
|
526
|
+
forceAccountPicker: Boolean
|
|
527
|
+
) {
|
|
159
528
|
val ctx = appContext ?: context.applicationContext
|
|
160
|
-
val intent = GoogleSignInActivity.createIntent(
|
|
529
|
+
val intent = GoogleSignInActivity.createIntent(
|
|
530
|
+
ctx,
|
|
531
|
+
clientId,
|
|
532
|
+
scopes.toTypedArray(),
|
|
533
|
+
loginHint,
|
|
534
|
+
forceAccountPicker
|
|
535
|
+
)
|
|
161
536
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
162
537
|
ctx.startActivity(intent)
|
|
163
538
|
}
|
|
@@ -199,58 +574,72 @@ object AuthAdapter {
|
|
|
199
574
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
200
575
|
val ctx = appContext ?: context.applicationContext
|
|
201
576
|
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
202
|
-
if (account
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
577
|
+
if (account != null) {
|
|
578
|
+
val newScopes = scopes.map { Scope(it) }
|
|
579
|
+
if (GoogleSignIn.hasPermissions(account, *newScopes.toTypedArray())) {
|
|
580
|
+
onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct())
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
val clientId = getClientIdFromResources(ctx)
|
|
584
|
+
if (clientId == null) {
|
|
585
|
+
nativeOnLoginError("configuration_error", "Google Client ID not configured")
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
val allScopes = (pendingScopes + scopes.toList()).distinct()
|
|
589
|
+
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email)
|
|
590
|
+
ctx.startActivity(intent)
|
|
210
591
|
return
|
|
211
592
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
593
|
+
val userJson = getUserJson(ctx)
|
|
594
|
+
if (userJson != null && userJson.contains("\"provider\":\"microsoft\"")) {
|
|
595
|
+
val currentScopes = extractScopesFromUserJson(userJson)
|
|
596
|
+
val defaultMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
597
|
+
val existing = if (currentScopes.isEmpty()) defaultMicrosoftScopes else currentScopes
|
|
598
|
+
val mergedScopes = (existing + scopes.toList()).distinct()
|
|
599
|
+
val tenant = getMicrosoftTenantFromResources(ctx)
|
|
600
|
+
loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null)
|
|
216
601
|
return
|
|
217
602
|
}
|
|
218
|
-
|
|
219
|
-
val allScopes = (pendingScopes + scopes.toList()).distinct()
|
|
220
|
-
val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email)
|
|
221
|
-
ctx.startActivity(intent)
|
|
603
|
+
nativeOnLoginError("unknown", "No user logged in")
|
|
222
604
|
}
|
|
223
605
|
|
|
224
606
|
@JvmStatic
|
|
225
607
|
fun refreshTokenSync(context: Context) {
|
|
226
608
|
val ctx = appContext ?: context.applicationContext
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
609
|
+
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
610
|
+
if (account != null) {
|
|
611
|
+
if (googleSignInClient == null) {
|
|
612
|
+
val clientId = getClientIdFromResources(ctx)
|
|
613
|
+
if (clientId == null) {
|
|
614
|
+
nativeOnRefreshError("configuration_error", "Google Client ID not configured")
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
618
|
+
.requestIdToken(clientId)
|
|
619
|
+
.requestServerAuthCode(clientId)
|
|
620
|
+
.requestEmail()
|
|
621
|
+
.build()
|
|
622
|
+
googleSignInClient = GoogleSignIn.getClient(ctx, gso)
|
|
232
623
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
624
|
+
googleSignInClient!!.silentSignIn().addOnCompleteListener { task ->
|
|
625
|
+
if (task.isSuccessful) {
|
|
626
|
+
val acc = task.result
|
|
627
|
+
nativeOnRefreshSuccess(acc?.idToken, null, null)
|
|
628
|
+
} else {
|
|
629
|
+
nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
|
|
630
|
+
}
|
|
237
631
|
}
|
|
238
|
-
|
|
239
|
-
.requestIdToken(clientId)
|
|
240
|
-
.requestServerAuthCode(clientId)
|
|
241
|
-
.requestEmail()
|
|
242
|
-
.build()
|
|
243
|
-
googleSignInClient = GoogleSignIn.getClient(ctx, gso)
|
|
632
|
+
return
|
|
244
633
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
|
|
634
|
+
val userJson = getUserJson(ctx)
|
|
635
|
+
if (userJson != null && userJson.contains("\"provider\":\"microsoft\"")) {
|
|
636
|
+
val refreshToken = getMicrosoftRefreshToken(ctx)
|
|
637
|
+
if (refreshToken != null) {
|
|
638
|
+
refreshMicrosoftTokenForRefresh(ctx, refreshToken)
|
|
639
|
+
return
|
|
252
640
|
}
|
|
253
641
|
}
|
|
642
|
+
nativeOnRefreshError("unknown", "No user logged in")
|
|
254
643
|
}
|
|
255
644
|
|
|
256
645
|
@JvmStatic
|
|
@@ -298,19 +687,19 @@ object AuthAdapter {
|
|
|
298
687
|
|
|
299
688
|
@JvmStatic
|
|
300
689
|
fun getUserJson(context: Context): String? {
|
|
301
|
-
val pref = context
|
|
690
|
+
val pref = getPrefs(context)
|
|
302
691
|
return pref.getString("user_json", null)
|
|
303
692
|
}
|
|
304
693
|
|
|
305
694
|
@JvmStatic
|
|
306
695
|
fun setUserJson(context: Context, json: String) {
|
|
307
|
-
val pref = context
|
|
696
|
+
val pref = getPrefs(context)
|
|
308
697
|
pref.edit().putString("user_json", json).apply()
|
|
309
698
|
}
|
|
310
699
|
|
|
311
700
|
@JvmStatic
|
|
312
701
|
fun clearUser(context: Context) {
|
|
313
|
-
val pref = context
|
|
702
|
+
val pref = getPrefs(context)
|
|
314
703
|
pref.edit().clear().apply()
|
|
315
704
|
}
|
|
316
705
|
|
|
@@ -325,42 +714,201 @@ object AuthAdapter {
|
|
|
325
714
|
} else {
|
|
326
715
|
val json = getUserJson(ctx)
|
|
327
716
|
if (json != null) {
|
|
328
|
-
val provider =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
717
|
+
val provider = try {
|
|
718
|
+
val parsed = JSONObject(json)
|
|
719
|
+
parsed.optString("provider")
|
|
720
|
+
} catch (_: Exception) {
|
|
721
|
+
""
|
|
722
|
+
}
|
|
723
|
+
val effectiveProvider = when (provider) {
|
|
724
|
+
"google" -> "google"
|
|
725
|
+
"microsoft" -> "microsoft"
|
|
726
|
+
"apple" -> "apple"
|
|
727
|
+
else -> "apple"
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (effectiveProvider == "microsoft") {
|
|
731
|
+
val refreshToken = getMicrosoftRefreshToken(ctx)
|
|
732
|
+
if (refreshToken != null) {
|
|
733
|
+
refreshMicrosoftToken(ctx, refreshToken)
|
|
734
|
+
} else {
|
|
735
|
+
val email = extractJsonValue(json, "email")
|
|
736
|
+
val name = extractJsonValue(json, "name")
|
|
737
|
+
val idToken = extractJsonValue(json, "idToken")
|
|
738
|
+
nativeOnLoginSuccess(effectiveProvider, email, name, null, idToken, null, null, null, null)
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
val email = extractJsonValue(json, "email")
|
|
742
|
+
val name = extractJsonValue(json, "name")
|
|
743
|
+
val photo = extractJsonValue(json, "photo")
|
|
744
|
+
val idToken = extractJsonValue(json, "idToken")
|
|
745
|
+
val serverAuthCode = extractJsonValue(json, "serverAuthCode")
|
|
746
|
+
nativeOnLoginSuccess(effectiveProvider, email, name, photo, idToken, null, serverAuthCode, null, null)
|
|
747
|
+
}
|
|
335
748
|
} else {
|
|
336
749
|
nativeOnLoginError("unknown", "No session")
|
|
337
750
|
}
|
|
338
751
|
}
|
|
339
752
|
}
|
|
340
753
|
|
|
754
|
+
private fun refreshMicrosoftToken(context: Context, refreshToken: String) {
|
|
755
|
+
val clientId = getMicrosoftClientIdFromResources(context)
|
|
756
|
+
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
757
|
+
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
758
|
+
|
|
759
|
+
if (clientId == null) {
|
|
760
|
+
nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
765
|
+
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
766
|
+
|
|
767
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
768
|
+
try {
|
|
769
|
+
val url = java.net.URL(tokenUrl)
|
|
770
|
+
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
771
|
+
connection.requestMethod = "POST"
|
|
772
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
773
|
+
connection.doOutput = true
|
|
774
|
+
|
|
775
|
+
val postData = buildString {
|
|
776
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
777
|
+
append("&grant_type=refresh_token")
|
|
778
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
782
|
+
|
|
783
|
+
val responseCode = connection.responseCode
|
|
784
|
+
val responseBody = if (responseCode == 200) {
|
|
785
|
+
connection.inputStream.bufferedReader().readText()
|
|
786
|
+
} else {
|
|
787
|
+
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
791
|
+
if (responseCode == 200) {
|
|
792
|
+
val json = JSONObject(responseBody)
|
|
793
|
+
val newIdToken = json.optString("id_token")
|
|
794
|
+
val newAccessToken = json.optString("access_token")
|
|
795
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
796
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
797
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
798
|
+
|
|
799
|
+
val claims = decodeJwt(newIdToken)
|
|
800
|
+
val email = claims["preferred_username"] ?: claims["email"]
|
|
801
|
+
val name = claims["name"]
|
|
802
|
+
|
|
803
|
+
if (newRefreshToken.isNotEmpty()) {
|
|
804
|
+
saveMicrosoftRefreshToken(context, newRefreshToken)
|
|
805
|
+
}
|
|
806
|
+
saveUser(context, "microsoft", email, name, null, newIdToken, newRefreshToken, null)
|
|
807
|
+
|
|
808
|
+
nativeOnLoginSuccess("microsoft", email, name, null, newIdToken, newAccessToken, null, null, expirationTime)
|
|
809
|
+
} else {
|
|
810
|
+
nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} catch (e: Exception) {
|
|
814
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
815
|
+
nativeOnLoginError("network_error", e.message)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private fun refreshMicrosoftTokenForRefresh(context: Context, refreshToken: String) {
|
|
822
|
+
val clientId = getMicrosoftClientIdFromResources(context)
|
|
823
|
+
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
824
|
+
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
825
|
+
if (clientId == null) {
|
|
826
|
+
nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
830
|
+
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
831
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
832
|
+
try {
|
|
833
|
+
val url = java.net.URL(tokenUrl)
|
|
834
|
+
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
835
|
+
connection.requestMethod = "POST"
|
|
836
|
+
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
837
|
+
connection.doOutput = true
|
|
838
|
+
val postData = buildString {
|
|
839
|
+
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
840
|
+
append("&grant_type=refresh_token")
|
|
841
|
+
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
842
|
+
}
|
|
843
|
+
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
844
|
+
val responseCode = connection.responseCode
|
|
845
|
+
val responseBody = if (responseCode == 200) {
|
|
846
|
+
connection.inputStream.bufferedReader().readText()
|
|
847
|
+
} else {
|
|
848
|
+
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
849
|
+
}
|
|
850
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
851
|
+
if (responseCode == 200) {
|
|
852
|
+
val json = JSONObject(responseBody)
|
|
853
|
+
val newIdToken = json.optString("id_token")
|
|
854
|
+
val newAccessToken = json.optString("access_token")
|
|
855
|
+
val newRefreshToken = json.optString("refresh_token")
|
|
856
|
+
val expiresIn = json.optLong("expires_in", 0)
|
|
857
|
+
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
858
|
+
val claims = decodeJwt(newIdToken)
|
|
859
|
+
val email = claims["preferred_username"] ?: claims["email"]
|
|
860
|
+
val name = claims["name"]
|
|
861
|
+
if (newRefreshToken.isNotEmpty()) {
|
|
862
|
+
saveMicrosoftRefreshToken(context, newRefreshToken)
|
|
863
|
+
}
|
|
864
|
+
saveUser(context, "microsoft", email, name, null, newIdToken, newRefreshToken, null)
|
|
865
|
+
nativeOnRefreshSuccess(
|
|
866
|
+
newIdToken.ifEmpty { null },
|
|
867
|
+
newAccessToken.ifEmpty { null },
|
|
868
|
+
expirationTime
|
|
869
|
+
)
|
|
870
|
+
} else {
|
|
871
|
+
nativeOnRefreshError("refresh_failed", "Microsoft token refresh failed")
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
} catch (e: Exception) {
|
|
875
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
876
|
+
nativeOnRefreshError("network_error", e.message)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
341
882
|
private fun extractJsonValue(json: String, key: String): String? {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
883
|
+
return try {
|
|
884
|
+
val value = JSONObject(json).optString(key, "")
|
|
885
|
+
if (value.isEmpty()) null else value
|
|
886
|
+
} catch (_: Exception) {
|
|
887
|
+
null
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private fun extractScopesFromUserJson(json: String): List<String> {
|
|
892
|
+
return try {
|
|
893
|
+
val jsonObj = JSONObject(json)
|
|
894
|
+
val arr = jsonObj.optJSONArray("scopes") ?: return emptyList()
|
|
895
|
+
(0 until arr.length()).mapNotNull { i -> arr.optString(i).takeIf { it.isNotEmpty() } }
|
|
896
|
+
} catch (_: Exception) {
|
|
897
|
+
emptyList()
|
|
898
|
+
}
|
|
345
899
|
}
|
|
346
900
|
|
|
347
901
|
private fun saveUser(context: Context, provider: String, email: String?, name: String?,
|
|
348
902
|
photo: String?, idToken: String?, serverAuthCode: String?, scopes: List<String>?) {
|
|
349
|
-
val pref = context
|
|
350
|
-
val json =
|
|
351
|
-
json.
|
|
352
|
-
json.
|
|
353
|
-
if (
|
|
354
|
-
if (
|
|
355
|
-
if (
|
|
356
|
-
if (
|
|
357
|
-
if (
|
|
358
|
-
if (scopes != null) {
|
|
359
|
-
json.append(",\"scopes\":[")
|
|
360
|
-
json.append(scopes.joinToString(",") { "\"$it\"" })
|
|
361
|
-
json.append("]")
|
|
362
|
-
}
|
|
363
|
-
json.append("}")
|
|
903
|
+
val pref = getPrefs(context)
|
|
904
|
+
val json = JSONObject()
|
|
905
|
+
json.put("provider", provider)
|
|
906
|
+
if (email != null) json.put("email", email)
|
|
907
|
+
if (name != null) json.put("name", name)
|
|
908
|
+
if (photo != null) json.put("photo", photo)
|
|
909
|
+
if (idToken != null) json.put("idToken", idToken)
|
|
910
|
+
if (serverAuthCode != null) json.put("serverAuthCode", serverAuthCode)
|
|
911
|
+
if (scopes != null) json.put("scopes", JSONArray(scopes))
|
|
364
912
|
pref.edit().putString("user_json", json.toString()).apply()
|
|
365
913
|
}
|
|
366
914
|
}
|