@capgo/capacitor-social-login 8.2.25 → 8.3.1
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/Package.swift +1 -1
- package/README.md +191 -22
- package/android/src/main/java/ee/forgr/capacitor/social/login/OAuth2Provider.java +464 -82
- package/android/src/main/java/ee/forgr/capacitor/social/login/SocialLoginPlugin.java +93 -1
- package/dist/docs.json +317 -5
- package/dist/esm/definitions.d.ts +187 -5
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/oauth2-provider.d.ts +18 -1
- package/dist/esm/oauth2-provider.js +227 -40
- package/dist/esm/oauth2-provider.js.map +1 -1
- package/dist/esm/web.d.ts +37 -2
- package/dist/esm/web.js +77 -17
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +304 -57
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +304 -57
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/SocialLoginPlugin/OAuth2Provider.swift +281 -103
- package/ios/Sources/SocialLoginPlugin/SocialLoginPlugin.swift +129 -1
- package/package.json +7 -7
|
@@ -55,6 +55,7 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
55
55
|
private static class OAuth2ProviderConfig {
|
|
56
56
|
|
|
57
57
|
final String appId;
|
|
58
|
+
final String issuerUrl;
|
|
58
59
|
final String authorizationBaseUrl;
|
|
59
60
|
final String accessTokenEndpoint;
|
|
60
61
|
final String redirectUrl;
|
|
@@ -63,12 +64,18 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
63
64
|
final boolean pkceEnabled;
|
|
64
65
|
final String scope;
|
|
65
66
|
final Map<String, String> additionalParameters;
|
|
67
|
+
final String loginHint;
|
|
68
|
+
final String prompt;
|
|
69
|
+
final Map<String, String> additionalTokenParameters;
|
|
66
70
|
final Map<String, String> additionalResourceHeaders;
|
|
67
71
|
final String logoutUrl;
|
|
72
|
+
final String postLogoutRedirectUrl;
|
|
73
|
+
final Map<String, String> additionalLogoutParameters;
|
|
68
74
|
final boolean logsEnabled;
|
|
69
75
|
|
|
70
76
|
OAuth2ProviderConfig(
|
|
71
77
|
String appId,
|
|
78
|
+
String issuerUrl,
|
|
72
79
|
String authorizationBaseUrl,
|
|
73
80
|
String accessTokenEndpoint,
|
|
74
81
|
String redirectUrl,
|
|
@@ -77,11 +84,17 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
77
84
|
boolean pkceEnabled,
|
|
78
85
|
String scope,
|
|
79
86
|
Map<String, String> additionalParameters,
|
|
87
|
+
String loginHint,
|
|
88
|
+
String prompt,
|
|
89
|
+
Map<String, String> additionalTokenParameters,
|
|
80
90
|
Map<String, String> additionalResourceHeaders,
|
|
81
91
|
String logoutUrl,
|
|
92
|
+
String postLogoutRedirectUrl,
|
|
93
|
+
Map<String, String> additionalLogoutParameters,
|
|
82
94
|
boolean logsEnabled
|
|
83
95
|
) {
|
|
84
96
|
this.appId = appId;
|
|
97
|
+
this.issuerUrl = issuerUrl;
|
|
85
98
|
this.authorizationBaseUrl = authorizationBaseUrl;
|
|
86
99
|
this.accessTokenEndpoint = accessTokenEndpoint;
|
|
87
100
|
this.redirectUrl = redirectUrl;
|
|
@@ -90,8 +103,13 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
90
103
|
this.pkceEnabled = pkceEnabled;
|
|
91
104
|
this.scope = scope;
|
|
92
105
|
this.additionalParameters = additionalParameters;
|
|
106
|
+
this.loginHint = loginHint;
|
|
107
|
+
this.prompt = prompt;
|
|
108
|
+
this.additionalTokenParameters = additionalTokenParameters;
|
|
93
109
|
this.additionalResourceHeaders = additionalResourceHeaders;
|
|
94
110
|
this.logoutUrl = logoutUrl;
|
|
111
|
+
this.postLogoutRedirectUrl = postLogoutRedirectUrl;
|
|
112
|
+
this.additionalLogoutParameters = additionalLogoutParameters;
|
|
95
113
|
this.logsEnabled = logsEnabled;
|
|
96
114
|
}
|
|
97
115
|
}
|
|
@@ -119,6 +137,100 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
119
137
|
this.httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
|
|
120
138
|
}
|
|
121
139
|
|
|
140
|
+
private interface DiscoveryCallback {
|
|
141
|
+
void onSuccess(OAuth2ProviderConfig config);
|
|
142
|
+
|
|
143
|
+
void onError(String message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static String trimTrailingSlashes(String s) {
|
|
147
|
+
if (s == null) return null;
|
|
148
|
+
int end = s.length();
|
|
149
|
+
while (end > 0 && s.charAt(end - 1) == '/') end--;
|
|
150
|
+
return s.substring(0, end);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private void ensureDiscovered(String providerId, OAuth2ProviderConfig config, DiscoveryCallback cb) {
|
|
154
|
+
if (config == null) {
|
|
155
|
+
cb.onError("OAuth2 provider '" + providerId + "' not found");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (config.issuerUrl == null || config.issuerUrl.isEmpty()) {
|
|
159
|
+
cb.onSuccess(config);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Already resolved enough for auth.
|
|
163
|
+
if (config.authorizationBaseUrl != null && !"code".equals(config.responseType)) {
|
|
164
|
+
cb.onSuccess(config);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (config.authorizationBaseUrl != null && config.accessTokenEndpoint != null) {
|
|
168
|
+
cb.onSuccess(config);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
String issuer = trimTrailingSlashes(config.issuerUrl);
|
|
173
|
+
String discoveryUrl = issuer + "/.well-known/openid-configuration";
|
|
174
|
+
Request req = new Request.Builder().url(discoveryUrl).get().build();
|
|
175
|
+
if (config.logsEnabled) {
|
|
176
|
+
Log.d(LOG_TAG, "Discovering OIDC configuration at: " + discoveryUrl);
|
|
177
|
+
}
|
|
178
|
+
httpClient
|
|
179
|
+
.newCall(req)
|
|
180
|
+
.enqueue(
|
|
181
|
+
new Callback() {
|
|
182
|
+
@Override
|
|
183
|
+
public void onFailure(Call call, IOException e) {
|
|
184
|
+
cb.onError("OIDC discovery failed: " + e.getMessage());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Override
|
|
188
|
+
public void onResponse(Call call, Response response) throws IOException {
|
|
189
|
+
if (!response.isSuccessful()) {
|
|
190
|
+
cb.onError("OIDC discovery failed: HTTP " + response.code());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
String body = response.body() != null ? response.body().string() : "";
|
|
194
|
+
try {
|
|
195
|
+
JSONObject json = new JSONObject(body);
|
|
196
|
+
String auth = json.optString("authorization_endpoint", null);
|
|
197
|
+
String token = json.optString("token_endpoint", null);
|
|
198
|
+
String endSession = json.optString("end_session_endpoint", null);
|
|
199
|
+
|
|
200
|
+
OAuth2ProviderConfig resolved = new OAuth2ProviderConfig(
|
|
201
|
+
config.appId,
|
|
202
|
+
config.issuerUrl,
|
|
203
|
+
(config.authorizationBaseUrl != null && !config.authorizationBaseUrl.isEmpty())
|
|
204
|
+
? config.authorizationBaseUrl
|
|
205
|
+
: auth,
|
|
206
|
+
(config.accessTokenEndpoint != null && !config.accessTokenEndpoint.isEmpty())
|
|
207
|
+
? config.accessTokenEndpoint
|
|
208
|
+
: token,
|
|
209
|
+
config.redirectUrl,
|
|
210
|
+
config.resourceUrl,
|
|
211
|
+
config.responseType,
|
|
212
|
+
config.pkceEnabled,
|
|
213
|
+
config.scope,
|
|
214
|
+
config.additionalParameters,
|
|
215
|
+
config.loginHint,
|
|
216
|
+
config.prompt,
|
|
217
|
+
config.additionalTokenParameters,
|
|
218
|
+
config.additionalResourceHeaders,
|
|
219
|
+
(config.logoutUrl != null && !config.logoutUrl.isEmpty()) ? config.logoutUrl : endSession,
|
|
220
|
+
config.postLogoutRedirectUrl,
|
|
221
|
+
config.additionalLogoutParameters,
|
|
222
|
+
config.logsEnabled
|
|
223
|
+
);
|
|
224
|
+
providers.put(providerId, resolved);
|
|
225
|
+
cb.onSuccess(resolved);
|
|
226
|
+
} catch (JSONException e) {
|
|
227
|
+
cb.onError("Failed to parse OIDC discovery response");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
122
234
|
/**
|
|
123
235
|
* Initialize multiple OAuth2 providers from a map of configs.
|
|
124
236
|
* @param configs Map of providerId -> config JSONObject
|
|
@@ -133,44 +245,67 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
133
245
|
JSONObject config = configs.getJSONObject(providerId);
|
|
134
246
|
|
|
135
247
|
String appId = config.optString("appId", null);
|
|
248
|
+
if (appId == null || appId.isEmpty()) {
|
|
249
|
+
appId = config.optString("clientId", null);
|
|
250
|
+
}
|
|
251
|
+
String issuerUrl = config.optString("issuerUrl", null);
|
|
136
252
|
String authorizationBaseUrl = config.optString("authorizationBaseUrl", null);
|
|
253
|
+
if (authorizationBaseUrl == null || authorizationBaseUrl.isEmpty()) {
|
|
254
|
+
authorizationBaseUrl = config.optString("authorizationEndpoint", null);
|
|
255
|
+
}
|
|
137
256
|
String redirectUrl = config.optString("redirectUrl", null);
|
|
138
257
|
|
|
139
258
|
if (appId == null || appId.isEmpty()) {
|
|
140
|
-
errors.add("oauth2." + providerId + ".appId is required");
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (authorizationBaseUrl == null || authorizationBaseUrl.isEmpty()) {
|
|
144
|
-
errors.add("oauth2." + providerId + ".authorizationBaseUrl is required");
|
|
259
|
+
errors.add("oauth2." + providerId + ".appId (or clientId) is required");
|
|
145
260
|
continue;
|
|
146
261
|
}
|
|
147
262
|
if (redirectUrl == null || redirectUrl.isEmpty()) {
|
|
148
263
|
errors.add("oauth2." + providerId + ".redirectUrl is required");
|
|
149
264
|
continue;
|
|
150
265
|
}
|
|
266
|
+
if ((authorizationBaseUrl == null || authorizationBaseUrl.isEmpty()) && (issuerUrl == null || issuerUrl.isEmpty())) {
|
|
267
|
+
errors.add("oauth2." + providerId + ".authorizationBaseUrl (or authorizationEndpoint) or issuerUrl is required");
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
151
270
|
|
|
152
271
|
Map<String, String> additionalParameters = null;
|
|
153
272
|
if (config.has("additionalParameters")) {
|
|
154
273
|
additionalParameters = jsonObjectToMap(config.getJSONObject("additionalParameters"));
|
|
155
274
|
}
|
|
156
275
|
|
|
276
|
+
Map<String, String> additionalTokenParameters = null;
|
|
277
|
+
if (config.has("additionalTokenParameters")) {
|
|
278
|
+
additionalTokenParameters = jsonObjectToMap(config.getJSONObject("additionalTokenParameters"));
|
|
279
|
+
}
|
|
280
|
+
|
|
157
281
|
Map<String, String> additionalResourceHeaders = null;
|
|
158
282
|
if (config.has("additionalResourceHeaders")) {
|
|
159
283
|
additionalResourceHeaders = jsonObjectToMap(config.getJSONObject("additionalResourceHeaders"));
|
|
160
284
|
}
|
|
161
285
|
|
|
286
|
+
Map<String, String> additionalLogoutParameters = null;
|
|
287
|
+
if (config.has("additionalLogoutParameters")) {
|
|
288
|
+
additionalLogoutParameters = jsonObjectToMap(config.getJSONObject("additionalLogoutParameters"));
|
|
289
|
+
}
|
|
290
|
+
|
|
162
291
|
OAuth2ProviderConfig providerConfig = new OAuth2ProviderConfig(
|
|
163
292
|
appId,
|
|
164
|
-
|
|
165
|
-
|
|
293
|
+
issuerUrl,
|
|
294
|
+
(authorizationBaseUrl != null && !authorizationBaseUrl.isEmpty()) ? authorizationBaseUrl : null,
|
|
295
|
+
config.has("accessTokenEndpoint") ? config.optString("accessTokenEndpoint", null) : config.optString("tokenEndpoint", null),
|
|
166
296
|
redirectUrl,
|
|
167
297
|
config.optString("resourceUrl", null),
|
|
168
298
|
config.optString("responseType", "code"),
|
|
169
299
|
config.optBoolean("pkceEnabled", true),
|
|
170
|
-
config.
|
|
300
|
+
normalizeScopeValue(config.has("scope") ? config.opt("scope") : config.opt("scopes")),
|
|
171
301
|
additionalParameters,
|
|
302
|
+
config.optString("loginHint", null),
|
|
303
|
+
config.optString("prompt", null),
|
|
304
|
+
additionalTokenParameters,
|
|
172
305
|
additionalResourceHeaders,
|
|
173
|
-
config.optString("logoutUrl", null),
|
|
306
|
+
config.has("logoutUrl") ? config.optString("logoutUrl", null) : config.optString("endSessionEndpoint", null),
|
|
307
|
+
config.optString("postLogoutRedirectUrl", null),
|
|
308
|
+
additionalLogoutParameters,
|
|
174
309
|
config.optBoolean("logsEnabled", false)
|
|
175
310
|
);
|
|
176
311
|
|
|
@@ -226,8 +361,11 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
226
361
|
}
|
|
227
362
|
|
|
228
363
|
String loginScope = providerConfig.scope;
|
|
229
|
-
if (config.has("scope")) {
|
|
230
|
-
|
|
364
|
+
if (config.has("scope") || config.has("scopes")) {
|
|
365
|
+
String normalized = normalizeScopeValue(config.has("scope") ? config.opt("scope") : config.opt("scopes"));
|
|
366
|
+
if (normalized != null && !normalized.isEmpty()) {
|
|
367
|
+
loginScope = normalized;
|
|
368
|
+
}
|
|
231
369
|
}
|
|
232
370
|
|
|
233
371
|
String redirect = providerConfig.redirectUrl;
|
|
@@ -237,7 +375,10 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
237
375
|
|
|
238
376
|
String state = config.has("state") ? config.optString("state", UUID.randomUUID().toString()) : UUID.randomUUID().toString();
|
|
239
377
|
|
|
240
|
-
String codeVerifier =
|
|
378
|
+
String codeVerifier = config.has("codeVerifier") ? config.optString("codeVerifier", null) : null;
|
|
379
|
+
if (codeVerifier == null || codeVerifier.isEmpty()) {
|
|
380
|
+
codeVerifier = generateCodeVerifier();
|
|
381
|
+
}
|
|
241
382
|
String codeChallenge;
|
|
242
383
|
try {
|
|
243
384
|
codeChallenge = generateCodeChallenge(codeVerifier);
|
|
@@ -246,55 +387,89 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
246
387
|
return;
|
|
247
388
|
}
|
|
248
389
|
|
|
249
|
-
|
|
250
|
-
|
|
390
|
+
final String finalState = state;
|
|
391
|
+
final String finalCodeVerifier = codeVerifier;
|
|
392
|
+
final String finalRedirect = redirect;
|
|
393
|
+
final String finalLoginScope = loginScope;
|
|
394
|
+
final String finalCodeChallenge = codeChallenge;
|
|
395
|
+
|
|
396
|
+
// Resolve endpoints via discovery if needed, then start the login activity.
|
|
397
|
+
ensureDiscovered(
|
|
398
|
+
providerId,
|
|
399
|
+
providerConfig,
|
|
400
|
+
new DiscoveryCallback() {
|
|
401
|
+
@Override
|
|
402
|
+
public void onSuccess(OAuth2ProviderConfig resolved) {
|
|
403
|
+
if (resolved.authorizationBaseUrl == null || resolved.authorizationBaseUrl.isEmpty()) {
|
|
404
|
+
call.reject("Missing authorization endpoint (discovery may have failed)");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
251
407
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.appendQueryParameter("response_type", providerConfig.responseType)
|
|
255
|
-
.appendQueryParameter("client_id", providerConfig.appId)
|
|
256
|
-
.appendQueryParameter("redirect_uri", redirect)
|
|
257
|
-
.appendQueryParameter("state", state);
|
|
408
|
+
pendingState = new OAuth2PendingState(providerId, finalState, finalCodeVerifier, finalRedirect, finalLoginScope);
|
|
409
|
+
pendingCall = call;
|
|
258
410
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
411
|
+
Uri.Builder builder = Uri.parse(resolved.authorizationBaseUrl)
|
|
412
|
+
.buildUpon()
|
|
413
|
+
.appendQueryParameter("response_type", resolved.responseType)
|
|
414
|
+
.appendQueryParameter("client_id", resolved.appId)
|
|
415
|
+
.appendQueryParameter("redirect_uri", finalRedirect)
|
|
416
|
+
.appendQueryParameter("state", finalState);
|
|
262
417
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
builder.appendQueryParameter("code_challenge_method", "S256");
|
|
267
|
-
}
|
|
418
|
+
if (!finalLoginScope.isEmpty()) {
|
|
419
|
+
builder.appendQueryParameter("scope", finalLoginScope);
|
|
420
|
+
}
|
|
268
421
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
422
|
+
// Add PKCE for code flow
|
|
423
|
+
if ("code".equals(resolved.responseType) && resolved.pkceEnabled) {
|
|
424
|
+
builder.appendQueryParameter("code_challenge", finalCodeChallenge);
|
|
425
|
+
builder.appendQueryParameter("code_challenge_method", "S256");
|
|
426
|
+
}
|
|
275
427
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
428
|
+
// Additional params: config + per-login
|
|
429
|
+
if (resolved.additionalParameters != null) {
|
|
430
|
+
for (Map.Entry<String, String> entry : resolved.additionalParameters.entrySet()) {
|
|
431
|
+
builder.appendQueryParameter(entry.getKey(), entry.getValue());
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (config.has("additionalParameters")) {
|
|
435
|
+
try {
|
|
436
|
+
JSONObject loginParams = config.getJSONObject("additionalParameters");
|
|
437
|
+
Iterator<String> keys = loginParams.keys();
|
|
438
|
+
while (keys.hasNext()) {
|
|
439
|
+
String key = keys.next();
|
|
440
|
+
builder.appendQueryParameter(key, loginParams.getString(key));
|
|
441
|
+
}
|
|
442
|
+
} catch (JSONException e) {
|
|
443
|
+
Log.w(LOG_TAG, "Failed to parse additionalParameters", e);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
289
446
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
447
|
+
// Convenience OIDC params
|
|
448
|
+
String loginHint = config.optString("loginHint", resolved.loginHint);
|
|
449
|
+
if (loginHint != null && !loginHint.isEmpty()) {
|
|
450
|
+
builder.appendQueryParameter("login_hint", loginHint);
|
|
451
|
+
}
|
|
452
|
+
String prompt = config.optString("prompt", resolved.prompt);
|
|
453
|
+
if (prompt != null && !prompt.isEmpty()) {
|
|
454
|
+
builder.appendQueryParameter("prompt", prompt);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (resolved.logsEnabled) {
|
|
458
|
+
Log.d(LOG_TAG, "Opening authorization URL: " + builder.build().toString());
|
|
459
|
+
}
|
|
293
460
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
461
|
+
Intent intent = new Intent(activity, OAuth2LoginActivity.class);
|
|
462
|
+
intent.putExtra(OAuth2LoginActivity.EXTRA_AUTH_URL, builder.build().toString());
|
|
463
|
+
intent.putExtra(OAuth2LoginActivity.EXTRA_REDIRECT_URL, finalRedirect);
|
|
464
|
+
activity.runOnUiThread(() -> activity.startActivityForResult(intent, REQUEST_CODE));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@Override
|
|
468
|
+
public void onError(String message) {
|
|
469
|
+
call.reject(message);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
);
|
|
298
473
|
}
|
|
299
474
|
|
|
300
475
|
@Override
|
|
@@ -305,15 +480,43 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
305
480
|
return;
|
|
306
481
|
}
|
|
307
482
|
|
|
483
|
+
OAuth2StoredTokens stored = loadStoredTokens(providerId);
|
|
308
484
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().remove(getTokenStorageKey(providerId)).apply();
|
|
309
485
|
|
|
310
486
|
OAuth2ProviderConfig config = getProvider(providerId);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
487
|
+
ensureDiscovered(
|
|
488
|
+
providerId,
|
|
489
|
+
config,
|
|
490
|
+
new DiscoveryCallback() {
|
|
491
|
+
@Override
|
|
492
|
+
public void onSuccess(OAuth2ProviderConfig resolved) {
|
|
493
|
+
if (resolved != null && resolved.logoutUrl != null && !resolved.logoutUrl.isEmpty()) {
|
|
494
|
+
Uri base = Uri.parse(resolved.logoutUrl);
|
|
495
|
+
Uri.Builder b = base.buildUpon();
|
|
496
|
+
if (stored != null && stored.idToken != null && !stored.idToken.isEmpty()) {
|
|
497
|
+
b.appendQueryParameter("id_token_hint", stored.idToken);
|
|
498
|
+
}
|
|
499
|
+
if (resolved.postLogoutRedirectUrl != null && !resolved.postLogoutRedirectUrl.isEmpty()) {
|
|
500
|
+
b.appendQueryParameter("post_logout_redirect_uri", resolved.postLogoutRedirectUrl);
|
|
501
|
+
}
|
|
502
|
+
if (resolved.additionalLogoutParameters != null) {
|
|
503
|
+
for (Map.Entry<String, String> entry : resolved.additionalLogoutParameters.entrySet()) {
|
|
504
|
+
b.appendQueryParameter(entry.getKey(), entry.getValue());
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
Intent browserIntent = new Intent(Intent.ACTION_VIEW, b.build());
|
|
508
|
+
activity.runOnUiThread(() -> activity.startActivity(browserIntent));
|
|
509
|
+
}
|
|
510
|
+
call.resolve();
|
|
511
|
+
}
|
|
315
512
|
|
|
316
|
-
|
|
513
|
+
@Override
|
|
514
|
+
public void onError(String message) {
|
|
515
|
+
// Logout still succeeds locally even if discovery fails
|
|
516
|
+
call.resolve();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
);
|
|
317
520
|
}
|
|
318
521
|
|
|
319
522
|
@Override
|
|
@@ -374,7 +577,43 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
374
577
|
call.reject("OAuth2 refresh token is not available. Make sure offline_access scope is granted.");
|
|
375
578
|
return;
|
|
376
579
|
}
|
|
377
|
-
refreshWithToken(call, providerId, config, tokens.refreshToken);
|
|
580
|
+
refreshWithToken(call, providerId, config, tokens.refreshToken, null, true);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
public void refreshTokenRaw(PluginCall call, String providerId, String refreshToken, JSONObject additionalParameters) {
|
|
584
|
+
OAuth2ProviderConfig config = getProvider(providerId);
|
|
585
|
+
if (config == null) {
|
|
586
|
+
call.reject("OAuth2 provider '" + providerId + "' is not initialized.");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
OAuth2StoredTokens stored = loadStoredTokens(providerId);
|
|
590
|
+
String effective = (refreshToken != null && !refreshToken.isEmpty()) ? refreshToken : (stored != null ? stored.refreshToken : null);
|
|
591
|
+
if (effective == null || effective.isEmpty()) {
|
|
592
|
+
call.reject("OAuth2 refresh token is not available. Make sure offline_access scope is granted.");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
refreshWithToken(call, providerId, config, effective, additionalParameters, false);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
public Long getAccessTokenExpirationDateMs(String providerId) {
|
|
599
|
+
OAuth2StoredTokens tokens = loadStoredTokens(providerId);
|
|
600
|
+
return tokens != null && tokens.expiresAt > 0 ? tokens.expiresAt : null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
public boolean isAccessTokenAvailableStatus(String providerId) {
|
|
604
|
+
OAuth2StoredTokens tokens = loadStoredTokens(providerId);
|
|
605
|
+
return tokens != null && tokens.accessToken != null && !tokens.accessToken.isEmpty();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
public boolean isAccessTokenExpiredStatus(String providerId) {
|
|
609
|
+
OAuth2StoredTokens tokens = loadStoredTokens(providerId);
|
|
610
|
+
if (tokens == null) return true;
|
|
611
|
+
return tokens.expiresAt <= System.currentTimeMillis();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
public boolean isRefreshTokenAvailableStatus(String providerId) {
|
|
615
|
+
OAuth2StoredTokens tokens = loadStoredTokens(providerId);
|
|
616
|
+
return tokens != null && tokens.refreshToken != null && !tokens.refreshToken.isEmpty();
|
|
378
617
|
}
|
|
379
618
|
|
|
380
619
|
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
|
@@ -493,10 +732,40 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
493
732
|
}
|
|
494
733
|
|
|
495
734
|
if (config.accessTokenEndpoint == null || config.accessTokenEndpoint.isEmpty()) {
|
|
496
|
-
|
|
735
|
+
// Try discovery if issuerUrl exists
|
|
736
|
+
ensureDiscovered(
|
|
737
|
+
providerId,
|
|
738
|
+
config,
|
|
739
|
+
new DiscoveryCallback() {
|
|
740
|
+
@Override
|
|
741
|
+
public void onSuccess(OAuth2ProviderConfig resolved) {
|
|
742
|
+
if (resolved.accessTokenEndpoint == null || resolved.accessTokenEndpoint.isEmpty()) {
|
|
743
|
+
pendingCall.reject("No accessTokenEndpoint configured for code exchange");
|
|
744
|
+
cleanupPending();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
exchangeAuthorizationCodeWithConfig(code, resolved);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
@Override
|
|
751
|
+
public void onError(String message) {
|
|
752
|
+
pendingCall.reject(message);
|
|
753
|
+
cleanupPending();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
exchangeAuthorizationCodeWithConfig(code, config);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private void exchangeAuthorizationCodeWithConfig(String code, OAuth2ProviderConfig config) {
|
|
764
|
+
if (pendingState == null || pendingCall == null) {
|
|
497
765
|
cleanupPending();
|
|
498
766
|
return;
|
|
499
767
|
}
|
|
768
|
+
final String providerId = pendingState.providerId;
|
|
500
769
|
|
|
501
770
|
FormBody.Builder bodyBuilder = new FormBody.Builder()
|
|
502
771
|
.add("grant_type", "authorization_code")
|
|
@@ -508,6 +777,12 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
508
777
|
bodyBuilder.add("code_verifier", pendingState.codeVerifier);
|
|
509
778
|
}
|
|
510
779
|
|
|
780
|
+
if (config.additionalTokenParameters != null) {
|
|
781
|
+
for (Map.Entry<String, String> entry : config.additionalTokenParameters.entrySet()) {
|
|
782
|
+
bodyBuilder.add(entry.getKey(), entry.getValue());
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
511
786
|
Request request = new Request.Builder().url(config.accessTokenEndpoint).post(bodyBuilder.build()).build();
|
|
512
787
|
|
|
513
788
|
if (config.logsEnabled) {
|
|
@@ -551,19 +826,62 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
551
826
|
);
|
|
552
827
|
}
|
|
553
828
|
|
|
554
|
-
private void refreshWithToken(
|
|
829
|
+
private void refreshWithToken(
|
|
830
|
+
final PluginCall pluginCall,
|
|
831
|
+
String providerId,
|
|
832
|
+
OAuth2ProviderConfig config,
|
|
833
|
+
String refreshToken,
|
|
834
|
+
JSONObject additionalParameters,
|
|
835
|
+
boolean wrapResponse
|
|
836
|
+
) {
|
|
555
837
|
if (config.accessTokenEndpoint == null || config.accessTokenEndpoint.isEmpty()) {
|
|
556
|
-
|
|
838
|
+
// Try discovery if issuerUrl exists
|
|
839
|
+
ensureDiscovered(
|
|
840
|
+
providerId,
|
|
841
|
+
config,
|
|
842
|
+
new DiscoveryCallback() {
|
|
843
|
+
@Override
|
|
844
|
+
public void onSuccess(OAuth2ProviderConfig resolved) {
|
|
845
|
+
if (resolved.accessTokenEndpoint == null || resolved.accessTokenEndpoint.isEmpty()) {
|
|
846
|
+
pluginCall.reject("No accessTokenEndpoint configured for refresh");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
refreshWithToken(pluginCall, providerId, resolved, refreshToken, additionalParameters, wrapResponse);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
@Override
|
|
853
|
+
public void onError(String message) {
|
|
854
|
+
pluginCall.reject(message);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
);
|
|
557
858
|
return;
|
|
558
859
|
}
|
|
559
860
|
|
|
560
|
-
FormBody
|
|
861
|
+
FormBody.Builder bodyBuilder = new FormBody.Builder()
|
|
561
862
|
.add("grant_type", "refresh_token")
|
|
562
863
|
.add("refresh_token", refreshToken)
|
|
563
|
-
.add("client_id", config.appId)
|
|
564
|
-
|
|
864
|
+
.add("client_id", config.appId);
|
|
865
|
+
|
|
866
|
+
if (config.additionalTokenParameters != null) {
|
|
867
|
+
for (Map.Entry<String, String> entry : config.additionalTokenParameters.entrySet()) {
|
|
868
|
+
bodyBuilder.add(entry.getKey(), entry.getValue());
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (additionalParameters != null) {
|
|
873
|
+
try {
|
|
874
|
+
Iterator<String> keys = additionalParameters.keys();
|
|
875
|
+
while (keys.hasNext()) {
|
|
876
|
+
String key = keys.next();
|
|
877
|
+
bodyBuilder.add(key, additionalParameters.getString(key));
|
|
878
|
+
}
|
|
879
|
+
} catch (JSONException e) {
|
|
880
|
+
Log.w(LOG_TAG, "Failed to parse additionalParameters for refresh", e);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
565
883
|
|
|
566
|
-
Request request = new Request.Builder().url(config.accessTokenEndpoint).post(
|
|
884
|
+
Request request = new Request.Builder().url(config.accessTokenEndpoint).post(bodyBuilder.build()).build();
|
|
567
885
|
|
|
568
886
|
httpClient
|
|
569
887
|
.newCall(request)
|
|
@@ -584,7 +902,7 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
584
902
|
String responseBody = response.body() != null ? response.body().string() : "";
|
|
585
903
|
try {
|
|
586
904
|
JSONObject tokenPayload = new JSONObject(responseBody);
|
|
587
|
-
handleTokenSuccess(providerId, config, tokenPayload, pluginCall);
|
|
905
|
+
handleTokenSuccess(providerId, config, tokenPayload, pluginCall, refreshToken, wrapResponse);
|
|
588
906
|
} catch (JSONException e) {
|
|
589
907
|
pluginCall.reject("Failed to parse OAuth2 refresh response", e);
|
|
590
908
|
}
|
|
@@ -594,12 +912,18 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
594
912
|
}
|
|
595
913
|
|
|
596
914
|
private void handleTokenSuccess(String providerId, OAuth2ProviderConfig config, JSONObject tokenPayload) throws JSONException {
|
|
597
|
-
handleTokenSuccess(providerId, config, tokenPayload, pendingCall);
|
|
915
|
+
handleTokenSuccess(providerId, config, tokenPayload, pendingCall, null, true);
|
|
598
916
|
cleanupPending();
|
|
599
917
|
}
|
|
600
918
|
|
|
601
|
-
private void handleTokenSuccess(
|
|
602
|
-
|
|
919
|
+
private void handleTokenSuccess(
|
|
920
|
+
String providerId,
|
|
921
|
+
OAuth2ProviderConfig config,
|
|
922
|
+
JSONObject tokenPayload,
|
|
923
|
+
PluginCall call,
|
|
924
|
+
String fallbackRefreshToken,
|
|
925
|
+
boolean wrapResponse
|
|
926
|
+
) throws JSONException {
|
|
603
927
|
if (call == null) {
|
|
604
928
|
return;
|
|
605
929
|
}
|
|
@@ -607,7 +931,8 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
607
931
|
final String accessToken = tokenPayload.getString("access_token");
|
|
608
932
|
final String tokenType = tokenPayload.optString("token_type", "bearer");
|
|
609
933
|
final int expiresIn = tokenPayload.optInt("expires_in", 3600);
|
|
610
|
-
final String refreshToken = tokenPayload.optString("refresh_token", null);
|
|
934
|
+
final String refreshToken = tokenPayload.has("refresh_token") ? tokenPayload.optString("refresh_token", null) : null;
|
|
935
|
+
final String effectiveRefreshToken = (refreshToken != null && !refreshToken.isEmpty()) ? refreshToken : fallbackRefreshToken;
|
|
611
936
|
final String idToken = tokenPayload.optString("id_token", null);
|
|
612
937
|
final String scopeRaw = tokenPayload.optString("scope", "");
|
|
613
938
|
final List<String> scopes = scopeRaw.isEmpty() ? Collections.emptyList() : Arrays.asList(scopeRaw.split(" "));
|
|
@@ -628,11 +953,12 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
628
953
|
tokenType,
|
|
629
954
|
expiresIn,
|
|
630
955
|
expiresAt,
|
|
631
|
-
|
|
956
|
+
effectiveRefreshToken,
|
|
632
957
|
idToken,
|
|
633
958
|
scopes,
|
|
634
959
|
resourceData,
|
|
635
|
-
call
|
|
960
|
+
call,
|
|
961
|
+
wrapResponse
|
|
636
962
|
);
|
|
637
963
|
}
|
|
638
964
|
|
|
@@ -641,12 +967,36 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
641
967
|
if (config.logsEnabled) {
|
|
642
968
|
Log.w(LOG_TAG, "Failed to fetch resource: " + message);
|
|
643
969
|
}
|
|
644
|
-
completeLogin(
|
|
970
|
+
completeLogin(
|
|
971
|
+
providerId,
|
|
972
|
+
accessToken,
|
|
973
|
+
tokenType,
|
|
974
|
+
expiresIn,
|
|
975
|
+
expiresAt,
|
|
976
|
+
effectiveRefreshToken,
|
|
977
|
+
idToken,
|
|
978
|
+
scopes,
|
|
979
|
+
null,
|
|
980
|
+
call,
|
|
981
|
+
wrapResponse
|
|
982
|
+
);
|
|
645
983
|
}
|
|
646
984
|
}
|
|
647
985
|
);
|
|
648
986
|
} else {
|
|
649
|
-
completeLogin(
|
|
987
|
+
completeLogin(
|
|
988
|
+
providerId,
|
|
989
|
+
accessToken,
|
|
990
|
+
tokenType,
|
|
991
|
+
expiresIn,
|
|
992
|
+
expiresAt,
|
|
993
|
+
effectiveRefreshToken,
|
|
994
|
+
idToken,
|
|
995
|
+
scopes,
|
|
996
|
+
null,
|
|
997
|
+
call,
|
|
998
|
+
wrapResponse
|
|
999
|
+
);
|
|
650
1000
|
}
|
|
651
1001
|
}
|
|
652
1002
|
|
|
@@ -661,7 +1011,19 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
661
1011
|
List<String> scopes,
|
|
662
1012
|
JSONObject resourceData
|
|
663
1013
|
) {
|
|
664
|
-
completeLogin(
|
|
1014
|
+
completeLogin(
|
|
1015
|
+
providerId,
|
|
1016
|
+
accessToken,
|
|
1017
|
+
tokenType,
|
|
1018
|
+
expiresIn,
|
|
1019
|
+
expiresAt,
|
|
1020
|
+
refreshToken,
|
|
1021
|
+
idToken,
|
|
1022
|
+
scopes,
|
|
1023
|
+
resourceData,
|
|
1024
|
+
pendingCall,
|
|
1025
|
+
true
|
|
1026
|
+
);
|
|
665
1027
|
cleanupPending();
|
|
666
1028
|
}
|
|
667
1029
|
|
|
@@ -675,7 +1037,8 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
675
1037
|
String idToken,
|
|
676
1038
|
List<String> scopes,
|
|
677
1039
|
JSONObject resourceData,
|
|
678
|
-
PluginCall call
|
|
1040
|
+
PluginCall call,
|
|
1041
|
+
boolean wrapResponse
|
|
679
1042
|
) {
|
|
680
1043
|
if (call == null) {
|
|
681
1044
|
return;
|
|
@@ -704,10 +1067,14 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
704
1067
|
result.put("tokenType", tokenType);
|
|
705
1068
|
result.put("expiresIn", expiresIn);
|
|
706
1069
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1070
|
+
if (wrapResponse) {
|
|
1071
|
+
JSObject response = new JSObject();
|
|
1072
|
+
response.put("provider", "oauth2");
|
|
1073
|
+
response.put("result", result);
|
|
1074
|
+
call.resolve(response);
|
|
1075
|
+
} else {
|
|
1076
|
+
call.resolve(result);
|
|
1077
|
+
}
|
|
711
1078
|
}
|
|
712
1079
|
|
|
713
1080
|
private interface ResourceCallback {
|
|
@@ -816,6 +1183,21 @@ public class OAuth2Provider implements SocialProvider {
|
|
|
816
1183
|
return map;
|
|
817
1184
|
}
|
|
818
1185
|
|
|
1186
|
+
private static String normalizeScopeValue(Object value) {
|
|
1187
|
+
if (value == null || value == JSONObject.NULL) return "";
|
|
1188
|
+
if (value instanceof String) return (String) value;
|
|
1189
|
+
if (value instanceof JSONArray) {
|
|
1190
|
+
JSONArray arr = (JSONArray) value;
|
|
1191
|
+
List<String> parts = new ArrayList<>();
|
|
1192
|
+
for (int i = 0; i < arr.length(); i++) {
|
|
1193
|
+
String s = arr.optString(i, null);
|
|
1194
|
+
if (s != null && !s.isEmpty()) parts.add(s);
|
|
1195
|
+
}
|
|
1196
|
+
return String.join(" ", parts);
|
|
1197
|
+
}
|
|
1198
|
+
return "";
|
|
1199
|
+
}
|
|
1200
|
+
|
|
819
1201
|
private static String generateCodeVerifier() {
|
|
820
1202
|
SecureRandom secureRandom = new SecureRandom();
|
|
821
1203
|
byte[] code = new byte[64];
|