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