react-native-nitro-auth 0.5.4 → 0.5.5
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 +60 -30
- package/android/src/main/cpp/JniOnLoad.cpp +3 -1
- package/android/src/main/cpp/PlatformAuth+Android.cpp +11 -11
- package/android/src/main/java/com/auth/AuthAdapter.kt +100 -116
- package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
- package/ios/AuthAdapter.swift +62 -28
- package/lib/commonjs/index.js +23 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/service.js +31 -6
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/use-auth.js +11 -22
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +37 -0
- package/lib/commonjs/utils/auth-error.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/service.js +31 -6
- package/lib/module/service.js.map +1 -1
- package/lib/module/use-auth.js +11 -22
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +30 -0
- package/lib/module/utils/auth-error.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +2 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
- package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +2 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/auth-error.d.ts +16 -0
- package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/service.ts +32 -6
- package/src/use-auth.ts +21 -86
- package/src/utils/auth-error.ts +49 -0
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
@file:Suppress("DEPRECATION")
|
|
2
|
-
|
|
3
1
|
package com.auth
|
|
4
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
5
|
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.net.Uri
|
|
6
8
|
import android.os.Bundle
|
|
9
|
+
import android.util.Base64
|
|
7
10
|
import android.util.Log
|
|
11
|
+
import androidx.browser.customtabs.CustomTabsIntent
|
|
12
|
+
import androidx.credentials.CredentialManager
|
|
13
|
+
import androidx.credentials.GetCredentialRequest
|
|
14
|
+
import androidx.credentials.GetCredentialResponse
|
|
8
15
|
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
|
9
16
|
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
|
10
17
|
import com.google.android.gms.auth.api.signin.GoogleSignInClient
|
|
11
18
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
|
12
|
-
import com.google.android.gms.common.GoogleApiAvailability
|
|
13
19
|
import com.google.android.gms.common.ConnectionResult
|
|
20
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
14
21
|
import com.google.android.gms.common.api.Scope
|
|
15
|
-
import androidx.credentials.CredentialManager
|
|
16
|
-
import androidx.credentials.GetCredentialRequest
|
|
17
|
-
import androidx.credentials.GetCredentialResponse
|
|
18
22
|
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
|
19
23
|
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
|
20
24
|
import kotlinx.coroutines.CoroutineScope
|
|
21
25
|
import kotlinx.coroutines.Dispatchers
|
|
26
|
+
import kotlinx.coroutines.SupervisorJob
|
|
27
|
+
import kotlinx.coroutines.cancel
|
|
22
28
|
import kotlinx.coroutines.launch
|
|
23
|
-
import
|
|
24
|
-
import android.app.Application
|
|
25
|
-
import android.content.Intent
|
|
26
|
-
import android.net.Uri
|
|
27
|
-
import androidx.browser.customtabs.CustomTabsIntent
|
|
28
|
-
import java.util.UUID
|
|
29
|
+
import kotlinx.coroutines.withContext
|
|
29
30
|
import org.json.JSONObject
|
|
30
|
-
import
|
|
31
|
+
import java.util.UUID
|
|
31
32
|
|
|
32
33
|
object AuthAdapter {
|
|
33
34
|
private const val TAG = "AuthAdapter"
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
@Volatile
|
|
36
37
|
private var isInitialized = false
|
|
37
38
|
|
|
@@ -41,34 +42,37 @@ object AuthAdapter {
|
|
|
41
42
|
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
|
42
43
|
private var pendingScopes: List<String> = emptyList()
|
|
43
44
|
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
private var pendingPkceVerifier: String? = null
|
|
46
47
|
private var pendingState: String? = null
|
|
47
48
|
private var pendingNonce: String? = null
|
|
48
49
|
private var pendingMicrosoftTenant: String? = null
|
|
49
50
|
private var pendingMicrosoftClientId: String? = null
|
|
50
51
|
private var pendingMicrosoftB2cDomain: String? = null
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
private var inMemoryMicrosoftRefreshToken: String? = null
|
|
53
54
|
private var inMemoryMicrosoftScopes: List<String> =
|
|
54
55
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
55
56
|
|
|
57
|
+
// Module-scoped coroutine scope — cancelled on module invalidation via dispose().
|
|
58
|
+
private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
59
|
+
|
|
56
60
|
@JvmStatic
|
|
57
61
|
private external fun nativeInitialize(context: Context)
|
|
58
|
-
|
|
62
|
+
|
|
59
63
|
@JvmStatic
|
|
60
64
|
private external fun nativeOnLoginSuccess(
|
|
61
|
-
provider: String,
|
|
62
|
-
email: String?,
|
|
63
|
-
name: String?,
|
|
64
|
-
photo: String?,
|
|
65
|
+
provider: String,
|
|
66
|
+
email: String?,
|
|
67
|
+
name: String?,
|
|
68
|
+
photo: String?,
|
|
65
69
|
idToken: String?,
|
|
66
70
|
accessToken: String?,
|
|
67
71
|
serverAuthCode: String?,
|
|
68
72
|
scopes: Array<String>?,
|
|
69
73
|
expirationTime: Long?
|
|
70
74
|
)
|
|
71
|
-
|
|
75
|
+
|
|
72
76
|
@JvmStatic
|
|
73
77
|
private external fun nativeOnLoginError(error: String, underlyingError: String?)
|
|
74
78
|
|
|
@@ -80,9 +84,7 @@ object AuthAdapter {
|
|
|
80
84
|
|
|
81
85
|
@Synchronized
|
|
82
86
|
fun initialize(context: Context) {
|
|
83
|
-
if (isInitialized)
|
|
84
|
-
return
|
|
85
|
-
}
|
|
87
|
+
if (isInitialized) return
|
|
86
88
|
|
|
87
89
|
val applicationContext = context.applicationContext
|
|
88
90
|
appContext = applicationContext
|
|
@@ -102,19 +104,35 @@ object AuthAdapter {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
try {
|
|
105
|
-
|
|
107
|
+
// The native library is already loaded by NitroAuthOnLoad.initializeNative()
|
|
108
|
+
// before this method is called from NitroAuthModule. We only need to wire
|
|
109
|
+
// the Android context so that native methods can call back into the JVM.
|
|
106
110
|
nativeInitialize(applicationContext)
|
|
107
111
|
isInitialized = true
|
|
108
112
|
} catch (e: Exception) {
|
|
109
|
-
Log.e(TAG, "Failed to
|
|
113
|
+
Log.e(TAG, "Failed to initialize NitroAuth native bridge", e)
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
fun dispose() {
|
|
118
|
+
moduleScope.cancel()
|
|
119
|
+
moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
120
|
+
|
|
121
|
+
val app = appContext as? Application
|
|
122
|
+
lifecycleCallbacks?.let { app?.unregisterActivityLifecycleCallbacks(it) }
|
|
123
|
+
lifecycleCallbacks = null
|
|
124
|
+
currentActivity = null
|
|
125
|
+
appContext = null
|
|
126
|
+
googleSignInClient = null
|
|
127
|
+
isInitialized = false
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>) {
|
|
114
131
|
appContext ?: return
|
|
115
132
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
116
133
|
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
117
|
-
|
|
134
|
+
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
135
|
+
scopes.toTypedArray(), expirationTime)
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
fun onSignInError(errorCode: Int, message: String?) {
|
|
@@ -144,12 +162,10 @@ object AuthAdapter {
|
|
|
144
162
|
nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
145
163
|
return
|
|
146
164
|
}
|
|
147
|
-
|
|
148
165
|
if (provider == "microsoft") {
|
|
149
166
|
loginMicrosoft(context, scopes, loginHint, tenant, prompt)
|
|
150
167
|
return
|
|
151
168
|
}
|
|
152
|
-
|
|
153
169
|
if (provider != "google") {
|
|
154
170
|
nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
|
|
155
171
|
return
|
|
@@ -169,7 +185,6 @@ object AuthAdapter {
|
|
|
169
185
|
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
|
|
170
186
|
return
|
|
171
187
|
}
|
|
172
|
-
|
|
173
188
|
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
|
|
174
189
|
}
|
|
175
190
|
|
|
@@ -253,19 +268,16 @@ object AuthAdapter {
|
|
|
253
268
|
nativeOnLoginError(error, errorDescription)
|
|
254
269
|
return
|
|
255
270
|
}
|
|
256
|
-
|
|
257
271
|
if (state != pendingState) {
|
|
258
272
|
clearPkceState()
|
|
259
273
|
nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
|
|
260
274
|
return
|
|
261
275
|
}
|
|
262
|
-
|
|
263
276
|
if (code == null) {
|
|
264
277
|
clearPkceState()
|
|
265
278
|
nativeOnLoginError("unknown", "No authorization code in response")
|
|
266
279
|
return
|
|
267
280
|
}
|
|
268
|
-
|
|
269
281
|
exchangeCodeForTokens(code)
|
|
270
282
|
}
|
|
271
283
|
|
|
@@ -285,7 +297,7 @@ object AuthAdapter {
|
|
|
285
297
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, pendingMicrosoftB2cDomain)
|
|
286
298
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
287
299
|
|
|
288
|
-
|
|
300
|
+
moduleScope.launch {
|
|
289
301
|
try {
|
|
290
302
|
val url = java.net.URL(tokenUrl)
|
|
291
303
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
@@ -300,7 +312,6 @@ object AuthAdapter {
|
|
|
300
312
|
append("&grant_type=authorization_code")
|
|
301
313
|
append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
|
|
302
314
|
}
|
|
303
|
-
|
|
304
315
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
305
316
|
|
|
306
317
|
val responseCode = connection.responseCode
|
|
@@ -310,11 +321,11 @@ object AuthAdapter {
|
|
|
310
321
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
311
322
|
}
|
|
312
323
|
|
|
313
|
-
|
|
324
|
+
withContext(Dispatchers.Main) {
|
|
314
325
|
handleTokenResponse(responseCode, responseBody)
|
|
315
326
|
}
|
|
316
327
|
} catch (e: Exception) {
|
|
317
|
-
|
|
328
|
+
withContext(Dispatchers.Main) {
|
|
318
329
|
clearPkceState()
|
|
319
330
|
nativeOnLoginError("network_error", e.message)
|
|
320
331
|
}
|
|
@@ -352,8 +363,7 @@ object AuthAdapter {
|
|
|
352
363
|
}
|
|
353
364
|
|
|
354
365
|
val claims = decodeJwt(idToken)
|
|
355
|
-
|
|
356
|
-
if (tokenNonce != pendingNonce) {
|
|
366
|
+
if (claims["nonce"] != pendingNonce) {
|
|
357
367
|
clearPkceState()
|
|
358
368
|
nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
359
369
|
return
|
|
@@ -362,24 +372,15 @@ object AuthAdapter {
|
|
|
362
372
|
val email = claims["preferred_username"] ?: claims["email"]
|
|
363
373
|
val name = claims["name"]
|
|
364
374
|
|
|
365
|
-
if (refreshToken.isNotEmpty())
|
|
366
|
-
inMemoryMicrosoftRefreshToken = refreshToken
|
|
367
|
-
}
|
|
375
|
+
if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
|
|
368
376
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
369
377
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
370
378
|
}
|
|
371
379
|
|
|
372
380
|
clearPkceState()
|
|
373
381
|
nativeOnLoginSuccess(
|
|
374
|
-
"microsoft",
|
|
375
|
-
|
|
376
|
-
name,
|
|
377
|
-
null,
|
|
378
|
-
idToken,
|
|
379
|
-
accessToken,
|
|
380
|
-
null,
|
|
381
|
-
pendingMicrosoftScopes.toTypedArray(),
|
|
382
|
-
expirationTime
|
|
382
|
+
"microsoft", email, name, null, idToken, accessToken, null,
|
|
383
|
+
pendingMicrosoftScopes.toTypedArray(), expirationTime
|
|
383
384
|
)
|
|
384
385
|
} catch (e: Exception) {
|
|
385
386
|
clearPkceState()
|
|
@@ -387,14 +388,6 @@ object AuthAdapter {
|
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
390
|
|
|
390
|
-
private fun saveMicrosoftRefreshToken(refreshToken: String) {
|
|
391
|
-
inMemoryMicrosoftRefreshToken = refreshToken
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
private fun getMicrosoftRefreshToken(): String? {
|
|
395
|
-
return inMemoryMicrosoftRefreshToken
|
|
396
|
-
}
|
|
397
|
-
|
|
398
391
|
private fun clearPkceState() {
|
|
399
392
|
pendingPkceVerifier = null
|
|
400
393
|
pendingState = null
|
|
@@ -422,10 +415,7 @@ object AuthAdapter {
|
|
|
422
415
|
}
|
|
423
416
|
|
|
424
417
|
private fun getJwtExpirationTimeMs(idToken: String?): Long? {
|
|
425
|
-
if (idToken.isNullOrEmpty())
|
|
426
|
-
return null
|
|
427
|
-
}
|
|
428
|
-
|
|
418
|
+
if (idToken.isNullOrEmpty()) return null
|
|
429
419
|
val expSeconds = decodeJwt(idToken)["exp"]?.toLongOrNull() ?: return null
|
|
430
420
|
return expSeconds * 1000
|
|
431
421
|
}
|
|
@@ -449,7 +439,6 @@ object AuthAdapter {
|
|
|
449
439
|
if (tenant.startsWith("https://")) {
|
|
450
440
|
return if (tenant.endsWith("/")) tenant else "$tenant/"
|
|
451
441
|
}
|
|
452
|
-
|
|
453
442
|
return if (b2cDomain != null) {
|
|
454
443
|
"https://$b2cDomain/tfp/$tenant/"
|
|
455
444
|
} else {
|
|
@@ -470,7 +459,7 @@ object AuthAdapter {
|
|
|
470
459
|
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
471
460
|
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
472
461
|
}
|
|
473
|
-
|
|
462
|
+
|
|
474
463
|
val credentialManager = CredentialManager.create(activity)
|
|
475
464
|
val googleIdOption = GetGoogleIdOption.Builder()
|
|
476
465
|
.setFilterByAuthorizedAccounts(false)
|
|
@@ -482,7 +471,7 @@ object AuthAdapter {
|
|
|
482
471
|
.addCredentialOption(googleIdOption)
|
|
483
472
|
.build()
|
|
484
473
|
|
|
485
|
-
|
|
474
|
+
moduleScope.launch(Dispatchers.Main) {
|
|
486
475
|
try {
|
|
487
476
|
val result = credentialManager.getCredential(context = activity, request = request)
|
|
488
477
|
handleCredentialResponse(result, scopes)
|
|
@@ -493,6 +482,7 @@ object AuthAdapter {
|
|
|
493
482
|
}
|
|
494
483
|
}
|
|
495
484
|
|
|
485
|
+
@Suppress("DEPRECATION")
|
|
496
486
|
private fun loginLegacy(
|
|
497
487
|
context: Context,
|
|
498
488
|
clientId: String,
|
|
@@ -502,11 +492,7 @@ object AuthAdapter {
|
|
|
502
492
|
) {
|
|
503
493
|
val ctx = appContext ?: context.applicationContext
|
|
504
494
|
val intent = GoogleSignInActivity.createIntent(
|
|
505
|
-
ctx,
|
|
506
|
-
clientId,
|
|
507
|
-
scopes.toTypedArray(),
|
|
508
|
-
loginHint,
|
|
509
|
-
forceAccountPicker
|
|
495
|
+
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
|
|
510
496
|
)
|
|
511
497
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
512
498
|
ctx.startActivity(intent)
|
|
@@ -535,8 +521,7 @@ object AuthAdapter {
|
|
|
535
521
|
googleIdTokenCredential.displayName,
|
|
536
522
|
googleIdTokenCredential.profilePictureUri?.toString(),
|
|
537
523
|
googleIdTokenCredential.idToken,
|
|
538
|
-
null,
|
|
539
|
-
null,
|
|
524
|
+
null, null,
|
|
540
525
|
scopes.toTypedArray(),
|
|
541
526
|
expirationTime
|
|
542
527
|
)
|
|
@@ -546,6 +531,9 @@ object AuthAdapter {
|
|
|
546
531
|
}
|
|
547
532
|
}
|
|
548
533
|
|
|
534
|
+
// requestScopesSync uses the legacy GoogleSignIn API to check the last signed-in account
|
|
535
|
+
// because Credential Manager has no equivalent for querying existing account state.
|
|
536
|
+
@Suppress("DEPRECATION")
|
|
549
537
|
@JvmStatic
|
|
550
538
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
551
539
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -575,6 +563,9 @@ object AuthAdapter {
|
|
|
575
563
|
nativeOnLoginError("unknown", "No user logged in")
|
|
576
564
|
}
|
|
577
565
|
|
|
566
|
+
// refreshTokenSync uses the legacy silentSignIn because AuthorizationClient (the recommended
|
|
567
|
+
// replacement) requires an Activity context which is not always available at refresh time.
|
|
568
|
+
@Suppress("DEPRECATION")
|
|
578
569
|
@JvmStatic
|
|
579
570
|
fun refreshTokenSync(context: Context) {
|
|
580
571
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -596,18 +587,14 @@ object AuthAdapter {
|
|
|
596
587
|
googleSignInClient!!.silentSignIn().addOnCompleteListener { task ->
|
|
597
588
|
if (task.isSuccessful) {
|
|
598
589
|
val acc = task.result
|
|
599
|
-
nativeOnRefreshSuccess(
|
|
600
|
-
acc?.idToken,
|
|
601
|
-
null,
|
|
602
|
-
getJwtExpirationTimeMs(acc?.idToken),
|
|
603
|
-
)
|
|
590
|
+
nativeOnRefreshSuccess(acc?.idToken, null, getJwtExpirationTimeMs(acc?.idToken))
|
|
604
591
|
} else {
|
|
605
592
|
nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
|
|
606
593
|
}
|
|
607
594
|
}
|
|
608
595
|
return
|
|
609
596
|
}
|
|
610
|
-
val refreshToken =
|
|
597
|
+
val refreshToken = inMemoryMicrosoftRefreshToken
|
|
611
598
|
if (refreshToken != null) {
|
|
612
599
|
refreshMicrosoftTokenForRefresh(ctx, refreshToken)
|
|
613
600
|
return
|
|
@@ -618,37 +605,34 @@ object AuthAdapter {
|
|
|
618
605
|
@JvmStatic
|
|
619
606
|
fun hasPlayServices(context: Context): Boolean {
|
|
620
607
|
val ctx = context.applicationContext ?: appContext ?: return false
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return result == ConnectionResult.SUCCESS
|
|
608
|
+
return GoogleApiAvailability.getInstance()
|
|
609
|
+
.isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
|
|
624
610
|
}
|
|
625
611
|
|
|
612
|
+
// logoutSync / revokeAccessSync use the legacy GoogleSignIn client because Credential Manager
|
|
613
|
+
// does not expose a sign-out or revoke API for the Google ID token flow.
|
|
614
|
+
@Suppress("DEPRECATION")
|
|
626
615
|
@JvmStatic
|
|
627
616
|
fun logoutSync(context: Context) {
|
|
628
617
|
val ctx = appContext ?: context.applicationContext
|
|
629
618
|
val clientId = getClientIdFromResources(ctx)
|
|
630
619
|
if (clientId != null) {
|
|
631
620
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
632
|
-
.requestIdToken(clientId)
|
|
633
|
-
.requestServerAuthCode(clientId)
|
|
634
|
-
.requestEmail()
|
|
635
|
-
.build()
|
|
621
|
+
.requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
|
|
636
622
|
GoogleSignIn.getClient(ctx, gso).signOut()
|
|
637
623
|
}
|
|
638
624
|
inMemoryMicrosoftRefreshToken = null
|
|
639
625
|
inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
640
626
|
}
|
|
641
627
|
|
|
628
|
+
@Suppress("DEPRECATION")
|
|
642
629
|
@JvmStatic
|
|
643
630
|
fun revokeAccessSync(context: Context) {
|
|
644
631
|
val ctx = appContext ?: context.applicationContext
|
|
645
632
|
val clientId = getClientIdFromResources(ctx)
|
|
646
633
|
if (clientId != null) {
|
|
647
634
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
648
|
-
.requestIdToken(clientId)
|
|
649
|
-
.requestServerAuthCode(clientId)
|
|
650
|
-
.requestEmail()
|
|
651
|
-
.build()
|
|
635
|
+
.requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
|
|
652
636
|
GoogleSignIn.getClient(ctx, gso).revokeAccess()
|
|
653
637
|
}
|
|
654
638
|
inMemoryMicrosoftRefreshToken = null
|
|
@@ -663,14 +647,15 @@ object AuthAdapter {
|
|
|
663
647
|
@JvmStatic
|
|
664
648
|
fun restoreSession(context: Context) {
|
|
665
649
|
val ctx = context.applicationContext ?: appContext ?: context
|
|
650
|
+
@Suppress("DEPRECATION")
|
|
666
651
|
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
667
652
|
if (account != null) {
|
|
668
653
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
669
654
|
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
670
|
-
|
|
671
|
-
|
|
655
|
+
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
656
|
+
account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
|
|
672
657
|
} else {
|
|
673
|
-
val refreshToken =
|
|
658
|
+
val refreshToken = inMemoryMicrosoftRefreshToken
|
|
674
659
|
if (refreshToken != null) {
|
|
675
660
|
refreshMicrosoftToken(ctx, refreshToken)
|
|
676
661
|
} else {
|
|
@@ -683,7 +668,7 @@ object AuthAdapter {
|
|
|
683
668
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
684
669
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
685
670
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
686
|
-
|
|
671
|
+
|
|
687
672
|
if (clientId == null) {
|
|
688
673
|
nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
|
|
689
674
|
return
|
|
@@ -692,7 +677,7 @@ object AuthAdapter {
|
|
|
692
677
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
693
678
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
694
679
|
|
|
695
|
-
|
|
680
|
+
moduleScope.launch {
|
|
696
681
|
try {
|
|
697
682
|
val url = java.net.URL(tokenUrl)
|
|
698
683
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
@@ -705,7 +690,6 @@ object AuthAdapter {
|
|
|
705
690
|
append("&grant_type=refresh_token")
|
|
706
691
|
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
707
692
|
}
|
|
708
|
-
|
|
709
693
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
710
694
|
|
|
711
695
|
val responseCode = connection.responseCode
|
|
@@ -715,7 +699,7 @@ object AuthAdapter {
|
|
|
715
699
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
716
700
|
}
|
|
717
701
|
|
|
718
|
-
|
|
702
|
+
withContext(Dispatchers.Main) {
|
|
719
703
|
if (responseCode == 200) {
|
|
720
704
|
val json = JSONObject(responseBody)
|
|
721
705
|
val newIdToken = json.optString("id_token")
|
|
@@ -723,25 +707,23 @@ object AuthAdapter {
|
|
|
723
707
|
val newRefreshToken = json.optString("refresh_token")
|
|
724
708
|
val expiresIn = json.optLong("expires_in", 0)
|
|
725
709
|
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
726
|
-
|
|
727
710
|
val claims = decodeJwt(newIdToken)
|
|
728
|
-
val email = claims["preferred_username"] ?: claims["email"]
|
|
729
|
-
val name = claims["name"]
|
|
730
711
|
|
|
731
|
-
if (newRefreshToken.isNotEmpty())
|
|
732
|
-
saveMicrosoftRefreshToken(newRefreshToken)
|
|
733
|
-
}
|
|
712
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
734
713
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
735
714
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
736
715
|
}
|
|
737
716
|
|
|
738
|
-
nativeOnLoginSuccess("microsoft",
|
|
717
|
+
nativeOnLoginSuccess("microsoft",
|
|
718
|
+
claims["preferred_username"] ?: claims["email"],
|
|
719
|
+
claims["name"], null,
|
|
720
|
+
newIdToken, newAccessToken, null, null, expirationTime)
|
|
739
721
|
} else {
|
|
740
722
|
nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
|
|
741
723
|
}
|
|
742
724
|
}
|
|
743
725
|
} catch (e: Exception) {
|
|
744
|
-
|
|
726
|
+
withContext(Dispatchers.Main) {
|
|
745
727
|
nativeOnLoginError("network_error", e.message)
|
|
746
728
|
}
|
|
747
729
|
}
|
|
@@ -752,32 +734,38 @@ object AuthAdapter {
|
|
|
752
734
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
753
735
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
754
736
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
737
|
+
|
|
755
738
|
if (clientId == null) {
|
|
756
739
|
nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
|
|
757
740
|
return
|
|
758
741
|
}
|
|
742
|
+
|
|
759
743
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
760
744
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
761
|
-
|
|
745
|
+
|
|
746
|
+
moduleScope.launch {
|
|
762
747
|
try {
|
|
763
748
|
val url = java.net.URL(tokenUrl)
|
|
764
749
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
765
750
|
connection.requestMethod = "POST"
|
|
766
751
|
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
767
752
|
connection.doOutput = true
|
|
753
|
+
|
|
768
754
|
val postData = buildString {
|
|
769
755
|
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
770
756
|
append("&grant_type=refresh_token")
|
|
771
757
|
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
772
758
|
}
|
|
773
759
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
760
|
+
|
|
774
761
|
val responseCode = connection.responseCode
|
|
775
762
|
val responseBody = if (responseCode == 200) {
|
|
776
763
|
connection.inputStream.bufferedReader().readText()
|
|
777
764
|
} else {
|
|
778
765
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
779
766
|
}
|
|
780
|
-
|
|
767
|
+
|
|
768
|
+
withContext(Dispatchers.Main) {
|
|
781
769
|
if (responseCode == 200) {
|
|
782
770
|
val json = JSONObject(responseBody)
|
|
783
771
|
val newIdToken = json.optString("id_token")
|
|
@@ -785,15 +773,12 @@ object AuthAdapter {
|
|
|
785
773
|
val newRefreshToken = json.optString("refresh_token")
|
|
786
774
|
val expiresIn = json.optLong("expires_in", 0)
|
|
787
775
|
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
val name = claims["name"]
|
|
791
|
-
if (newRefreshToken.isNotEmpty()) {
|
|
792
|
-
saveMicrosoftRefreshToken(newRefreshToken)
|
|
793
|
-
}
|
|
776
|
+
|
|
777
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
794
778
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
795
779
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
796
780
|
}
|
|
781
|
+
|
|
797
782
|
nativeOnRefreshSuccess(
|
|
798
783
|
newIdToken.ifEmpty { null },
|
|
799
784
|
newAccessToken.ifEmpty { null },
|
|
@@ -804,11 +789,10 @@ object AuthAdapter {
|
|
|
804
789
|
}
|
|
805
790
|
}
|
|
806
791
|
} catch (e: Exception) {
|
|
807
|
-
|
|
792
|
+
withContext(Dispatchers.Main) {
|
|
808
793
|
nativeOnRefreshError("network_error", e.message)
|
|
809
794
|
}
|
|
810
795
|
}
|
|
811
796
|
}
|
|
812
797
|
}
|
|
813
|
-
|
|
814
798
|
}
|
|
@@ -3,17 +3,24 @@ package com.auth
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
5
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.margelo.nitro.com.auth.NitroAuthOnLoad
|
|
6
7
|
|
|
7
8
|
class NitroAuthModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
8
9
|
override fun getName(): String = "NitroAuthModule"
|
|
9
10
|
|
|
10
11
|
init {
|
|
11
12
|
try {
|
|
13
|
+
// Load the native library first so that AuthAdapter's JNI methods are resolvable.
|
|
14
|
+
NitroAuthOnLoad.initializeNative()
|
|
12
15
|
AuthAdapter.initialize(reactContext)
|
|
13
|
-
com.margelo.nitro.com.auth.NitroAuthOnLoad.initializeNative()
|
|
14
16
|
Log.d("NitroAuthModule", "NitroAuth initialized")
|
|
15
17
|
} catch (e: Exception) {
|
|
16
18
|
Log.e("NitroAuthModule", "Failed to initialize NitroAuth", e)
|
|
17
19
|
}
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
override fun invalidate() {
|
|
23
|
+
super.invalidate()
|
|
24
|
+
AuthAdapter.dispose()
|
|
25
|
+
}
|
|
19
26
|
}
|