react-native-nitro-auth 0.5.3 → 0.5.4
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/android/build.gradle +2 -5
- package/android/src/main/cpp/PlatformAuth+Android.cpp +84 -18
- package/android/src/main/java/com/auth/AuthAdapter.kt +27 -13
- 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 +2 -0
- package/lib/commonjs/Auth.web.js +96 -43
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/service.js +2 -2
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/use-auth.js +48 -40
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/module/Auth.web.js +96 -43
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/service.js +2 -2
- package/lib/module/service.js.map +1 -1
- package/lib/module/use-auth.js +48 -40
- package/lib/module/use-auth.js.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +7 -0
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +7 -0
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-nitro-auth.podspec +1 -1
- package/src/Auth.web.ts +124 -50
- package/src/service.ts +2 -2
- package/src/use-auth.ts +98 -66
- package/ios/KeychainStore.swift +0 -43
package/android/build.gradle
CHANGED
|
@@ -93,7 +93,7 @@ dependencies {
|
|
|
93
93
|
implementation project(":react-native-nitro-modules")
|
|
94
94
|
|
|
95
95
|
// Google Sign-In SDK (full scope support)
|
|
96
|
-
implementation "com.google.android.gms:play-services-auth:21.
|
|
96
|
+
implementation "com.google.android.gms:play-services-auth:21.5.1"
|
|
97
97
|
|
|
98
98
|
// Activity result APIs
|
|
99
99
|
implementation "androidx.activity:activity-ktx:1.9.3"
|
|
@@ -104,8 +104,5 @@ dependencies {
|
|
|
104
104
|
// Google Credential Manager (One-Tap / Passkeys)
|
|
105
105
|
implementation "androidx.credentials:credentials:1.5.0"
|
|
106
106
|
implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
|
|
107
|
-
implementation "com.google.android.libraries.identity.googleid:googleid:1.
|
|
108
|
-
|
|
109
|
-
// Secure storage (EncryptedSharedPreferences)
|
|
110
|
-
implementation "androidx.security:security-crypto:1.0.0"
|
|
107
|
+
implementation "com.google.android.libraries.identity.googleid:googleid:1.2.0"
|
|
111
108
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
#include <fbjni/fbjni.h>
|
|
7
7
|
#include <NitroModules/NitroLogger.hpp>
|
|
8
8
|
#include <NitroModules/Promise.hpp>
|
|
9
|
+
#include <exception>
|
|
10
|
+
#include <stdexcept>
|
|
9
11
|
|
|
10
12
|
namespace margelo::nitro::NitroAuth {
|
|
11
13
|
|
|
@@ -24,6 +26,33 @@ static std::shared_ptr<Promise<AuthUser>> gScopesPromise;
|
|
|
24
26
|
static std::shared_ptr<Promise<AuthTokens>> gRefreshPromise;
|
|
25
27
|
static std::shared_ptr<Promise<std::optional<AuthUser>>> gSilentPromise;
|
|
26
28
|
static std::mutex gMutex;
|
|
29
|
+
static jclass gAuthAdapterClass = nullptr;
|
|
30
|
+
static jmethodID gLoginMethod = nullptr;
|
|
31
|
+
static jmethodID gRequestScopesMethod = nullptr;
|
|
32
|
+
|
|
33
|
+
static void ensureAuthAdapterMethods(JNIEnv* env) {
|
|
34
|
+
if (gAuthAdapterClass != nullptr && gLoginMethod != nullptr && gRequestScopesMethod != nullptr) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
jclass localAdapterClass = env->FindClass("com/auth/AuthAdapter");
|
|
39
|
+
if (localAdapterClass == nullptr) {
|
|
40
|
+
throw std::runtime_error("Unable to resolve com/auth/AuthAdapter");
|
|
41
|
+
}
|
|
42
|
+
gAuthAdapterClass = static_cast<jclass>(env->NewGlobalRef(localAdapterClass));
|
|
43
|
+
env->DeleteLocalRef(localAdapterClass);
|
|
44
|
+
|
|
45
|
+
gLoginMethod = env->GetStaticMethodID(
|
|
46
|
+
gAuthAdapterClass,
|
|
47
|
+
"loginSync",
|
|
48
|
+
"(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;)V"
|
|
49
|
+
);
|
|
50
|
+
gRequestScopesMethod = env->GetStaticMethodID(
|
|
51
|
+
gAuthAdapterClass,
|
|
52
|
+
"requestScopesSync",
|
|
53
|
+
"(Landroid/content/Context;[Ljava/lang/String;)V"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
27
56
|
|
|
28
57
|
std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
|
|
29
58
|
auto promise = Promise<AuthUser>::create();
|
|
@@ -35,6 +64,9 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
|
|
|
35
64
|
|
|
36
65
|
{
|
|
37
66
|
std::lock_guard<std::mutex> lock(gMutex);
|
|
67
|
+
if (gLoginPromise) {
|
|
68
|
+
gLoginPromise->reject(std::make_exception_ptr(std::runtime_error("Login request superseded by a newer request")));
|
|
69
|
+
}
|
|
38
70
|
gLoginPromise = promise;
|
|
39
71
|
}
|
|
40
72
|
|
|
@@ -71,30 +103,47 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
|
|
|
71
103
|
}
|
|
72
104
|
|
|
73
105
|
JNIEnv* env = Environment::current();
|
|
106
|
+
try {
|
|
107
|
+
ensureAuthAdapterMethods(env);
|
|
108
|
+
} catch (...) {
|
|
109
|
+
promise->reject(std::current_exception());
|
|
110
|
+
return promise;
|
|
111
|
+
}
|
|
74
112
|
jclass stringClass = env->FindClass("java/lang/String");
|
|
75
113
|
jobjectArray jScopes = env->NewObjectArray(scopes.size(), stringClass, nullptr);
|
|
76
114
|
for (size_t i = 0; i < scopes.size(); i++) {
|
|
77
115
|
env->SetObjectArrayElement(jScopes, i, make_jstring(scopes[i]).get());
|
|
78
116
|
}
|
|
79
117
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
local_ref<JString> providerRef = make_jstring(providerStr);
|
|
119
|
+
local_ref<JString> loginHintRef;
|
|
120
|
+
local_ref<JString> tenantRef;
|
|
121
|
+
local_ref<JString> promptRef;
|
|
122
|
+
|
|
123
|
+
if (loginHint.has_value()) {
|
|
124
|
+
loginHintRef = make_jstring(loginHint.value());
|
|
125
|
+
}
|
|
126
|
+
if (tenant.has_value()) {
|
|
127
|
+
tenantRef = make_jstring(tenant.value());
|
|
128
|
+
}
|
|
129
|
+
if (prompt.has_value()) {
|
|
130
|
+
promptRef = make_jstring(prompt.value());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
env->CallStaticVoidMethod(gAuthAdapterClass, gLoginMethod,
|
|
88
134
|
contextPtr,
|
|
89
|
-
|
|
135
|
+
providerRef.get(),
|
|
90
136
|
nullptr,
|
|
91
137
|
jScopes,
|
|
92
|
-
|
|
138
|
+
loginHintRef.get(),
|
|
93
139
|
(jboolean)useOneTap,
|
|
94
140
|
(jboolean)forceAccountPicker,
|
|
95
141
|
(jboolean)useLegacyGoogleSignIn,
|
|
96
|
-
|
|
97
|
-
|
|
142
|
+
tenantRef.get(),
|
|
143
|
+
promptRef.get());
|
|
144
|
+
|
|
145
|
+
env->DeleteLocalRef(jScopes);
|
|
146
|
+
env->DeleteLocalRef(stringClass);
|
|
98
147
|
|
|
99
148
|
return promise;
|
|
100
149
|
}
|
|
@@ -109,20 +158,28 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
|
|
|
109
158
|
|
|
110
159
|
{
|
|
111
160
|
std::lock_guard<std::mutex> lock(gMutex);
|
|
161
|
+
if (gScopesPromise) {
|
|
162
|
+
gScopesPromise->reject(std::make_exception_ptr(std::runtime_error("Scope request superseded by a newer request")));
|
|
163
|
+
}
|
|
112
164
|
gScopesPromise = promise;
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
JNIEnv* env = Environment::current();
|
|
168
|
+
try {
|
|
169
|
+
ensureAuthAdapterMethods(env);
|
|
170
|
+
} catch (...) {
|
|
171
|
+
promise->reject(std::current_exception());
|
|
172
|
+
return promise;
|
|
173
|
+
}
|
|
116
174
|
jclass stringClass = env->FindClass("java/lang/String");
|
|
117
175
|
jobjectArray jScopes = env->NewObjectArray(scopes.size(), stringClass, nullptr);
|
|
118
176
|
for (size_t i = 0; i < scopes.size(); i++) {
|
|
119
177
|
env->SetObjectArrayElement(jScopes, i, make_jstring(scopes[i]).get());
|
|
120
178
|
}
|
|
121
179
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
env->CallStaticVoidMethod(adapterClass, requestMethod, contextPtr, jScopes);
|
|
180
|
+
env->CallStaticVoidMethod(gAuthAdapterClass, gRequestScopesMethod, contextPtr, jScopes);
|
|
181
|
+
env->DeleteLocalRef(jScopes);
|
|
182
|
+
env->DeleteLocalRef(stringClass);
|
|
126
183
|
return promise;
|
|
127
184
|
}
|
|
128
185
|
|
|
@@ -136,6 +193,9 @@ std::shared_ptr<Promise<AuthTokens>> PlatformAuth::refreshToken() {
|
|
|
136
193
|
|
|
137
194
|
{
|
|
138
195
|
std::lock_guard<std::mutex> lock(gMutex);
|
|
196
|
+
if (gRefreshPromise) {
|
|
197
|
+
gRefreshPromise->reject(std::make_exception_ptr(std::runtime_error("Refresh request superseded by a newer request")));
|
|
198
|
+
}
|
|
139
199
|
gRefreshPromise = promise;
|
|
140
200
|
}
|
|
141
201
|
|
|
@@ -155,6 +215,9 @@ std::shared_ptr<Promise<std::optional<AuthUser>>> PlatformAuth::silentRestore()
|
|
|
155
215
|
|
|
156
216
|
{
|
|
157
217
|
std::lock_guard<std::mutex> lock(gMutex);
|
|
218
|
+
if (gSilentPromise) {
|
|
219
|
+
gSilentPromise->reject(std::make_exception_ptr(std::runtime_error("Silent restore superseded by a newer request")));
|
|
220
|
+
}
|
|
158
221
|
gSilentPromise = promise;
|
|
159
222
|
}
|
|
160
223
|
|
|
@@ -182,8 +245,8 @@ void PlatformAuth::logout() {
|
|
|
182
245
|
logoutMethod(JAuthAdapter::javaClassStatic(), static_ref_cast<JContext>(jContext));
|
|
183
246
|
}
|
|
184
247
|
|
|
185
|
-
extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeInitialize(JNIEnv
|
|
186
|
-
AuthCache::setAndroidContext(
|
|
248
|
+
extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeInitialize(JNIEnv*, jclass, jobject context) {
|
|
249
|
+
AuthCache::setAndroidContext(context);
|
|
187
250
|
}
|
|
188
251
|
|
|
189
252
|
extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess(
|
|
@@ -253,6 +316,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
|
|
|
253
316
|
const char* s = env->GetStringUTFChars(jstr, nullptr);
|
|
254
317
|
scopeVec.push_back(std::string(s));
|
|
255
318
|
env->ReleaseStringUTFChars(jstr, s);
|
|
319
|
+
env->DeleteLocalRef(jstr);
|
|
256
320
|
}
|
|
257
321
|
user.scopes = scopeVec;
|
|
258
322
|
}
|
|
@@ -260,6 +324,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
|
|
|
260
324
|
jclass longClass = env->FindClass("java/lang/Long");
|
|
261
325
|
jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
|
|
262
326
|
user.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
|
|
327
|
+
env->DeleteLocalRef(longClass);
|
|
263
328
|
}
|
|
264
329
|
|
|
265
330
|
if (loginPromise) loginPromise->resolve(user);
|
|
@@ -328,6 +393,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshSucce
|
|
|
328
393
|
jclass longClass = env->FindClass("java/lang/Long");
|
|
329
394
|
jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
|
|
330
395
|
tokens.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
|
|
396
|
+
env->DeleteLocalRef(longClass);
|
|
331
397
|
}
|
|
332
398
|
refreshPromise->resolve(tokens);
|
|
333
399
|
}
|
|
@@ -32,9 +32,13 @@ import android.util.Base64
|
|
|
32
32
|
object AuthAdapter {
|
|
33
33
|
private const val TAG = "AuthAdapter"
|
|
34
34
|
|
|
35
|
+
@Volatile
|
|
36
|
+
private var isInitialized = false
|
|
37
|
+
|
|
35
38
|
private var appContext: Context? = null
|
|
36
39
|
private var currentActivity: Activity? = null
|
|
37
40
|
private var googleSignInClient: GoogleSignInClient? = null
|
|
41
|
+
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
|
38
42
|
private var pendingScopes: List<String> = emptyList()
|
|
39
43
|
private var pendingMicrosoftScopes: List<String> = emptyList()
|
|
40
44
|
|
|
@@ -74,23 +78,33 @@ object AuthAdapter {
|
|
|
74
78
|
@JvmStatic
|
|
75
79
|
private external fun nativeOnRefreshError(error: String, underlyingError: String?)
|
|
76
80
|
|
|
81
|
+
@Synchronized
|
|
77
82
|
fun initialize(context: Context) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
if (isInitialized) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val applicationContext = context.applicationContext
|
|
88
|
+
appContext = applicationContext
|
|
89
|
+
|
|
90
|
+
val app = applicationContext as? Application
|
|
91
|
+
if (app != null && lifecycleCallbacks == null) {
|
|
92
|
+
lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
|
|
93
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
|
|
94
|
+
override fun onActivityStarted(activity: Activity) { currentActivity = activity }
|
|
95
|
+
override fun onActivityResumed(activity: Activity) { currentActivity = activity }
|
|
96
|
+
override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
97
|
+
override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
98
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
99
|
+
override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
|
|
100
|
+
}
|
|
101
|
+
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
try {
|
|
92
105
|
System.loadLibrary("NitroAuth")
|
|
93
|
-
nativeInitialize(
|
|
106
|
+
nativeInitialize(applicationContext)
|
|
107
|
+
isInitialized = true
|
|
94
108
|
} catch (e: Exception) {
|
|
95
109
|
Log.e(TAG, "Failed to load NitroAuth library", e)
|
|
96
110
|
}
|
package/cpp/AuthCache.cpp
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
#include "AuthCache.hpp"
|
|
2
2
|
|
|
3
|
-
#ifdef __APPLE__
|
|
4
|
-
#include <CoreFoundation/CoreFoundation.h>
|
|
5
|
-
#include <Security/Security.h>
|
|
6
|
-
#endif
|
|
7
|
-
|
|
8
3
|
#ifdef __ANDROID__
|
|
9
4
|
#include <jni.h>
|
|
10
5
|
#include <fbjni/fbjni.h>
|
|
@@ -12,34 +7,10 @@
|
|
|
12
7
|
|
|
13
8
|
namespace margelo::nitro::NitroAuth {
|
|
14
9
|
|
|
15
|
-
#ifdef __APPLE__
|
|
16
|
-
static std::string sInMemoryUserJson;
|
|
17
|
-
|
|
18
|
-
void AuthCache::setUserJson(const std::string& json) {
|
|
19
|
-
sInMemoryUserJson = json;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
std::optional<std::string> AuthCache::getUserJson() {
|
|
23
|
-
if (sInMemoryUserJson.empty()) {
|
|
24
|
-
return std::nullopt;
|
|
25
|
-
}
|
|
26
|
-
return sInMemoryUserJson;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
void AuthCache::clear() {
|
|
30
|
-
sInMemoryUserJson.clear();
|
|
31
|
-
}
|
|
32
|
-
#endif
|
|
33
|
-
|
|
34
10
|
#ifdef __ANDROID__
|
|
35
11
|
using namespace facebook::jni;
|
|
36
12
|
|
|
37
|
-
struct JContext : JavaClass<JContext> {
|
|
38
|
-
static constexpr auto kJavaDescriptor = "Landroid/content/Context;";
|
|
39
|
-
};
|
|
40
|
-
|
|
41
13
|
static facebook::jni::global_ref<jobject> gContext;
|
|
42
|
-
static std::string sInMemoryUserJson;
|
|
43
14
|
|
|
44
15
|
void AuthCache::setAndroidContext(void* context) {
|
|
45
16
|
gContext = facebook::jni::make_global(static_cast<jobject>(context));
|
|
@@ -48,21 +19,6 @@ void AuthCache::setAndroidContext(void* context) {
|
|
|
48
19
|
void* AuthCache::getAndroidContext() {
|
|
49
20
|
return gContext.get();
|
|
50
21
|
}
|
|
51
|
-
|
|
52
|
-
void AuthCache::setUserJson(const std::string& json) {
|
|
53
|
-
sInMemoryUserJson = json;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
std::optional<std::string> AuthCache::getUserJson() {
|
|
57
|
-
if (sInMemoryUserJson.empty()) {
|
|
58
|
-
return std::nullopt;
|
|
59
|
-
}
|
|
60
|
-
return sInMemoryUserJson;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
void AuthCache::clear() {
|
|
64
|
-
sInMemoryUserJson.clear();
|
|
65
|
-
}
|
|
66
22
|
#endif
|
|
67
23
|
|
|
68
24
|
} // namespace margelo::nitro::NitroAuth
|
package/cpp/AuthCache.hpp
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#include <string>
|
|
4
|
-
#include <optional>
|
|
5
|
-
|
|
6
3
|
namespace margelo::nitro::NitroAuth {
|
|
7
4
|
|
|
8
5
|
class AuthCache {
|
|
9
6
|
public:
|
|
10
|
-
static void setUserJson(const std::string& json);
|
|
11
|
-
static std::optional<std::string> getUserJson();
|
|
12
|
-
static void clear();
|
|
13
|
-
|
|
14
7
|
#ifdef __ANDROID__
|
|
15
8
|
static void setAndroidContext(void* context);
|
|
16
9
|
static void* getAndroidContext();
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -209,7 +209,16 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
|
|
212
|
-
|
|
212
|
+
std::shared_ptr<Promise<AuthTokens>> promise;
|
|
213
|
+
{
|
|
214
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
215
|
+
if (_refreshInFlight) {
|
|
216
|
+
return _refreshInFlight;
|
|
217
|
+
}
|
|
218
|
+
promise = Promise<AuthTokens>::create();
|
|
219
|
+
_refreshInFlight = promise;
|
|
220
|
+
}
|
|
221
|
+
|
|
213
222
|
auto refreshPromise = PlatformAuth::refreshToken();
|
|
214
223
|
refreshPromise->addOnResolvedListener([this, promise](const AuthTokens& tokens) {
|
|
215
224
|
{
|
|
@@ -228,13 +237,22 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
|
|
|
228
237
|
_currentUser->expirationTime = tokens.expirationTime;
|
|
229
238
|
}
|
|
230
239
|
}
|
|
240
|
+
if (_refreshInFlight == promise) {
|
|
241
|
+
_refreshInFlight = nullptr;
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
notifyTokensRefreshed(tokens);
|
|
233
245
|
notifyAuthStateChanged();
|
|
234
246
|
promise->resolve(tokens);
|
|
235
247
|
});
|
|
236
248
|
|
|
237
|
-
refreshPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
|
|
249
|
+
refreshPromise->addOnRejectedListener([this, promise](const std::exception_ptr& error) {
|
|
250
|
+
{
|
|
251
|
+
std::lock_guard<std::mutex> lock(_mutex);
|
|
252
|
+
if (_refreshInFlight == promise) {
|
|
253
|
+
_refreshInFlight = nullptr;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
238
256
|
promise->reject(error);
|
|
239
257
|
});
|
|
240
258
|
return promise;
|
package/cpp/HybridAuth.hpp
CHANGED
package/ios/AuthAdapter.swift
CHANGED
|
@@ -288,6 +288,8 @@ public class AuthAdapter: NSObject {
|
|
|
288
288
|
guard parts.count >= 2 else { return [:] }
|
|
289
289
|
|
|
290
290
|
var base64 = parts[1]
|
|
291
|
+
.replacingOccurrences(of: "-", with: "+")
|
|
292
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
291
293
|
let remainder = base64.count % 4
|
|
292
294
|
if remainder > 0 {
|
|
293
295
|
base64 += String(repeating: "=", count: 4 - remainder)
|
package/lib/commonjs/Auth.web.js
CHANGED
|
@@ -91,7 +91,9 @@ class AuthWeb {
|
|
|
91
91
|
_grantedScopes = [];
|
|
92
92
|
_listeners = [];
|
|
93
93
|
_tokenListeners = [];
|
|
94
|
+
_browserStorageResolved = false;
|
|
94
95
|
constructor() {
|
|
96
|
+
this._config = getConfig();
|
|
95
97
|
this.loadFromCache();
|
|
96
98
|
}
|
|
97
99
|
isPromiseLike(value) {
|
|
@@ -127,21 +129,27 @@ class AuthWeb {
|
|
|
127
129
|
if (this._storageAdapter) {
|
|
128
130
|
return true;
|
|
129
131
|
}
|
|
130
|
-
return
|
|
132
|
+
return this._config.nitroAuthPersistTokensOnWeb === true;
|
|
131
133
|
}
|
|
132
134
|
getWebStorageMode() {
|
|
133
|
-
const configuredMode =
|
|
135
|
+
const configuredMode = this._config.nitroAuthWebStorage;
|
|
134
136
|
if (configuredMode && WEB_STORAGE_MODES.has(configuredMode)) {
|
|
135
137
|
return configuredMode;
|
|
136
138
|
}
|
|
137
139
|
return STORAGE_MODE_SESSION;
|
|
138
140
|
}
|
|
139
141
|
getBrowserStorage() {
|
|
142
|
+
if (this._browserStorageResolved) {
|
|
143
|
+
return this._browserStorageCache;
|
|
144
|
+
}
|
|
145
|
+
this._browserStorageResolved = true;
|
|
140
146
|
if (typeof window === "undefined") {
|
|
147
|
+
this._browserStorageCache = undefined;
|
|
141
148
|
return undefined;
|
|
142
149
|
}
|
|
143
150
|
const mode = this.getWebStorageMode();
|
|
144
151
|
if (mode === STORAGE_MODE_MEMORY) {
|
|
152
|
+
this._browserStorageCache = undefined;
|
|
145
153
|
return undefined;
|
|
146
154
|
}
|
|
147
155
|
const storage = mode === STORAGE_MODE_LOCAL ? window.localStorage : window.sessionStorage;
|
|
@@ -149,12 +157,14 @@ class AuthWeb {
|
|
|
149
157
|
const testKey = "__nitro_auth_storage_probe__";
|
|
150
158
|
storage.setItem(testKey, "1");
|
|
151
159
|
storage.removeItem(testKey);
|
|
160
|
+
this._browserStorageCache = storage;
|
|
152
161
|
return storage;
|
|
153
162
|
} catch (error) {
|
|
154
163
|
_logger.logger.warn("Configured web storage is unavailable; using in-memory fallback", {
|
|
155
164
|
mode,
|
|
156
165
|
error: String(error)
|
|
157
166
|
});
|
|
167
|
+
this._browserStorageCache = undefined;
|
|
158
168
|
return undefined;
|
|
159
169
|
}
|
|
160
170
|
}
|
|
@@ -371,6 +381,20 @@ class AuthWeb {
|
|
|
371
381
|
return this._currentUser?.accessToken;
|
|
372
382
|
}
|
|
373
383
|
async refreshToken() {
|
|
384
|
+
if (this._refreshPromise) {
|
|
385
|
+
return this._refreshPromise;
|
|
386
|
+
}
|
|
387
|
+
const refreshPromise = this.performRefreshToken();
|
|
388
|
+
this._refreshPromise = refreshPromise;
|
|
389
|
+
try {
|
|
390
|
+
return await refreshPromise;
|
|
391
|
+
} finally {
|
|
392
|
+
if (this._refreshPromise === refreshPromise) {
|
|
393
|
+
this._refreshPromise = undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async performRefreshToken() {
|
|
374
398
|
if (!this._currentUser) {
|
|
375
399
|
throw new Error("No user logged in");
|
|
376
400
|
}
|
|
@@ -380,13 +404,12 @@ class AuthWeb {
|
|
|
380
404
|
if (!refreshToken) {
|
|
381
405
|
throw new Error("No refresh token available");
|
|
382
406
|
}
|
|
383
|
-
const
|
|
384
|
-
const clientId = config.microsoftClientId;
|
|
407
|
+
const clientId = this._config.microsoftClientId;
|
|
385
408
|
if (!clientId) {
|
|
386
409
|
throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
|
|
387
410
|
}
|
|
388
|
-
const tenant =
|
|
389
|
-
const b2cDomain =
|
|
411
|
+
const tenant = this._config.microsoftTenant ?? "common";
|
|
412
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
390
413
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, b2cDomain);
|
|
391
414
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
392
415
|
const body = new URLSearchParams({
|
|
@@ -480,7 +503,9 @@ class AuthWeb {
|
|
|
480
503
|
if (!payload) {
|
|
481
504
|
throw new Error("Invalid JWT payload");
|
|
482
505
|
}
|
|
483
|
-
const
|
|
506
|
+
const normalizedPayload = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
507
|
+
const padding = "=".repeat((4 - normalizedPayload.length % 4) % 4);
|
|
508
|
+
const decoded = JSON.parse(atob(`${normalizedPayload}${padding}`));
|
|
484
509
|
if (!isJsonObject(decoded)) {
|
|
485
510
|
throw new Error("Expected JWT payload to be an object");
|
|
486
511
|
}
|
|
@@ -531,8 +556,7 @@ class AuthWeb {
|
|
|
531
556
|
});
|
|
532
557
|
}
|
|
533
558
|
async loginGoogle(scopes, loginHint) {
|
|
534
|
-
const
|
|
535
|
-
const clientId = config.googleWebClientId;
|
|
559
|
+
const clientId = this._config.googleWebClientId;
|
|
536
560
|
if (!clientId) {
|
|
537
561
|
throw new Error("Google Web Client ID not configured. Add 'GOOGLE_WEB_CLIENT_ID' to your .env file.");
|
|
538
562
|
}
|
|
@@ -603,13 +627,12 @@ class AuthWeb {
|
|
|
603
627
|
}
|
|
604
628
|
}
|
|
605
629
|
async loginMicrosoft(scopes, loginHint, tenant, prompt) {
|
|
606
|
-
const
|
|
607
|
-
const clientId = config.microsoftClientId;
|
|
630
|
+
const clientId = this._config.microsoftClientId;
|
|
608
631
|
if (!clientId) {
|
|
609
632
|
throw new Error("Microsoft Client ID not configured. Add 'microsoftClientId' to expo.extra in your app.config.js");
|
|
610
633
|
}
|
|
611
|
-
const effectiveTenant = tenant ??
|
|
612
|
-
const b2cDomain =
|
|
634
|
+
const effectiveTenant = tenant ?? this._config.microsoftTenant ?? "common";
|
|
635
|
+
const b2cDomain = this._config.microsoftB2cDomain;
|
|
613
636
|
const authBaseUrl = this.getMicrosoftAuthBaseUrl(effectiveTenant, b2cDomain);
|
|
614
637
|
const effectiveScopes = scopes.length ? scopes : ["openid", "email", "profile", "offline_access", "User.Read"];
|
|
615
638
|
const codeVerifier = this.generateCodeVerifier();
|
|
@@ -680,8 +703,7 @@ class AuthWeb {
|
|
|
680
703
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
681
704
|
}
|
|
682
705
|
async exchangeMicrosoftCodeForTokens(code, codeVerifier, clientId, redirectUri, tenant, expectedNonce, scopes) {
|
|
683
|
-
const
|
|
684
|
-
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, config.microsoftB2cDomain);
|
|
706
|
+
const authBaseUrl = this.getMicrosoftAuthBaseUrl(tenant, this._config.microsoftB2cDomain);
|
|
685
707
|
const tokenUrl = `${authBaseUrl}oauth2/v2.0/token`;
|
|
686
708
|
const body = new URLSearchParams({
|
|
687
709
|
client_id: clientId,
|
|
@@ -753,45 +775,76 @@ class AuthWeb {
|
|
|
753
775
|
return {};
|
|
754
776
|
}
|
|
755
777
|
}
|
|
756
|
-
async
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (!clientId) {
|
|
760
|
-
throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
|
|
778
|
+
async ensureAppleSdkLoaded() {
|
|
779
|
+
if (window.AppleID) {
|
|
780
|
+
return;
|
|
761
781
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
782
|
+
if (this._appleSdkLoadPromise) {
|
|
783
|
+
return this._appleSdkLoadPromise;
|
|
784
|
+
}
|
|
785
|
+
this._appleSdkLoadPromise = new Promise((resolve, reject) => {
|
|
786
|
+
const scriptId = "nitro-auth-apple-sdk";
|
|
787
|
+
const existingScript = document.getElementById(scriptId);
|
|
788
|
+
if (existingScript) {
|
|
789
|
+
if (window.AppleID) {
|
|
790
|
+
resolve();
|
|
769
791
|
return;
|
|
770
792
|
}
|
|
771
|
-
|
|
772
|
-
clientId,
|
|
773
|
-
scope: "name email",
|
|
774
|
-
redirectURI: window.location.origin,
|
|
775
|
-
usePopup: true
|
|
776
|
-
});
|
|
777
|
-
window.AppleID.auth.signIn().then(response => {
|
|
778
|
-
const user = {
|
|
779
|
-
provider: "apple",
|
|
780
|
-
idToken: response.authorization.id_token,
|
|
781
|
-
email: response.user?.email,
|
|
782
|
-
name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
|
|
783
|
-
};
|
|
784
|
-
this.updateUser(user);
|
|
793
|
+
existingScript.addEventListener("load", () => {
|
|
785
794
|
resolve();
|
|
786
|
-
}
|
|
787
|
-
|
|
795
|
+
}, {
|
|
796
|
+
once: true
|
|
797
|
+
});
|
|
798
|
+
existingScript.addEventListener("error", () => {
|
|
799
|
+
this._appleSdkLoadPromise = undefined;
|
|
800
|
+
reject(new Error("Failed to load Apple SDK"));
|
|
801
|
+
}, {
|
|
802
|
+
once: true
|
|
788
803
|
});
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const script = document.createElement("script");
|
|
807
|
+
script.id = scriptId;
|
|
808
|
+
script.src = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
|
|
809
|
+
script.async = true;
|
|
810
|
+
script.onload = () => {
|
|
811
|
+
resolve();
|
|
789
812
|
};
|
|
790
813
|
script.onerror = () => {
|
|
814
|
+
this._appleSdkLoadPromise = undefined;
|
|
791
815
|
reject(new Error("Failed to load Apple SDK"));
|
|
792
816
|
};
|
|
793
817
|
document.head.appendChild(script);
|
|
794
818
|
});
|
|
819
|
+
return this._appleSdkLoadPromise;
|
|
820
|
+
}
|
|
821
|
+
async loginApple() {
|
|
822
|
+
const clientId = this._config.appleWebClientId;
|
|
823
|
+
if (!clientId) {
|
|
824
|
+
throw new Error("Apple Web Client ID not configured. Add 'APPLE_WEB_CLIENT_ID' to your .env file.");
|
|
825
|
+
}
|
|
826
|
+
await this.ensureAppleSdkLoaded();
|
|
827
|
+
if (!window.AppleID) {
|
|
828
|
+
throw new Error("Apple SDK not loaded");
|
|
829
|
+
}
|
|
830
|
+
window.AppleID.auth.init({
|
|
831
|
+
clientId,
|
|
832
|
+
scope: "name email",
|
|
833
|
+
redirectURI: window.location.origin,
|
|
834
|
+
usePopup: true
|
|
835
|
+
});
|
|
836
|
+
try {
|
|
837
|
+
const response = await window.AppleID.auth.signIn();
|
|
838
|
+
const user = {
|
|
839
|
+
provider: "apple",
|
|
840
|
+
idToken: response.authorization.id_token,
|
|
841
|
+
email: response.user?.email,
|
|
842
|
+
name: response.user?.name ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim() : undefined
|
|
843
|
+
};
|
|
844
|
+
this.updateUser(user);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
throw this.mapError(error);
|
|
847
|
+
}
|
|
795
848
|
}
|
|
796
849
|
async silentRestore() {
|
|
797
850
|
_logger.logger.log("Attempting silent restore...");
|