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