react-native-nitro-auth 0.5.3 → 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/build.gradle +2 -5
- package/android/src/main/cpp/JniOnLoad.cpp +3 -1
- package/android/src/main/cpp/PlatformAuth+Android.cpp +95 -29
- package/android/src/main/java/com/auth/AuthAdapter.kt +124 -126
- package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
- package/cpp/AuthCache.cpp +0 -44
- package/cpp/AuthCache.hpp +0 -7
- package/cpp/HybridAuth.cpp +20 -2
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +64 -28
- package/lib/commonjs/Auth.web.js +96 -43
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +23 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/service.js +33 -8
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/use-auth.js +51 -54
- 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/Auth.web.js +96 -43
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/service.js +33 -8
- package/lib/module/service.js.map +1 -1
- package/lib/module/use-auth.js +51 -54
- 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/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 -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/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 -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/react-native-nitro-auth.podspec +1 -1
- package/src/Auth.web.ts +124 -50
- package/src/index.ts +1 -0
- package/src/service.ts +34 -8
- package/src/use-auth.ts +81 -114
- package/src/utils/auth-error.ts +49 -0
- package/ios/KeychainStore.swift +0 -43
|
@@ -1,70 +1,78 @@
|
|
|
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
|
+
|
|
36
|
+
@Volatile
|
|
37
|
+
private var isInitialized = false
|
|
38
|
+
|
|
35
39
|
private var appContext: Context? = null
|
|
36
40
|
private var currentActivity: Activity? = null
|
|
37
41
|
private var googleSignInClient: GoogleSignInClient? = null
|
|
42
|
+
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
|
38
43
|
private var pendingScopes: List<String> = emptyList()
|
|
39
44
|
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
40
|
-
|
|
45
|
+
|
|
41
46
|
private var pendingPkceVerifier: String? = null
|
|
42
47
|
private var pendingState: String? = null
|
|
43
48
|
private var pendingNonce: String? = null
|
|
44
49
|
private var pendingMicrosoftTenant: String? = null
|
|
45
50
|
private var pendingMicrosoftClientId: String? = null
|
|
46
51
|
private var pendingMicrosoftB2cDomain: String? = null
|
|
47
|
-
|
|
52
|
+
|
|
48
53
|
private var inMemoryMicrosoftRefreshToken: String? = null
|
|
49
54
|
private var inMemoryMicrosoftScopes: List<String> =
|
|
50
55
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
51
56
|
|
|
57
|
+
// Module-scoped coroutine scope — cancelled on module invalidation via dispose().
|
|
58
|
+
private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
59
|
+
|
|
52
60
|
@JvmStatic
|
|
53
61
|
private external fun nativeInitialize(context: Context)
|
|
54
|
-
|
|
62
|
+
|
|
55
63
|
@JvmStatic
|
|
56
64
|
private external fun nativeOnLoginSuccess(
|
|
57
|
-
provider: String,
|
|
58
|
-
email: String?,
|
|
59
|
-
name: String?,
|
|
60
|
-
photo: String?,
|
|
65
|
+
provider: String,
|
|
66
|
+
email: String?,
|
|
67
|
+
name: String?,
|
|
68
|
+
photo: String?,
|
|
61
69
|
idToken: String?,
|
|
62
70
|
accessToken: String?,
|
|
63
71
|
serverAuthCode: String?,
|
|
64
72
|
scopes: Array<String>?,
|
|
65
73
|
expirationTime: Long?
|
|
66
74
|
)
|
|
67
|
-
|
|
75
|
+
|
|
68
76
|
@JvmStatic
|
|
69
77
|
private external fun nativeOnLoginError(error: String, underlyingError: String?)
|
|
70
78
|
|
|
@@ -74,33 +82,57 @@ object AuthAdapter {
|
|
|
74
82
|
@JvmStatic
|
|
75
83
|
private external fun nativeOnRefreshError(error: String, underlyingError: String?)
|
|
76
84
|
|
|
85
|
+
@Synchronized
|
|
77
86
|
fun initialize(context: Context) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (isInitialized) return
|
|
88
|
+
|
|
89
|
+
val applicationContext = context.applicationContext
|
|
90
|
+
appContext = applicationContext
|
|
91
|
+
|
|
92
|
+
val app = applicationContext as? Application
|
|
93
|
+
if (app != null && lifecycleCallbacks == null) {
|
|
94
|
+
lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
|
|
95
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
|
|
96
|
+
override fun onActivityStarted(activity: Activity) { currentActivity = activity }
|
|
97
|
+
override fun onActivityResumed(activity: Activity) { currentActivity = activity }
|
|
98
|
+
override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
99
|
+
override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
100
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
101
|
+
override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
102
|
+
}
|
|
103
|
+
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
|
104
|
+
}
|
|
90
105
|
|
|
91
106
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
110
|
+
nativeInitialize(applicationContext)
|
|
111
|
+
isInitialized = true
|
|
94
112
|
} catch (e: Exception) {
|
|
95
|
-
Log.e(TAG, "Failed to
|
|
113
|
+
Log.e(TAG, "Failed to initialize NitroAuth native bridge", e)
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
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
|
+
|
|
99
130
|
fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>) {
|
|
100
131
|
appContext ?: return
|
|
101
132
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
102
133
|
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
103
|
-
|
|
134
|
+
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
135
|
+
scopes.toTypedArray(), expirationTime)
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
fun onSignInError(errorCode: Int, message: String?) {
|
|
@@ -130,12 +162,10 @@ object AuthAdapter {
|
|
|
130
162
|
nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
|
|
131
163
|
return
|
|
132
164
|
}
|
|
133
|
-
|
|
134
165
|
if (provider == "microsoft") {
|
|
135
166
|
loginMicrosoft(context, scopes, loginHint, tenant, prompt)
|
|
136
167
|
return
|
|
137
168
|
}
|
|
138
|
-
|
|
139
169
|
if (provider != "google") {
|
|
140
170
|
nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
|
|
141
171
|
return
|
|
@@ -155,7 +185,6 @@ object AuthAdapter {
|
|
|
155
185
|
loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
|
|
156
186
|
return
|
|
157
187
|
}
|
|
158
|
-
|
|
159
188
|
loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
|
|
160
189
|
}
|
|
161
190
|
|
|
@@ -239,19 +268,16 @@ object AuthAdapter {
|
|
|
239
268
|
nativeOnLoginError(error, errorDescription)
|
|
240
269
|
return
|
|
241
270
|
}
|
|
242
|
-
|
|
243
271
|
if (state != pendingState) {
|
|
244
272
|
clearPkceState()
|
|
245
273
|
nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
|
|
246
274
|
return
|
|
247
275
|
}
|
|
248
|
-
|
|
249
276
|
if (code == null) {
|
|
250
277
|
clearPkceState()
|
|
251
278
|
nativeOnLoginError("unknown", "No authorization code in response")
|
|
252
279
|
return
|
|
253
280
|
}
|
|
254
|
-
|
|
255
281
|
exchangeCodeForTokens(code)
|
|
256
282
|
}
|
|
257
283
|
|
|
@@ -271,7 +297,7 @@ object AuthAdapter {
|
|
|
271
297
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, pendingMicrosoftB2cDomain)
|
|
272
298
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
273
299
|
|
|
274
|
-
|
|
300
|
+
moduleScope.launch {
|
|
275
301
|
try {
|
|
276
302
|
val url = java.net.URL(tokenUrl)
|
|
277
303
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
@@ -286,7 +312,6 @@ object AuthAdapter {
|
|
|
286
312
|
append("&grant_type=authorization_code")
|
|
287
313
|
append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
|
|
288
314
|
}
|
|
289
|
-
|
|
290
315
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
291
316
|
|
|
292
317
|
val responseCode = connection.responseCode
|
|
@@ -296,11 +321,11 @@ object AuthAdapter {
|
|
|
296
321
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
297
322
|
}
|
|
298
323
|
|
|
299
|
-
|
|
324
|
+
withContext(Dispatchers.Main) {
|
|
300
325
|
handleTokenResponse(responseCode, responseBody)
|
|
301
326
|
}
|
|
302
327
|
} catch (e: Exception) {
|
|
303
|
-
|
|
328
|
+
withContext(Dispatchers.Main) {
|
|
304
329
|
clearPkceState()
|
|
305
330
|
nativeOnLoginError("network_error", e.message)
|
|
306
331
|
}
|
|
@@ -338,8 +363,7 @@ object AuthAdapter {
|
|
|
338
363
|
}
|
|
339
364
|
|
|
340
365
|
val claims = decodeJwt(idToken)
|
|
341
|
-
|
|
342
|
-
if (tokenNonce != pendingNonce) {
|
|
366
|
+
if (claims["nonce"] != pendingNonce) {
|
|
343
367
|
clearPkceState()
|
|
344
368
|
nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
|
|
345
369
|
return
|
|
@@ -348,24 +372,15 @@ object AuthAdapter {
|
|
|
348
372
|
val email = claims["preferred_username"] ?: claims["email"]
|
|
349
373
|
val name = claims["name"]
|
|
350
374
|
|
|
351
|
-
if (refreshToken.isNotEmpty())
|
|
352
|
-
inMemoryMicrosoftRefreshToken = refreshToken
|
|
353
|
-
}
|
|
375
|
+
if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
|
|
354
376
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
355
377
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
356
378
|
}
|
|
357
379
|
|
|
358
380
|
clearPkceState()
|
|
359
381
|
nativeOnLoginSuccess(
|
|
360
|
-
"microsoft",
|
|
361
|
-
|
|
362
|
-
name,
|
|
363
|
-
null,
|
|
364
|
-
idToken,
|
|
365
|
-
accessToken,
|
|
366
|
-
null,
|
|
367
|
-
pendingMicrosoftScopes.toTypedArray(),
|
|
368
|
-
expirationTime
|
|
382
|
+
"microsoft", email, name, null, idToken, accessToken, null,
|
|
383
|
+
pendingMicrosoftScopes.toTypedArray(), expirationTime
|
|
369
384
|
)
|
|
370
385
|
} catch (e: Exception) {
|
|
371
386
|
clearPkceState()
|
|
@@ -373,14 +388,6 @@ object AuthAdapter {
|
|
|
373
388
|
}
|
|
374
389
|
}
|
|
375
390
|
|
|
376
|
-
private fun saveMicrosoftRefreshToken(refreshToken: String) {
|
|
377
|
-
inMemoryMicrosoftRefreshToken = refreshToken
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private fun getMicrosoftRefreshToken(): String? {
|
|
381
|
-
return inMemoryMicrosoftRefreshToken
|
|
382
|
-
}
|
|
383
|
-
|
|
384
391
|
private fun clearPkceState() {
|
|
385
392
|
pendingPkceVerifier = null
|
|
386
393
|
pendingState = null
|
|
@@ -408,10 +415,7 @@ object AuthAdapter {
|
|
|
408
415
|
}
|
|
409
416
|
|
|
410
417
|
private fun getJwtExpirationTimeMs(idToken: String?): Long? {
|
|
411
|
-
if (idToken.isNullOrEmpty())
|
|
412
|
-
return null
|
|
413
|
-
}
|
|
414
|
-
|
|
418
|
+
if (idToken.isNullOrEmpty()) return null
|
|
415
419
|
val expSeconds = decodeJwt(idToken)["exp"]?.toLongOrNull() ?: return null
|
|
416
420
|
return expSeconds * 1000
|
|
417
421
|
}
|
|
@@ -435,7 +439,6 @@ object AuthAdapter {
|
|
|
435
439
|
if (tenant.startsWith("https://")) {
|
|
436
440
|
return if (tenant.endsWith("/")) tenant else "$tenant/"
|
|
437
441
|
}
|
|
438
|
-
|
|
439
442
|
return if (b2cDomain != null) {
|
|
440
443
|
"https://$b2cDomain/tfp/$tenant/"
|
|
441
444
|
} else {
|
|
@@ -456,7 +459,7 @@ object AuthAdapter {
|
|
|
456
459
|
Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
|
|
457
460
|
return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
|
|
458
461
|
}
|
|
459
|
-
|
|
462
|
+
|
|
460
463
|
val credentialManager = CredentialManager.create(activity)
|
|
461
464
|
val googleIdOption = GetGoogleIdOption.Builder()
|
|
462
465
|
.setFilterByAuthorizedAccounts(false)
|
|
@@ -468,7 +471,7 @@ object AuthAdapter {
|
|
|
468
471
|
.addCredentialOption(googleIdOption)
|
|
469
472
|
.build()
|
|
470
473
|
|
|
471
|
-
|
|
474
|
+
moduleScope.launch(Dispatchers.Main) {
|
|
472
475
|
try {
|
|
473
476
|
val result = credentialManager.getCredential(context = activity, request = request)
|
|
474
477
|
handleCredentialResponse(result, scopes)
|
|
@@ -479,6 +482,7 @@ object AuthAdapter {
|
|
|
479
482
|
}
|
|
480
483
|
}
|
|
481
484
|
|
|
485
|
+
@Suppress("DEPRECATION")
|
|
482
486
|
private fun loginLegacy(
|
|
483
487
|
context: Context,
|
|
484
488
|
clientId: String,
|
|
@@ -488,11 +492,7 @@ object AuthAdapter {
|
|
|
488
492
|
) {
|
|
489
493
|
val ctx = appContext ?: context.applicationContext
|
|
490
494
|
val intent = GoogleSignInActivity.createIntent(
|
|
491
|
-
ctx,
|
|
492
|
-
clientId,
|
|
493
|
-
scopes.toTypedArray(),
|
|
494
|
-
loginHint,
|
|
495
|
-
forceAccountPicker
|
|
495
|
+
ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
|
|
496
496
|
)
|
|
497
497
|
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
498
498
|
ctx.startActivity(intent)
|
|
@@ -521,8 +521,7 @@ object AuthAdapter {
|
|
|
521
521
|
googleIdTokenCredential.displayName,
|
|
522
522
|
googleIdTokenCredential.profilePictureUri?.toString(),
|
|
523
523
|
googleIdTokenCredential.idToken,
|
|
524
|
-
null,
|
|
525
|
-
null,
|
|
524
|
+
null, null,
|
|
526
525
|
scopes.toTypedArray(),
|
|
527
526
|
expirationTime
|
|
528
527
|
)
|
|
@@ -532,6 +531,9 @@ object AuthAdapter {
|
|
|
532
531
|
}
|
|
533
532
|
}
|
|
534
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")
|
|
535
537
|
@JvmStatic
|
|
536
538
|
fun requestScopesSync(context: Context, scopes: Array<String>) {
|
|
537
539
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -561,6 +563,9 @@ object AuthAdapter {
|
|
|
561
563
|
nativeOnLoginError("unknown", "No user logged in")
|
|
562
564
|
}
|
|
563
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")
|
|
564
569
|
@JvmStatic
|
|
565
570
|
fun refreshTokenSync(context: Context) {
|
|
566
571
|
val ctx = appContext ?: context.applicationContext
|
|
@@ -582,18 +587,14 @@ object AuthAdapter {
|
|
|
582
587
|
googleSignInClient!!.silentSignIn().addOnCompleteListener { task ->
|
|
583
588
|
if (task.isSuccessful) {
|
|
584
589
|
val acc = task.result
|
|
585
|
-
nativeOnRefreshSuccess(
|
|
586
|
-
acc?.idToken,
|
|
587
|
-
null,
|
|
588
|
-
getJwtExpirationTimeMs(acc?.idToken),
|
|
589
|
-
)
|
|
590
|
+
nativeOnRefreshSuccess(acc?.idToken, null, getJwtExpirationTimeMs(acc?.idToken))
|
|
590
591
|
} else {
|
|
591
592
|
nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
|
|
592
593
|
}
|
|
593
594
|
}
|
|
594
595
|
return
|
|
595
596
|
}
|
|
596
|
-
val refreshToken =
|
|
597
|
+
val refreshToken = inMemoryMicrosoftRefreshToken
|
|
597
598
|
if (refreshToken != null) {
|
|
598
599
|
refreshMicrosoftTokenForRefresh(ctx, refreshToken)
|
|
599
600
|
return
|
|
@@ -604,37 +605,34 @@ object AuthAdapter {
|
|
|
604
605
|
@JvmStatic
|
|
605
606
|
fun hasPlayServices(context: Context): Boolean {
|
|
606
607
|
val ctx = context.applicationContext ?: appContext ?: return false
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return result == ConnectionResult.SUCCESS
|
|
608
|
+
return GoogleApiAvailability.getInstance()
|
|
609
|
+
.isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
|
|
610
610
|
}
|
|
611
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")
|
|
612
615
|
@JvmStatic
|
|
613
616
|
fun logoutSync(context: Context) {
|
|
614
617
|
val ctx = appContext ?: context.applicationContext
|
|
615
618
|
val clientId = getClientIdFromResources(ctx)
|
|
616
619
|
if (clientId != null) {
|
|
617
620
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
618
|
-
.requestIdToken(clientId)
|
|
619
|
-
.requestServerAuthCode(clientId)
|
|
620
|
-
.requestEmail()
|
|
621
|
-
.build()
|
|
621
|
+
.requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
|
|
622
622
|
GoogleSignIn.getClient(ctx, gso).signOut()
|
|
623
623
|
}
|
|
624
624
|
inMemoryMicrosoftRefreshToken = null
|
|
625
625
|
inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
@Suppress("DEPRECATION")
|
|
628
629
|
@JvmStatic
|
|
629
630
|
fun revokeAccessSync(context: Context) {
|
|
630
631
|
val ctx = appContext ?: context.applicationContext
|
|
631
632
|
val clientId = getClientIdFromResources(ctx)
|
|
632
633
|
if (clientId != null) {
|
|
633
634
|
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
|
634
|
-
.requestIdToken(clientId)
|
|
635
|
-
.requestServerAuthCode(clientId)
|
|
636
|
-
.requestEmail()
|
|
637
|
-
.build()
|
|
635
|
+
.requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
|
|
638
636
|
GoogleSignIn.getClient(ctx, gso).revokeAccess()
|
|
639
637
|
}
|
|
640
638
|
inMemoryMicrosoftRefreshToken = null
|
|
@@ -649,14 +647,15 @@ object AuthAdapter {
|
|
|
649
647
|
@JvmStatic
|
|
650
648
|
fun restoreSession(context: Context) {
|
|
651
649
|
val ctx = context.applicationContext ?: appContext ?: context
|
|
650
|
+
@Suppress("DEPRECATION")
|
|
652
651
|
val account = GoogleSignIn.getLastSignedInAccount(ctx)
|
|
653
652
|
if (account != null) {
|
|
654
653
|
val expirationTime = getJwtExpirationTimeMs(account.idToken)
|
|
655
654
|
nativeOnLoginSuccess("google", account.email, account.displayName,
|
|
656
|
-
|
|
657
|
-
|
|
655
|
+
account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
|
|
656
|
+
account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
|
|
658
657
|
} else {
|
|
659
|
-
val refreshToken =
|
|
658
|
+
val refreshToken = inMemoryMicrosoftRefreshToken
|
|
660
659
|
if (refreshToken != null) {
|
|
661
660
|
refreshMicrosoftToken(ctx, refreshToken)
|
|
662
661
|
} else {
|
|
@@ -669,7 +668,7 @@ object AuthAdapter {
|
|
|
669
668
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
670
669
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
671
670
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
672
|
-
|
|
671
|
+
|
|
673
672
|
if (clientId == null) {
|
|
674
673
|
nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
|
|
675
674
|
return
|
|
@@ -678,7 +677,7 @@ object AuthAdapter {
|
|
|
678
677
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
679
678
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
680
679
|
|
|
681
|
-
|
|
680
|
+
moduleScope.launch {
|
|
682
681
|
try {
|
|
683
682
|
val url = java.net.URL(tokenUrl)
|
|
684
683
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
@@ -691,7 +690,6 @@ object AuthAdapter {
|
|
|
691
690
|
append("&grant_type=refresh_token")
|
|
692
691
|
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
693
692
|
}
|
|
694
|
-
|
|
695
693
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
696
694
|
|
|
697
695
|
val responseCode = connection.responseCode
|
|
@@ -701,7 +699,7 @@ object AuthAdapter {
|
|
|
701
699
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
702
700
|
}
|
|
703
701
|
|
|
704
|
-
|
|
702
|
+
withContext(Dispatchers.Main) {
|
|
705
703
|
if (responseCode == 200) {
|
|
706
704
|
val json = JSONObject(responseBody)
|
|
707
705
|
val newIdToken = json.optString("id_token")
|
|
@@ -709,25 +707,23 @@ object AuthAdapter {
|
|
|
709
707
|
val newRefreshToken = json.optString("refresh_token")
|
|
710
708
|
val expiresIn = json.optLong("expires_in", 0)
|
|
711
709
|
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
712
|
-
|
|
713
710
|
val claims = decodeJwt(newIdToken)
|
|
714
|
-
val email = claims["preferred_username"] ?: claims["email"]
|
|
715
|
-
val name = claims["name"]
|
|
716
711
|
|
|
717
|
-
if (newRefreshToken.isNotEmpty())
|
|
718
|
-
saveMicrosoftRefreshToken(newRefreshToken)
|
|
719
|
-
}
|
|
712
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
720
713
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
721
714
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
722
715
|
}
|
|
723
716
|
|
|
724
|
-
nativeOnLoginSuccess("microsoft",
|
|
717
|
+
nativeOnLoginSuccess("microsoft",
|
|
718
|
+
claims["preferred_username"] ?: claims["email"],
|
|
719
|
+
claims["name"], null,
|
|
720
|
+
newIdToken, newAccessToken, null, null, expirationTime)
|
|
725
721
|
} else {
|
|
726
722
|
nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
|
|
727
723
|
}
|
|
728
724
|
}
|
|
729
725
|
} catch (e: Exception) {
|
|
730
|
-
|
|
726
|
+
withContext(Dispatchers.Main) {
|
|
731
727
|
nativeOnLoginError("network_error", e.message)
|
|
732
728
|
}
|
|
733
729
|
}
|
|
@@ -738,32 +734,38 @@ object AuthAdapter {
|
|
|
738
734
|
val clientId = getMicrosoftClientIdFromResources(context)
|
|
739
735
|
val tenant = getMicrosoftTenantFromResources(context) ?: "common"
|
|
740
736
|
val b2cDomain = getMicrosoftB2cDomainFromResources(context)
|
|
737
|
+
|
|
741
738
|
if (clientId == null) {
|
|
742
739
|
nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
|
|
743
740
|
return
|
|
744
741
|
}
|
|
742
|
+
|
|
745
743
|
val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
|
|
746
744
|
val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
|
|
747
|
-
|
|
745
|
+
|
|
746
|
+
moduleScope.launch {
|
|
748
747
|
try {
|
|
749
748
|
val url = java.net.URL(tokenUrl)
|
|
750
749
|
val connection = url.openConnection() as java.net.HttpURLConnection
|
|
751
750
|
connection.requestMethod = "POST"
|
|
752
751
|
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
|
753
752
|
connection.doOutput = true
|
|
753
|
+
|
|
754
754
|
val postData = buildString {
|
|
755
755
|
append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
|
|
756
756
|
append("&grant_type=refresh_token")
|
|
757
757
|
append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
|
|
758
758
|
}
|
|
759
759
|
connection.outputStream.use { it.write(postData.toByteArray()) }
|
|
760
|
+
|
|
760
761
|
val responseCode = connection.responseCode
|
|
761
762
|
val responseBody = if (responseCode == 200) {
|
|
762
763
|
connection.inputStream.bufferedReader().readText()
|
|
763
764
|
} else {
|
|
764
765
|
connection.errorStream?.bufferedReader()?.readText() ?: ""
|
|
765
766
|
}
|
|
766
|
-
|
|
767
|
+
|
|
768
|
+
withContext(Dispatchers.Main) {
|
|
767
769
|
if (responseCode == 200) {
|
|
768
770
|
val json = JSONObject(responseBody)
|
|
769
771
|
val newIdToken = json.optString("id_token")
|
|
@@ -771,15 +773,12 @@ object AuthAdapter {
|
|
|
771
773
|
val newRefreshToken = json.optString("refresh_token")
|
|
772
774
|
val expiresIn = json.optLong("expires_in", 0)
|
|
773
775
|
val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
val name = claims["name"]
|
|
777
|
-
if (newRefreshToken.isNotEmpty()) {
|
|
778
|
-
saveMicrosoftRefreshToken(newRefreshToken)
|
|
779
|
-
}
|
|
776
|
+
|
|
777
|
+
if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
|
|
780
778
|
inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
|
|
781
779
|
listOf("openid", "email", "profile", "offline_access", "User.Read")
|
|
782
780
|
}
|
|
781
|
+
|
|
783
782
|
nativeOnRefreshSuccess(
|
|
784
783
|
newIdToken.ifEmpty { null },
|
|
785
784
|
newAccessToken.ifEmpty { null },
|
|
@@ -790,11 +789,10 @@ object AuthAdapter {
|
|
|
790
789
|
}
|
|
791
790
|
}
|
|
792
791
|
} catch (e: Exception) {
|
|
793
|
-
|
|
792
|
+
withContext(Dispatchers.Main) {
|
|
794
793
|
nativeOnRefreshError("network_error", e.message)
|
|
795
794
|
}
|
|
796
795
|
}
|
|
797
796
|
}
|
|
798
797
|
}
|
|
799
|
-
|
|
800
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
|
}
|