capacitor-oidc 0.0.1-alpha.1 → 0.0.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/CapacitorOidc.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +66 -62
- package/SECURITY.md +60 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/jojopirker/capacitor/oidc/CapacitorOidcPlugin.java +33 -27
- package/contracts/fixtures/stored-session-v1.json +11 -0
- package/contracts/stored-session-v1.schema.json +42 -0
- package/dist/esm/capacitor-navigator.js +14 -12
- package/dist/esm/capacitor-navigator.js.map +1 -1
- package/dist/esm/capacitor-secure-state-store.js +4 -20
- package/dist/esm/capacitor-secure-state-store.js.map +1 -1
- package/dist/esm/capacitor-user-manager.js +8 -13
- package/dist/esm/capacitor-user-manager.js.map +1 -1
- package/dist/esm/definitions.d.ts +3 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/errors.js +1 -2
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/native.js +1 -2
- package/dist/esm/native.js.map +1 -1
- package/dist/plugin.cjs +28 -64
- package/dist/plugin.cjs.map +1 -1
- package/docs/API.md +91 -0
- package/docs/GETTING_STARTED.md +132 -0
- package/docs/PLATFORM_SETUP.md +98 -0
- package/docs/PROVIDERS.md +184 -0
- package/docs/PUBLISHING.md +90 -0
- package/docs/README.md +23 -0
- package/docs/SESSIONS_AND_WIDGETS.md +82 -0
- package/docs/TESTING.md +60 -0
- package/docs/TROUBLESHOOTING.md +67 -0
- package/docs/adr/0001-use-oidc-client-ts.md +22 -0
- package/docs/adr/0002-keep-the-native-boundary-small.md +16 -0
- package/docs/adr/0003-share-session-storage-with-widgets.md +20 -0
- package/ios/Sources/CapacitorOidcPlugin/CapacitorOidcPlugin.swift +52 -43
- package/package.json +14 -14
- package/android/src/main/java/com/jojopirker/capacitor/oidc/NativeContract.java +0 -35
- package/dist/esm/generated/native-contract.d.ts +0 -37
- package/dist/esm/generated/native-contract.js +0 -38
- package/dist/esm/generated/native-contract.js.map +0 -1
- package/ios/Sources/CapacitorOidcPlugin/NativeContract.generated.swift +0 -39
package/CapacitorOidc.podspec
CHANGED
|
@@ -13,6 +13,6 @@ Pod::Spec.new do |spec|
|
|
|
13
13
|
spec.source = { git: repository_url, tag: spec.version.to_s }
|
|
14
14
|
spec.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
15
15
|
spec.ios.deployment_target = '15.0'
|
|
16
|
-
spec.dependency 'Capacitor'
|
|
16
|
+
spec.dependency 'Capacitor', '>= 7.0'
|
|
17
17
|
spec.swift_version = '5.9'
|
|
18
18
|
end
|
package/Package.swift
CHANGED
|
@@ -8,7 +8,7 @@ let package = Package(
|
|
|
8
8
|
.library(name: "CapacitorOidc", targets: ["CapacitorOidcPlugin"])
|
|
9
9
|
],
|
|
10
10
|
dependencies: [
|
|
11
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git",
|
|
11
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", "7.0.0"..<"9.0.0")
|
|
12
12
|
],
|
|
13
13
|
targets: [
|
|
14
14
|
.target(
|
package/README.md
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
# capacitor-oidc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/capacitor-oidc)
|
|
4
|
+
[](https://github.com/jojopirker/capacitor-oidc/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
Native OAuth 2.0 and OpenID Connect for Capacitor, powered by
|
|
8
|
+
[`oidc-client-ts`](https://github.com/authts/oidc-client-ts).
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
`capacitor-oidc` adapts the familiar `UserManager` API to native iOS and Android
|
|
11
|
+
applications. It keeps protocol behavior in `oidc-client-ts` and adds only the
|
|
12
|
+
Capacitor-specific pieces:
|
|
9
13
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
14
|
+
- system authentication UI instead of an embedded WebView;
|
|
15
|
+
- secure native storage for OIDC transactions and sessions;
|
|
16
|
+
- refresh-token renewal when the app is active or resumes;
|
|
17
|
+
- a native-readable session snapshot for app widgets.
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
On iOS, authentication uses `ASWebAuthenticationSession`. On Android, it uses
|
|
20
|
+
AndroidX Auth Tab with its Custom Tab fallback.
|
|
21
|
+
|
|
22
|
+
> [!CAUTION]
|
|
23
|
+
> This package is an alpha. The public API and stored-session format can still
|
|
24
|
+
> change before v1. See [Security](SECURITY.md) and
|
|
25
|
+
> [current test coverage](docs/TESTING.md) before making a production decision.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Capacitor 7 or 8
|
|
30
|
+
- iOS 15 or newer
|
|
31
|
+
- Android API 24 or newer
|
|
32
|
+
- Android compile SDK 36, Java 21, and a compatible Android Gradle Plugin
|
|
33
|
+
- an OAuth public client using Authorization Code Flow with PKCE
|
|
34
|
+
- Web Crypto in the packaged Capacitor WebView
|
|
35
|
+
- CORS support for the app's Capacitor origin on OIDC HTTP endpoints
|
|
36
|
+
|
|
37
|
+
Never ship a client secret in a Capacitor application.
|
|
15
38
|
|
|
16
39
|
## Install
|
|
17
40
|
|
|
18
41
|
```sh
|
|
19
|
-
npm install capacitor-oidc
|
|
42
|
+
npm install capacitor-oidc @capacitor/app
|
|
20
43
|
npx cap sync
|
|
21
44
|
```
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Authorization and logout endpoints must use HTTPS. Plain HTTP is accepted only for loopback local development.
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
46
|
+
## Quick start
|
|
28
47
|
|
|
29
48
|
```ts
|
|
30
49
|
import { CapacitorUserManager } from 'capacitor-oidc';
|
|
@@ -41,69 +60,54 @@ const manager = await CapacitorUserManager.create(
|
|
|
41
60
|
},
|
|
42
61
|
{
|
|
43
62
|
storageNamespace: 'primary',
|
|
44
|
-
ios: {
|
|
45
|
-
keychainAccessGroup: 'TEAMID.group.com.example.app',
|
|
46
|
-
keychainAccessibility: 'afterFirstUnlockThisDeviceOnly',
|
|
47
|
-
prefersEphemeralWebBrowserSession: false,
|
|
48
|
-
},
|
|
49
63
|
},
|
|
50
64
|
);
|
|
51
65
|
|
|
52
66
|
const user = await manager.signin();
|
|
53
67
|
const validUser = await manager.getValidUser(30);
|
|
68
|
+
|
|
54
69
|
await manager.signout();
|
|
55
70
|
await manager.dispose();
|
|
56
71
|
```
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Redirect setup
|
|
63
|
-
|
|
64
|
-
Register the redirect URI as a native public-client redirect at the provider. Never put a client secret in the app.
|
|
73
|
+
Register the redirect and post-logout redirect URIs exactly at your provider and
|
|
74
|
+
in the native application. The provider client must be public and must allow
|
|
75
|
+
Authorization Code Flow with PKCE.
|
|
65
76
|
|
|
66
|
-
|
|
77
|
+
`CapacitorUserManager` is for native Capacitor runtimes. For a browser build, use
|
|
78
|
+
the normal `UserManager` from `oidc-client-ts` and select the implementation with
|
|
79
|
+
`Capacitor.isNativePlatform()` in the application.
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
## Documentation
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
- [Getting started](docs/GETTING_STARTED.md)
|
|
84
|
+
- [iOS and Android setup](docs/PLATFORM_SETUP.md)
|
|
85
|
+
- [Provider configuration](docs/PROVIDERS.md)
|
|
86
|
+
- [API and `oidc-client-ts` compatibility](docs/API.md)
|
|
87
|
+
- [Sessions, secure storage, and widgets](docs/SESSIONS_AND_WIDGETS.md)
|
|
88
|
+
- [Troubleshooting](docs/TROUBLESHOOTING.md)
|
|
89
|
+
- [Testing status](docs/TESTING.md)
|
|
90
|
+
- [Security](SECURITY.md)
|
|
71
91
|
|
|
72
|
-
##
|
|
92
|
+
## Session renewal
|
|
73
93
|
|
|
74
|
-
`
|
|
94
|
+
`automaticSilentRenew` uses the foreground expiry timer from `oidc-client-ts`.
|
|
95
|
+
Native silent renewal uses the refresh token and never falls back to an iframe.
|
|
96
|
+
Concurrent renewal triggers share one request. When the app returns to the
|
|
97
|
+
foreground, the manager checks whether the current user needs renewal.
|
|
75
98
|
|
|
76
|
-
|
|
99
|
+
The operating system can suspend or terminate the app, so the package does not
|
|
100
|
+
promise exact background refresh timing.
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
let vault = TokenVault(accessGroup: "TEAMID.group.com.example.app")
|
|
80
|
-
let session = try vault.loadSession(namespace: "primary")
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Replace `TEAMID` with the Apple Developer Team ID. Both targets must carry the same
|
|
84
|
-
Keychain Sharing entitlement, normally declared as
|
|
85
|
-
`$(AppIdentifierPrefix)group.com.example.app`; the runtime option uses its expanded
|
|
86
|
-
`TEAMID.group.com.example.app` value. The default accessibility is
|
|
87
|
-
`AfterFirstUnlockThisDeviceOnly`; Keychain synchronization and biometric gating are disabled.
|
|
88
|
-
|
|
89
|
-
An Android app widget in the same package can use the public native vault:
|
|
90
|
-
|
|
91
|
-
```java
|
|
92
|
-
TokenVault vault = new TokenVault(context);
|
|
93
|
-
StoredSessionV1 session = vault.loadSession("primary");
|
|
94
|
-
```
|
|
102
|
+
## Networking
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
Discovery, token exchange, refresh, UserInfo, and revocation are performed by
|
|
105
|
+
`oidc-client-ts` through the unmodified WebView `fetch`. The corresponding
|
|
106
|
+
provider endpoints must allow the application's configured Capacitor origin
|
|
107
|
+
through CORS.
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`signout()` uses the provider's end-session endpoint in the same native authentication UI. If `revokeTokensOnSignout` is enabled, upstream revocation runs before local state is cleared. A revocation failure therefore preserves the local session and its renewal timer; after revocation succeeds, a later browser or callback failure leaves the app locally signed out. Removing the user cancels its expiry timer without disabling renewal for a later sign-in with the same manager.
|
|
101
|
-
|
|
102
|
-
## Unsupported APIs
|
|
103
|
-
|
|
104
|
-
Use `signin()` and `signout()` for interactive flows. Redirect navigation, iframe logout, and browser session monitoring reject with `UNSUPPORTED_RUNTIME`. Resource Owner Password Credentials, DPoP, client secrets, exact background scheduling, and autonomous widget refresh are outside v1.
|
|
105
|
-
|
|
106
|
-
Only one interactive native authentication session and one configured manager are supported at a time.
|
|
109
|
+
The package does not patch `fetch`, add native OIDC networking, accept client
|
|
110
|
+
secrets, or render authentication inside a WebView.
|
|
107
111
|
|
|
108
112
|
## Development
|
|
109
113
|
|
|
@@ -113,9 +117,9 @@ npm run verify:ios
|
|
|
113
117
|
npm run verify:android
|
|
114
118
|
```
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
Architecture decisions and contributor constraints are documented in
|
|
121
|
+
[ARCHITECTURE.md](ARCHITECTURE.md), [REQUIREMENTS.md](REQUIREMENTS.md), and
|
|
122
|
+
[docs/adr](docs/adr).
|
|
119
123
|
|
|
120
124
|
## License
|
|
121
125
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Design boundary
|
|
4
|
+
|
|
5
|
+
`capacitor-oidc` delegates OAuth and OpenID Connect protocol behavior to
|
|
6
|
+
`oidc-client-ts`. Native code is limited to presenting system authentication UI
|
|
7
|
+
and storing state securely. The package does not patch `fetch`, process JWTs, or
|
|
8
|
+
implement OAuth endpoints in Swift or Java.
|
|
9
|
+
|
|
10
|
+
Installed applications are public clients. They must use Authorization Code Flow
|
|
11
|
+
with PKCE and must not contain a client secret. Authorization and provider logout
|
|
12
|
+
run in system authentication UI, never an embedded WebView.
|
|
13
|
+
|
|
14
|
+
## ID-token validation in `oidc-client-ts` 3.5.0
|
|
15
|
+
|
|
16
|
+
The pinned `oidc-client-ts` 3.5.0 decodes ID-token claims and validates `sub`,
|
|
17
|
+
`nonce`, and selected refresh-token continuity claims. It does not independently
|
|
18
|
+
compare the decoded `iss`, `aud`, and `exp` claims with the configured client and
|
|
19
|
+
current time. This behavior is visible in the
|
|
20
|
+
[v3.5.0 ResponseValidator](https://github.com/authts/oidc-client-ts/blob/v3.5.0/src/ResponseValidator.ts)
|
|
21
|
+
and is the subject of the open upstream report
|
|
22
|
+
[authts/oidc-client-ts#2475](https://github.com/authts/oidc-client-ts/issues/2475).
|
|
23
|
+
|
|
24
|
+
The practical exposure is narrower than an implicit-flow token parser: this
|
|
25
|
+
package uses Authorization Code Flow only, and the token response comes from the
|
|
26
|
+
configured token endpoint over TLS after state, nonce, and PKCE processing. The
|
|
27
|
+
upstream project has not yet classified or resolved the report.
|
|
28
|
+
|
|
29
|
+
This is a standards-conformance and threat-model consideration, not a categorical
|
|
30
|
+
claim that every application using `oidc-client-ts` is unsafe. Consumers should
|
|
31
|
+
evaluate it against their provider, reliance on ID-token claims, and assurance
|
|
32
|
+
requirements. Resource servers must always validate access tokens independently;
|
|
33
|
+
an application must not use unverified client-side profile claims as its API
|
|
34
|
+
authorization boundary.
|
|
35
|
+
|
|
36
|
+
The package will follow the upstream resolution and cover the expected behavior
|
|
37
|
+
in provider integration tests before a stable v1 decision. It will not add a
|
|
38
|
+
package-local JWT or cryptographic-validation layer, because duplicating protocol
|
|
39
|
+
security code would expand the attack surface and create a second OIDC engine.
|
|
40
|
+
|
|
41
|
+
## Secure-storage boundary
|
|
42
|
+
|
|
43
|
+
OIDC transactions and sessions are stored through iOS Keychain or an Android
|
|
44
|
+
Keystore-backed vault. Configuring an iOS Keychain access group gives every
|
|
45
|
+
entitled target access to the plugin's canonical session, transaction data, and
|
|
46
|
+
widget snapshot. Treat those extensions as part of the application's credential
|
|
47
|
+
trust boundary.
|
|
48
|
+
|
|
49
|
+
The widget snapshot can contain access, refresh, and ID tokens. Autonomous widget
|
|
50
|
+
refresh is not currently implemented; see
|
|
51
|
+
[Sessions, secure storage, and widgets](docs/SESSIONS_AND_WIDGETS.md).
|
|
52
|
+
|
|
53
|
+
## Runtime assumptions
|
|
54
|
+
|
|
55
|
+
`oidc-client-ts` requires `crypto.subtle` and `crypto.getRandomValues`.
|
|
56
|
+
`CapacitorUserManager.create()` rejects with `UNSUPPORTED_RUNTIME` when they are
|
|
57
|
+
absent. Their behavior must be verified in packaged applications on supported
|
|
58
|
+
physical devices.
|
|
59
|
+
|
|
60
|
+
See [Testing](docs/TESTING.md) for current platform and provider coverage.
|
package/android/build.gradle
CHANGED
|
@@ -16,9 +16,15 @@ import java.security.GeneralSecurityException;
|
|
|
16
16
|
import java.util.List;
|
|
17
17
|
import org.json.JSONObject;
|
|
18
18
|
|
|
19
|
-
@CapacitorPlugin(name =
|
|
19
|
+
@CapacitorPlugin(name = "CapacitorOidc")
|
|
20
20
|
public final class CapacitorOidcPlugin extends Plugin {
|
|
21
21
|
|
|
22
|
+
private static final String AUTH_SESSION_IN_PROGRESS = "AUTH_SESSION_IN_PROGRESS";
|
|
23
|
+
private static final String USER_CANCELLED = "USER_CANCELLED";
|
|
24
|
+
private static final String BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
25
|
+
private static final String INVALID_CALLBACK = "INVALID_CALLBACK";
|
|
26
|
+
private static final String SECURE_STORAGE_ERROR = "SECURE_STORAGE_ERROR";
|
|
27
|
+
|
|
22
28
|
private ActivityResultLauncher<Intent> authLauncher;
|
|
23
29
|
private PluginCall pendingAuthCall;
|
|
24
30
|
private Uri pendingCallback;
|
|
@@ -38,26 +44,26 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
38
44
|
@PluginMethod
|
|
39
45
|
public void open(PluginCall call) {
|
|
40
46
|
if (pendingAuthCall != null) {
|
|
41
|
-
call.reject("An authentication session is already active.",
|
|
47
|
+
call.reject("An authentication session is already active.", AUTH_SESSION_IN_PROGRESS);
|
|
42
48
|
return;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
String urlValue = requiredString(call,
|
|
46
|
-
String callbackValue = requiredString(call,
|
|
51
|
+
String urlValue = requiredString(call, "url");
|
|
52
|
+
String callbackValue = requiredString(call, "callbackUrl");
|
|
47
53
|
if (urlValue == null || callbackValue == null) return;
|
|
48
54
|
|
|
49
55
|
Uri url = Uri.parse(urlValue);
|
|
50
56
|
Uri callback = Uri.parse(callbackValue);
|
|
51
57
|
if (!isSecureRequestUrl(url)) {
|
|
52
|
-
call.reject("The authentication URL must use HTTPS outside loopback development.",
|
|
58
|
+
call.reject("The authentication URL must use HTTPS outside loopback development.", BROWSER_UNAVAILABLE);
|
|
53
59
|
return;
|
|
54
60
|
}
|
|
55
61
|
if (!isSupportedCallback(callback)) {
|
|
56
|
-
call.reject("The callback URL must use HTTPS or a custom scheme.",
|
|
62
|
+
call.reject("The callback URL must use HTTPS or a custom scheme.", INVALID_CALLBACK);
|
|
57
63
|
return;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
boolean ephemeral = Boolean.TRUE.equals(call.getBoolean(
|
|
66
|
+
boolean ephemeral = Boolean.TRUE.equals(call.getBoolean("prefersEphemeralWebBrowserSession", false));
|
|
61
67
|
AuthTabIntent authTab = new AuthTabIntent.Builder().setEphemeralBrowsingEnabled(ephemeral).build();
|
|
62
68
|
pendingAuthCall = call;
|
|
63
69
|
pendingCallback = callback;
|
|
@@ -70,7 +76,7 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
70
76
|
}
|
|
71
77
|
} catch (ActivityNotFoundException | IllegalStateException error) {
|
|
72
78
|
clearPendingAuth();
|
|
73
|
-
call.reject("No compatible system browser is available.",
|
|
79
|
+
call.reject("No compatible system browser is available.", BROWSER_UNAVAILABLE);
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -78,7 +84,7 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
78
84
|
public void cancel(PluginCall call) {
|
|
79
85
|
PluginCall authCall = pendingAuthCall;
|
|
80
86
|
clearPendingAuth();
|
|
81
|
-
if (authCall != null) authCall.reject("The authentication session was cancelled.",
|
|
87
|
+
if (authCall != null) authCall.reject("The authentication session was cancelled.", USER_CANCELLED);
|
|
82
88
|
call.resolve();
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -90,15 +96,15 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
90
96
|
|
|
91
97
|
clearPendingAuth();
|
|
92
98
|
JSObject response = new JSObject();
|
|
93
|
-
response.put(
|
|
99
|
+
response.put("url", callback.toString());
|
|
94
100
|
call.resolve(response);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
@PluginMethod
|
|
98
104
|
public void storageSet(PluginCall call) {
|
|
99
|
-
String namespace = requiredString(call,
|
|
100
|
-
String key = requiredString(call,
|
|
101
|
-
String value = call.getString(
|
|
105
|
+
String namespace = requiredString(call, "namespace");
|
|
106
|
+
String key = requiredString(call, "key");
|
|
107
|
+
String value = call.getString("value");
|
|
102
108
|
if (namespace == null || key == null) return;
|
|
103
109
|
if (value == null) {
|
|
104
110
|
call.reject("value is required.");
|
|
@@ -115,8 +121,8 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
115
121
|
|
|
116
122
|
@PluginMethod
|
|
117
123
|
public void storageGet(PluginCall call) {
|
|
118
|
-
String namespace = requiredString(call,
|
|
119
|
-
String key = requiredString(call,
|
|
124
|
+
String namespace = requiredString(call, "namespace");
|
|
125
|
+
String key = requiredString(call, "key");
|
|
120
126
|
if (namespace == null || key == null) return;
|
|
121
127
|
|
|
122
128
|
try {
|
|
@@ -128,8 +134,8 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
128
134
|
|
|
129
135
|
@PluginMethod
|
|
130
136
|
public void storageRemove(PluginCall call) {
|
|
131
|
-
String namespace = requiredString(call,
|
|
132
|
-
String key = requiredString(call,
|
|
137
|
+
String namespace = requiredString(call, "namespace");
|
|
138
|
+
String key = requiredString(call, "key");
|
|
133
139
|
if (namespace == null || key == null) return;
|
|
134
140
|
|
|
135
141
|
try {
|
|
@@ -141,13 +147,13 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
141
147
|
|
|
142
148
|
@PluginMethod
|
|
143
149
|
public void storageGetAllKeys(PluginCall call) {
|
|
144
|
-
String namespace = requiredString(call,
|
|
150
|
+
String namespace = requiredString(call, "namespace");
|
|
145
151
|
if (namespace == null) return;
|
|
146
152
|
|
|
147
153
|
try {
|
|
148
154
|
List<String> keys = vault.getAllKeys(namespace);
|
|
149
155
|
JSObject result = new JSObject();
|
|
150
|
-
result.put(
|
|
156
|
+
result.put("keys", new JSArray(keys));
|
|
151
157
|
call.resolve(result);
|
|
152
158
|
} catch (GeneralSecurityException | IOException error) {
|
|
153
159
|
rejectStorage(call);
|
|
@@ -156,11 +162,11 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
156
162
|
|
|
157
163
|
@PluginMethod
|
|
158
164
|
public void setSessionSnapshot(PluginCall call) {
|
|
159
|
-
String namespace = requiredString(call,
|
|
165
|
+
String namespace = requiredString(call, "namespace");
|
|
160
166
|
if (namespace == null) return;
|
|
161
167
|
|
|
162
168
|
try {
|
|
163
|
-
vault.setSessionSnapshot(namespace, call.getString(
|
|
169
|
+
vault.setSessionSnapshot(namespace, call.getString("value"));
|
|
164
170
|
call.resolve();
|
|
165
171
|
} catch (GeneralSecurityException | IOException error) {
|
|
166
172
|
rejectStorage(call);
|
|
@@ -174,20 +180,20 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
174
180
|
if (call == null) return;
|
|
175
181
|
|
|
176
182
|
if (result.resultCode == AuthTabIntent.RESULT_CANCELED) {
|
|
177
|
-
call.reject("The authentication session was cancelled.",
|
|
183
|
+
call.reject("The authentication session was cancelled.", USER_CANCELLED);
|
|
178
184
|
return;
|
|
179
185
|
}
|
|
180
186
|
if (result.resultCode != AuthTabIntent.RESULT_OK || result.resultUri == null) {
|
|
181
|
-
call.reject("The browser did not return a valid callback.",
|
|
187
|
+
call.reject("The browser did not return a valid callback.", INVALID_CALLBACK);
|
|
182
188
|
return;
|
|
183
189
|
}
|
|
184
190
|
if (!CallbackUriMatcher.matches(result.resultUri, expectedCallback)) {
|
|
185
|
-
call.reject("The authentication callback does not match the configured redirect URL.",
|
|
191
|
+
call.reject("The authentication callback does not match the configured redirect URL.", INVALID_CALLBACK);
|
|
186
192
|
return;
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
JSObject response = new JSObject();
|
|
190
|
-
response.put(
|
|
196
|
+
response.put("url", result.resultUri.toString());
|
|
191
197
|
call.resolve(response);
|
|
192
198
|
}
|
|
193
199
|
|
|
@@ -230,11 +236,11 @@ public final class CapacitorOidcPlugin extends Plugin {
|
|
|
230
236
|
|
|
231
237
|
private static void resolveValue(PluginCall call, String value) {
|
|
232
238
|
JSObject result = new JSObject();
|
|
233
|
-
result.put(
|
|
239
|
+
result.put("value", value == null ? JSONObject.NULL : value);
|
|
234
240
|
call.resolve(result);
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
private static void rejectStorage(PluginCall call) {
|
|
238
|
-
call.reject("Secure storage failed.",
|
|
244
|
+
call.reject("Secure storage failed.", SECURE_STORAGE_ERROR);
|
|
239
245
|
}
|
|
240
246
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"issuer": "https://issuer.example",
|
|
4
|
+
"clientId": "mobile",
|
|
5
|
+
"accessToken": "access",
|
|
6
|
+
"refreshToken": "refresh",
|
|
7
|
+
"idToken": "id-token",
|
|
8
|
+
"tokenType": "Bearer",
|
|
9
|
+
"scope": "openid profile offline_access",
|
|
10
|
+
"expiresAt": 1800000000
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/jojopirker/capacitor-oidc/contracts/stored-session-v1.schema.json",
|
|
4
|
+
"title": "StoredSessionV1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "issuer", "clientId", "accessToken", "tokenType"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"const": 1
|
|
11
|
+
},
|
|
12
|
+
"issuer": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"format": "uri"
|
|
15
|
+
},
|
|
16
|
+
"clientId": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 1
|
|
19
|
+
},
|
|
20
|
+
"accessToken": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"refreshToken": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"idToken": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
},
|
|
30
|
+
"tokenType": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1
|
|
33
|
+
},
|
|
34
|
+
"scope": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"expiresAt": {
|
|
38
|
+
"type": "integer",
|
|
39
|
+
"minimum": 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { CapacitorOidcError, unsupported } from './errors';
|
|
2
|
-
import { nativeContract } from './generated/native-contract';
|
|
3
2
|
import { NativeOidc } from './native';
|
|
4
3
|
export class CapacitorNavigator {
|
|
5
4
|
prefersEphemeralWebBrowserSession;
|
|
@@ -22,14 +21,14 @@ class CapacitorWindow {
|
|
|
22
21
|
assertSecureRequestUrl(url);
|
|
23
22
|
const callbackUrl = callbackUrlFromRequest(url);
|
|
24
23
|
const response = await NativeOidc.open({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
url,
|
|
25
|
+
callbackUrl,
|
|
26
|
+
prefersEphemeralWebBrowserSession: this.prefersEphemeralWebBrowserSession,
|
|
28
27
|
});
|
|
29
|
-
if (!isExpectedCallback(response
|
|
30
|
-
throw new CapacitorOidcError(
|
|
28
|
+
if (!isExpectedCallback(response.url, callbackUrl)) {
|
|
29
|
+
throw new CapacitorOidcError('INVALID_CALLBACK', 'The authentication callback does not match the configured redirect URI');
|
|
31
30
|
}
|
|
32
|
-
return
|
|
31
|
+
return response;
|
|
33
32
|
}
|
|
34
33
|
close() {
|
|
35
34
|
void NativeOidc.cancel();
|
|
@@ -37,16 +36,19 @@ class CapacitorWindow {
|
|
|
37
36
|
}
|
|
38
37
|
export function assertSecureRequestUrl(requestUrl) {
|
|
39
38
|
const url = new URL(requestUrl);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const isHttpLoopback = url.protocol === 'http:' &&
|
|
40
|
+
(url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]');
|
|
41
|
+
if (url.protocol !== 'https:' && !isHttpLoopback) {
|
|
42
|
+
throw new CapacitorOidcError('BROWSER_UNAVAILABLE', 'OIDC authorization and logout endpoints must use HTTPS');
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
export function callbackUrlFromRequest(requestUrl) {
|
|
46
46
|
const url = new URL(requestUrl);
|
|
47
|
-
const callback = url.searchParams.get('
|
|
47
|
+
const callback = url.searchParams.get('post_logout_redirect_uri') ??
|
|
48
|
+
url.searchParams.get('logout_uri') ??
|
|
49
|
+
url.searchParams.get('redirect_uri');
|
|
48
50
|
if (!callback) {
|
|
49
|
-
throw new CapacitorOidcError(
|
|
51
|
+
throw new CapacitorOidcError('INVALID_CALLBACK', 'The OIDC request does not contain a callback URI');
|
|
50
52
|
}
|
|
51
53
|
return callback;
|
|
52
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capacitor-navigator.js","sourceRoot":"","sources":["../../src/capacitor-navigator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"capacitor-navigator.js","sourceRoot":"","sources":["../../src/capacitor-navigator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { INavigator, IWindow, NavigateParams, NavigateResponse } from 'oidc-client-ts';\n\nimport { CapacitorOidcError, unsupported } from './errors';\nimport { NativeOidc } from './native';\n\nexport class CapacitorNavigator implements INavigator {\n constructor(private readonly prefersEphemeralWebBrowserSession: boolean) {}\n\n async prepare(): Promise<IWindow> {\n return new CapacitorWindow(this.prefersEphemeralWebBrowserSession);\n }\n\n async callback(): Promise<void> {\n unsupported('Navigator callbacks');\n }\n}\n\nclass CapacitorWindow implements IWindow {\n constructor(private readonly prefersEphemeralWebBrowserSession: boolean) {}\n\n async navigate({ url }: NavigateParams): Promise<NavigateResponse> {\n assertSecureRequestUrl(url);\n const callbackUrl = callbackUrlFromRequest(url);\n const response = await NativeOidc.open({\n url,\n callbackUrl,\n prefersEphemeralWebBrowserSession: this.prefersEphemeralWebBrowserSession,\n });\n\n if (!isExpectedCallback(response.url, callbackUrl)) {\n throw new CapacitorOidcError(\n 'INVALID_CALLBACK',\n 'The authentication callback does not match the configured redirect URI',\n );\n }\n\n return response;\n }\n\n close(): void {\n void NativeOidc.cancel();\n }\n}\n\nexport function assertSecureRequestUrl(requestUrl: string): void {\n const url = new URL(requestUrl);\n const isHttpLoopback =\n url.protocol === 'http:' &&\n (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]');\n if (url.protocol !== 'https:' && !isHttpLoopback) {\n throw new CapacitorOidcError('BROWSER_UNAVAILABLE', 'OIDC authorization and logout endpoints must use HTTPS');\n }\n}\n\nexport function callbackUrlFromRequest(requestUrl: string): string {\n const url = new URL(requestUrl);\n const callback =\n url.searchParams.get('post_logout_redirect_uri') ??\n url.searchParams.get('logout_uri') ??\n url.searchParams.get('redirect_uri');\n if (!callback) {\n throw new CapacitorOidcError('INVALID_CALLBACK', 'The OIDC request does not contain a callback URI');\n }\n return callback;\n}\n\nexport function isExpectedCallback(actualValue: string, expectedValue: string): boolean {\n const actual = new URL(actualValue);\n const expected = new URL(expectedValue);\n return (\n actual.protocol.toLowerCase() === expected.protocol.toLowerCase() &&\n actual.host.toLowerCase() === expected.host.toLowerCase() &&\n actual.pathname === expected.pathname\n );\n}\n\nexport class UnsupportedIframeNavigator implements INavigator {\n async prepare(): Promise<IWindow> {\n unsupported('Iframe navigation');\n }\n\n async callback(): Promise<void> {\n unsupported('Iframe navigation');\n }\n}\n"]}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { nativeContract } from './generated/native-contract';
|
|
2
1
|
import { NativeOidc } from './native';
|
|
3
2
|
export class CapacitorSecureStateStore {
|
|
4
3
|
namespace;
|
|
@@ -6,31 +5,16 @@ export class CapacitorSecureStateStore {
|
|
|
6
5
|
this.namespace = namespace;
|
|
7
6
|
}
|
|
8
7
|
async set(key, value) {
|
|
9
|
-
await NativeOidc.storageSet({
|
|
10
|
-
[nativeContract.fields.namespace]: this.namespace,
|
|
11
|
-
[nativeContract.fields.key]: key,
|
|
12
|
-
[nativeContract.fields.value]: value,
|
|
13
|
-
});
|
|
8
|
+
await NativeOidc.storageSet({ namespace: this.namespace, key, value });
|
|
14
9
|
}
|
|
15
10
|
async get(key) {
|
|
16
|
-
|
|
17
|
-
[nativeContract.fields.namespace]: this.namespace,
|
|
18
|
-
[nativeContract.fields.key]: key,
|
|
19
|
-
});
|
|
20
|
-
return result[nativeContract.fields.value];
|
|
11
|
+
return (await NativeOidc.storageGet({ namespace: this.namespace, key })).value;
|
|
21
12
|
}
|
|
22
13
|
async remove(key) {
|
|
23
|
-
|
|
24
|
-
[nativeContract.fields.namespace]: this.namespace,
|
|
25
|
-
[nativeContract.fields.key]: key,
|
|
26
|
-
});
|
|
27
|
-
return result[nativeContract.fields.value];
|
|
14
|
+
return (await NativeOidc.storageRemove({ namespace: this.namespace, key })).value;
|
|
28
15
|
}
|
|
29
16
|
async getAllKeys() {
|
|
30
|
-
|
|
31
|
-
[nativeContract.fields.namespace]: this.namespace,
|
|
32
|
-
});
|
|
33
|
-
return result[nativeContract.fields.keys];
|
|
17
|
+
return (await NativeOidc.storageGetAllKeys({ namespace: this.namespace })).keys;
|
|
34
18
|
}
|
|
35
19
|
}
|
|
36
20
|
//# sourceMappingURL=capacitor-secure-state-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capacitor-secure-state-store.js","sourceRoot":"","sources":["../../src/capacitor-secure-state-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"capacitor-secure-state-store.js","sourceRoot":"","sources":["../../src/capacitor-secure-state-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,MAAM,OAAO,yBAAyB;IACP;IAA7B,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAElD,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa;QAClC,MAAM,UAAU,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,OAAO,CAAC,MAAM,UAAU,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,OAAO,CAAC,MAAM,UAAU,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,UAAU;QACd,OAAO,CAAC,MAAM,UAAU,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAClF,CAAC;CACF","sourcesContent":["import type { StateStore } from 'oidc-client-ts';\n\nimport { NativeOidc } from './native';\n\nexport class CapacitorSecureStateStore implements StateStore {\n constructor(private readonly namespace: string) {}\n\n async set(key: string, value: string): Promise<void> {\n await NativeOidc.storageSet({ namespace: this.namespace, key, value });\n }\n\n async get(key: string): Promise<string | null> {\n return (await NativeOidc.storageGet({ namespace: this.namespace, key })).value;\n }\n\n async remove(key: string): Promise<string | null> {\n return (await NativeOidc.storageRemove({ namespace: this.namespace, key })).value;\n }\n\n async getAllKeys(): Promise<string[]> {\n return (await NativeOidc.storageGetAllKeys({ namespace: this.namespace })).keys;\n }\n}\n"]}
|