capacitor-oidc 0.0.1-alpha.2 → 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/README.md +66 -90
- package/SECURITY.md +43 -14
- 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 +9 -5
- package/docs/README.md +23 -0
- package/docs/SESSIONS_AND_WIDGETS.md +82 -0
- package/docs/TESTING.md +29 -16
- package/docs/TROUBLESHOOTING.md +67 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,32 +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
|
-
All discovery, token, refresh, UserInfo, and revocation requests use normal WebView `fetch`. The provider must allow the app's configured Capacitor origin through CORS.
|
|
26
|
-
|
|
27
|
-
Authorization and logout endpoints must use HTTPS. Plain HTTP is accepted only for loopback local development.
|
|
28
|
-
|
|
29
|
-
## Usage
|
|
46
|
+
## Quick start
|
|
30
47
|
|
|
31
48
|
```ts
|
|
32
49
|
import { CapacitorUserManager } from 'capacitor-oidc';
|
|
@@ -42,96 +59,55 @@ const manager = await CapacitorUserManager.create(
|
|
|
42
59
|
revokeTokensOnSignout: true,
|
|
43
60
|
},
|
|
44
61
|
{
|
|
45
|
-
prefersEphemeralWebBrowserSession: false,
|
|
46
62
|
storageNamespace: 'primary',
|
|
47
|
-
ios: {
|
|
48
|
-
keychainAccessGroup: 'TEAMID.group.com.example.app',
|
|
49
|
-
keychainAccessibility: 'afterFirstUnlockThisDeviceOnly',
|
|
50
|
-
},
|
|
51
63
|
},
|
|
52
64
|
);
|
|
53
65
|
|
|
54
66
|
const user = await manager.signin();
|
|
55
67
|
const validUser = await manager.getValidUser(30);
|
|
68
|
+
|
|
56
69
|
await manager.signout();
|
|
57
70
|
await manager.dispose();
|
|
58
71
|
```
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Redirect setup
|
|
65
|
-
|
|
66
|
-
Register the redirect URI as a native public-client redirect at the provider. Never put a client secret in the app.
|
|
67
|
-
|
|
68
|
-
For an iOS custom scheme, add it to the application target's `CFBundleURLTypes`. HTTPS callbacks through `ASWebAuthenticationSession` require iOS 17.4 or newer and the appropriate Associated Domains configuration.
|
|
69
|
-
|
|
70
|
-
For an Android custom-scheme redirect, keep the host app's `MainActivity` in Capacitor's default `singleTask` launch mode and add this intent filter inside that existing activity declaration. This ensures callbacks from Auth Tab's Custom Tab fallback reach the plugin instance that opened the session. Replace the scheme with the one used by your redirect URI:
|
|
71
|
-
|
|
72
|
-
```xml
|
|
73
|
-
<activity
|
|
74
|
-
android:name=".MainActivity"
|
|
75
|
-
android:launchMode="singleTask">
|
|
76
|
-
<intent-filter>
|
|
77
|
-
<action android:name="android.intent.action.VIEW" />
|
|
78
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
79
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
80
|
-
<data android:scheme="com.example.app" />
|
|
81
|
-
</intent-filter>
|
|
82
|
-
</activity>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
HTTPS callbacks require a verified App Link and Digital Asset Links. Auth Tab handles the result directly when the installed browser supports it and falls back to a Custom Tab on older browsers.
|
|
86
|
-
|
|
87
|
-
Calling `cancel()` on Android rejects the pending JavaScript promise, but Android does not provide an API to forcibly close an already-open system Auth Tab or Custom Tab. Ephemeral browsing is requested on both platforms and may be ignored by an Android fallback browser.
|
|
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.
|
|
88
76
|
|
|
89
|
-
|
|
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.
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
await manager.signout({
|
|
93
|
-
extraQueryParams: {
|
|
94
|
-
client_id: 'mobile-app',
|
|
95
|
-
logout_uri: 'com.example.app:/logout-callback',
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
```
|
|
81
|
+
## Documentation
|
|
99
82
|
|
|
100
|
-
|
|
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)
|
|
101
91
|
|
|
102
|
-
|
|
92
|
+
## Session renewal
|
|
103
93
|
|
|
104
|
-
|
|
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.
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let session = try vault.loadSession(namespace: "primary")
|
|
109
|
-
```
|
|
99
|
+
The operating system can suspend or terminate the app, so the package does not
|
|
100
|
+
promise exact background refresh timing.
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
Keychain Sharing entitlement, normally declared as
|
|
113
|
-
`$(AppIdentifierPrefix)group.com.example.app`; the runtime option uses its expanded
|
|
114
|
-
`TEAMID.group.com.example.app` value. The default accessibility is
|
|
115
|
-
`AfterFirstUnlockThisDeviceOnly`; Keychain synchronization and biometric gating are disabled.
|
|
102
|
+
## Networking
|
|
116
103
|
|
|
117
|
-
|
|
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.
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
StoredSessionV1 session = vault.loadSession("primary");
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Widgets may read the current session. Autonomous native refresh is deliberately not implemented.
|
|
125
|
-
|
|
126
|
-
## Logout behavior
|
|
127
|
-
|
|
128
|
-
`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.
|
|
129
|
-
|
|
130
|
-
## Unsupported APIs
|
|
131
|
-
|
|
132
|
-
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.
|
|
133
|
-
|
|
134
|
-
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.
|
|
135
111
|
|
|
136
112
|
## Development
|
|
137
113
|
|
|
@@ -141,9 +117,9 @@ npm run verify:ios
|
|
|
141
117
|
npm run verify:android
|
|
142
118
|
```
|
|
143
119
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
120
|
+
Architecture decisions and contributor constraints are documented in
|
|
121
|
+
[ARCHITECTURE.md](ARCHITECTURE.md), [REQUIREMENTS.md](REQUIREMENTS.md), and
|
|
122
|
+
[docs/adr](docs/adr).
|
|
147
123
|
|
|
148
124
|
## License
|
|
149
125
|
|
package/SECURITY.md
CHANGED
|
@@ -7,25 +7,54 @@
|
|
|
7
7
|
and storing state securely. The package does not patch `fetch`, process JWTs, or
|
|
8
8
|
implement OAuth endpoints in Swift or Java.
|
|
9
9
|
|
|
10
|
-
|
|
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.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
checks `sub`, `nonce`, and selected refresh-token continuity claims, but does not
|
|
14
|
-
check the required `iss`, `aud`, and `exp` claims. The behavior is visible in the
|
|
15
|
-
[v3.5.0 ResponseValidator](https://github.com/authts/oidc-client-ts/blob/v3.5.0/src/ResponseValidator.ts)
|
|
16
|
-
and is tracked in [authts/oidc-client-ts#2475](https://github.com/authts/oidc-client-ts/issues/2475).
|
|
14
|
+
## ID-token validation in `oidc-client-ts` 3.5.0
|
|
17
15
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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).
|
|
23
52
|
|
|
24
53
|
## Runtime assumptions
|
|
25
54
|
|
|
26
55
|
`oidc-client-ts` requires `crypto.subtle` and `crypto.getRandomValues`.
|
|
27
56
|
`CapacitorUserManager.create()` rejects with `UNSUPPORTED_RUNTIME` when they are
|
|
28
|
-
absent. Their
|
|
29
|
-
|
|
57
|
+
absent. Their behavior must be verified in packaged applications on supported
|
|
58
|
+
physical devices.
|
|
30
59
|
|
|
31
|
-
See [Testing](docs/TESTING.md) for
|
|
60
|
+
See [Testing](docs/TESTING.md) for current platform and provider coverage.
|
package/docs/API.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# API and `oidc-client-ts` compatibility
|
|
2
|
+
|
|
3
|
+
`CapacitorUserManager` extends `UserManager` from `oidc-client-ts`. The adapter
|
|
4
|
+
adds a small native-friendly surface and preserves upstream session objects,
|
|
5
|
+
events, settings, refresh, UserInfo, and revocation behavior where they apply to
|
|
6
|
+
a native public client.
|
|
7
|
+
|
|
8
|
+
## Added API
|
|
9
|
+
|
|
10
|
+
| API | Purpose |
|
|
11
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
12
|
+
| `CapacitorUserManager.create(settings, nativeOptions?)` | Configures native storage and creates the manager. |
|
|
13
|
+
| `signin(args?)` | Runs interactive Authorization Code Flow with PKCE in system UI. |
|
|
14
|
+
| `signout(args?)` | Runs provider logout in system UI. |
|
|
15
|
+
| `getValidUser(minimumValiditySeconds?)` | Returns the user or performs one refresh-token renewal. |
|
|
16
|
+
| `cancel()` | Rejects the pending native session. |
|
|
17
|
+
| `dispose()` | Stops renewal, removes the app listener, and cancels pending navigation. |
|
|
18
|
+
|
|
19
|
+
Only one configured manager and one interactive native authentication session are
|
|
20
|
+
supported at a time.
|
|
21
|
+
|
|
22
|
+
## Relevant inherited API
|
|
23
|
+
|
|
24
|
+
The following upstream APIs remain available:
|
|
25
|
+
|
|
26
|
+
- `getUser()`, `storeUser()`, and `removeUser()`;
|
|
27
|
+
- `signinSilent()` for refresh-token renewal;
|
|
28
|
+
- `revokeTokens()`;
|
|
29
|
+
- `startSilentRenew()` and `stopSilentRenew()`;
|
|
30
|
+
- `events`, including user-loaded, user-unloaded, token-expiring, token-expired,
|
|
31
|
+
and silent-renew-error events;
|
|
32
|
+
- the upstream `User`, profile, settings, metadata, UserInfo, and token response
|
|
33
|
+
types.
|
|
34
|
+
|
|
35
|
+
`signinPopup()` and `signoutPopup()` are inherited and use the native navigator,
|
|
36
|
+
but applications should prefer `signin()` and `signout()`.
|
|
37
|
+
|
|
38
|
+
## Unsupported browser API
|
|
39
|
+
|
|
40
|
+
These inherited browser-oriented methods reject with `UNSUPPORTED_RUNTIME`:
|
|
41
|
+
|
|
42
|
+
- `signinRedirect()`, `signinRedirectCallback()`, and `signinCallback()`;
|
|
43
|
+
- `signinSilentCallback()` and iframe-based silent authentication;
|
|
44
|
+
- `signoutRedirect()`, `signoutRedirectCallback()`, and `signoutCallback()`;
|
|
45
|
+
- `signoutSilent()` and `signoutSilentCallback()`;
|
|
46
|
+
- `querySessionStatus()`;
|
|
47
|
+
- Resource Owner Password Credentials.
|
|
48
|
+
|
|
49
|
+
The native settings type excludes:
|
|
50
|
+
|
|
51
|
+
- `client_secret` and `client_authentication`;
|
|
52
|
+
- `disablePKCE` and `response_type`;
|
|
53
|
+
- `dpop`;
|
|
54
|
+
- `monitorSession` and `silent_redirect_uri`;
|
|
55
|
+
- custom `stateStore` and `userStore`.
|
|
56
|
+
|
|
57
|
+
The adapter forces Authorization Code Flow with PKCE, secure native stores, and
|
|
58
|
+
no iframe session monitoring.
|
|
59
|
+
|
|
60
|
+
## Native options
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
interface CapacitorOidcNativeOptions {
|
|
64
|
+
prefersEphemeralWebBrowserSession?: boolean;
|
|
65
|
+
storageNamespace?: string;
|
|
66
|
+
ios?: {
|
|
67
|
+
keychainAccessGroup?: string;
|
|
68
|
+
keychainAccessibility?: 'afterFirstUnlockThisDeviceOnly' | 'whenUnlockedThisDeviceOnly';
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`storageNamespace` defaults to `default`. Use a stable, application-specific
|
|
74
|
+
value and do not change it between releases unless intentionally starting a new
|
|
75
|
+
session store.
|
|
76
|
+
|
|
77
|
+
## Adapter error codes
|
|
78
|
+
|
|
79
|
+
| Code | Meaning |
|
|
80
|
+
| -------------------------- | ------------------------------------------------------------------------ |
|
|
81
|
+
| `AUTH_SESSION_IN_PROGRESS` | Another native login or logout is active. |
|
|
82
|
+
| `USER_CANCELLED` | The user or application cancelled the system session. |
|
|
83
|
+
| `BROWSER_UNAVAILABLE` | No compatible browser exists or the request endpoint is insecure. |
|
|
84
|
+
| `INVALID_CALLBACK` | The callback is absent, malformed, or does not match the configured URI. |
|
|
85
|
+
| `SECURE_STORAGE_ERROR` | Keychain or Keystore-backed storage failed. |
|
|
86
|
+
| `UNSUPPORTED_RUNTIME` | Web Crypto is unavailable or a browser-only API was called. |
|
|
87
|
+
|
|
88
|
+
OAuth and OIDC server errors remain upstream `oidc-client-ts` errors.
|
|
89
|
+
|
|
90
|
+
For the complete upstream model and event API, use the
|
|
91
|
+
[`oidc-client-ts` documentation](https://authts.github.io/oidc-client-ts/).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## 1. Configure a public client
|
|
4
|
+
|
|
5
|
+
Create a native or public client at your identity provider with:
|
|
6
|
+
|
|
7
|
+
- Authorization Code Flow enabled;
|
|
8
|
+
- PKCE with the `S256` challenge method;
|
|
9
|
+
- no client secret;
|
|
10
|
+
- an exact native redirect URI, such as `com.example.app:/callback`;
|
|
11
|
+
- an exact post-logout redirect URI when the provider supports one;
|
|
12
|
+
- the `openid` scope and any application scopes you need;
|
|
13
|
+
- a refresh-token or offline-access grant if the app must renew sessions.
|
|
14
|
+
|
|
15
|
+
The redirect URI must be registered at the provider and in the native app. A
|
|
16
|
+
minor difference in scheme, host, path, casing, or slash placement can prevent
|
|
17
|
+
the callback from completing.
|
|
18
|
+
|
|
19
|
+
## 2. Install the package
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install capacitor-oidc @capacitor/app
|
|
23
|
+
npx cap sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Complete the [iOS or Android setup](PLATFORM_SETUP.md) before running the app.
|
|
27
|
+
|
|
28
|
+
## 3. Create one manager
|
|
29
|
+
|
|
30
|
+
Create and retain one manager for the application session. Recreating managers
|
|
31
|
+
for individual API calls also recreates listeners and can produce competing
|
|
32
|
+
renewal work.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { CapacitorUserManager } from 'capacitor-oidc';
|
|
36
|
+
|
|
37
|
+
export const auth = await CapacitorUserManager.create(
|
|
38
|
+
{
|
|
39
|
+
authority: 'https://identity.example.com',
|
|
40
|
+
client_id: 'mobile-app',
|
|
41
|
+
redirect_uri: 'com.example.app:/callback',
|
|
42
|
+
post_logout_redirect_uri: 'com.example.app:/logout-callback',
|
|
43
|
+
scope: 'openid profile offline_access',
|
|
44
|
+
automaticSilentRenew: true,
|
|
45
|
+
loadUserInfo: true,
|
|
46
|
+
revokeTokensOnSignout: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
prefersEphemeralWebBrowserSession: false,
|
|
50
|
+
storageNamespace: 'primary',
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`prefersEphemeralWebBrowserSession` defaults to `false`, allowing the system
|
|
56
|
+
browser to reuse an existing provider session. Set it to `true` when an isolated
|
|
57
|
+
session is more important than shared SSO. Android fallback browsers may ignore
|
|
58
|
+
the preference.
|
|
59
|
+
|
|
60
|
+
The current alpha restores the stored user during `create()`. If that user needs
|
|
61
|
+
immediate renewal and the device is offline, manager creation can reject. This
|
|
62
|
+
behavior is tracked for improvement; applications should currently initialize
|
|
63
|
+
the manager where they can present an authentication or connectivity error.
|
|
64
|
+
|
|
65
|
+
## 4. Sign in
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const user = await auth.signin();
|
|
69
|
+
|
|
70
|
+
console.log(user.profile.sub);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The promise resolves after the system authentication session returns to the app,
|
|
74
|
+
the authorization response is validated, and the code is exchanged.
|
|
75
|
+
|
|
76
|
+
Listen for session changes through the normal `oidc-client-ts` events:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
auth.events.addUserLoaded((user) => {
|
|
80
|
+
console.log('Signed in as', user.profile.sub);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
auth.events.addSilentRenewError((error) => {
|
|
84
|
+
console.error('Token renewal failed', error);
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 5. Get a usable access token
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const user = await auth.getValidUser(30);
|
|
92
|
+
|
|
93
|
+
if (!user) {
|
|
94
|
+
// No signed-in session is available.
|
|
95
|
+
} else {
|
|
96
|
+
await fetch('https://api.example.com/profile', {
|
|
97
|
+
headers: { Authorization: `Bearer ${user.access_token}` },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`getValidUser(30)` returns the current user when its access token is valid for at
|
|
103
|
+
least 30 more seconds. Otherwise it performs one refresh-token renewal. Concurrent
|
|
104
|
+
renewal triggers share the same request.
|
|
105
|
+
|
|
106
|
+
## 6. Sign out
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await auth.signout();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`signout()` opens the provider's end-session endpoint in the same native system
|
|
113
|
+
authentication UI. Provider-specific parameters can be passed through
|
|
114
|
+
`extraQueryParams`; see [Provider configuration](PROVIDERS.md).
|
|
115
|
+
|
|
116
|
+
Use `removeUser()` when the application intentionally needs local-only logout.
|
|
117
|
+
|
|
118
|
+
## 7. Dispose application listeners
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await auth.dispose();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Call `dispose()` when the manager will no longer be used. It stops silent renewal,
|
|
125
|
+
removes the Capacitor app-state listener, and cancels a pending native session.
|
|
126
|
+
|
|
127
|
+
## Browser builds
|
|
128
|
+
|
|
129
|
+
`CapacitorUserManager` depends on native Capacitor plugins and is not a web
|
|
130
|
+
replacement for `UserManager`. Applications that also run in a normal browser
|
|
131
|
+
should instantiate `UserManager` from `oidc-client-ts` for the web branch and
|
|
132
|
+
`CapacitorUserManager` for the native branch.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# iOS and Android setup
|
|
2
|
+
|
|
3
|
+
## iOS
|
|
4
|
+
|
|
5
|
+
The package supports iOS 15 and newer. Interactive login and logout use
|
|
6
|
+
`ASWebAuthenticationSession`, including the operating system's provider-consent
|
|
7
|
+
dialog and sheet presentation.
|
|
8
|
+
|
|
9
|
+
### Custom-scheme callback
|
|
10
|
+
|
|
11
|
+
For `com.example.app:/callback`, register `com.example.app` in the application
|
|
12
|
+
target's `Info.plist`:
|
|
13
|
+
|
|
14
|
+
```xml
|
|
15
|
+
<key>CFBundleURLTypes</key>
|
|
16
|
+
<array>
|
|
17
|
+
<dict>
|
|
18
|
+
<key>CFBundleURLSchemes</key>
|
|
19
|
+
<array>
|
|
20
|
+
<string>com.example.app</string>
|
|
21
|
+
</array>
|
|
22
|
+
</dict>
|
|
23
|
+
</array>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use an application-specific scheme that your organization controls. Register the
|
|
27
|
+
complete URI at the provider.
|
|
28
|
+
|
|
29
|
+
### HTTPS callback
|
|
30
|
+
|
|
31
|
+
HTTPS callbacks through `ASWebAuthenticationSession` require iOS 17.4 or newer.
|
|
32
|
+
Configure the appropriate Associated Domains entitlement and association file,
|
|
33
|
+
then register the exact HTTPS callback at the provider.
|
|
34
|
+
|
|
35
|
+
### Shared Keychain access
|
|
36
|
+
|
|
37
|
+
To let an iOS widget read the stored session, add the same Keychain Sharing
|
|
38
|
+
entitlement to the application and extension, then configure the expanded access
|
|
39
|
+
group:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const manager = await CapacitorUserManager.create(settings, {
|
|
43
|
+
ios: {
|
|
44
|
+
keychainAccessGroup: 'TEAMID.group.com.example.app',
|
|
45
|
+
keychainAccessibility: 'afterFirstUnlockThisDeviceOnly',
|
|
46
|
+
},
|
|
47
|
+
storageNamespace: 'primary',
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See [Sessions, secure storage, and widgets](SESSIONS_AND_WIDGETS.md) before
|
|
52
|
+
enabling shared access.
|
|
53
|
+
|
|
54
|
+
## Android
|
|
55
|
+
|
|
56
|
+
The Android adapter supports API 24 and newer. It is currently built and tested
|
|
57
|
+
with compile SDK 36, Android Gradle Plugin 8.9.1, Java 21, AndroidX Activity
|
|
58
|
+
1.11.0, and AndroidX Browser 1.10.0. Capacitor 7 applications may therefore need
|
|
59
|
+
their Android toolchain upgraded even though the JavaScript peer dependency
|
|
60
|
+
allows Capacitor 7.
|
|
61
|
+
|
|
62
|
+
### Custom-scheme callback
|
|
63
|
+
|
|
64
|
+
Keep the host application's existing `MainActivity` in Capacitor's default
|
|
65
|
+
`singleTask` launch mode. Add the callback intent filter inside that same activity
|
|
66
|
+
declaration:
|
|
67
|
+
|
|
68
|
+
```xml
|
|
69
|
+
<activity
|
|
70
|
+
android:name=".MainActivity"
|
|
71
|
+
android:launchMode="singleTask">
|
|
72
|
+
<intent-filter>
|
|
73
|
+
<action android:name="android.intent.action.VIEW" />
|
|
74
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
75
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
76
|
+
<data android:scheme="com.example.app" />
|
|
77
|
+
</intent-filter>
|
|
78
|
+
</activity>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `singleTask` activity receives callbacks from Auth Tab's Custom Tab fallback
|
|
82
|
+
through `onNewIntent`, allowing Capacitor to route the URL to the plugin instance
|
|
83
|
+
that opened the session.
|
|
84
|
+
|
|
85
|
+
### HTTPS callback
|
|
86
|
+
|
|
87
|
+
Configure a verified Android App Link and publish a matching Digital Asset Links
|
|
88
|
+
file. Register the exact HTTPS URI at the identity provider.
|
|
89
|
+
|
|
90
|
+
### Auth Tab behavior
|
|
91
|
+
|
|
92
|
+
AndroidX Auth Tab handles the result directly when the installed browser supports
|
|
93
|
+
it and falls back to a Custom Tab on older browsers. Calling `cancel()` rejects
|
|
94
|
+
the pending JavaScript promise, but Android does not expose an API that forcibly
|
|
95
|
+
closes an already-visible system Auth Tab or Custom Tab.
|
|
96
|
+
|
|
97
|
+
Ephemeral browsing is requested when configured and may be ignored by a fallback
|
|
98
|
+
browser.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Provider configuration
|
|
2
|
+
|
|
3
|
+
`capacitor-oidc` works with providers that support Authorization Code Flow with
|
|
4
|
+
PKCE for public clients and permit token-related requests from the configured
|
|
5
|
+
Capacitor origin.
|
|
6
|
+
|
|
7
|
+
## Provider checklist
|
|
8
|
+
|
|
9
|
+
For every provider:
|
|
10
|
+
|
|
11
|
+
1. Create a native, mobile, SPA, or other public client. Do not create or embed a
|
|
12
|
+
client secret.
|
|
13
|
+
2. Enable Authorization Code Flow and PKCE with `S256`.
|
|
14
|
+
3. Register the native redirect and post-logout redirect URIs exactly.
|
|
15
|
+
4. Enable refresh tokens and request the provider's offline-access scope if the
|
|
16
|
+
app must renew sessions.
|
|
17
|
+
5. Allow the app's Capacitor origin through CORS for discovery, token, refresh,
|
|
18
|
+
UserInfo, and revocation endpoints used by your configuration.
|
|
19
|
+
6. Use HTTPS for provider endpoints.
|
|
20
|
+
|
|
21
|
+
The recipes below identify the relevant authority and provider-console choices.
|
|
22
|
+
They do not replace the provider's own security guidance.
|
|
23
|
+
|
|
24
|
+
| Provider | Current package validation |
|
|
25
|
+
| ------------------ | --------------------------------------------------------------- |
|
|
26
|
+
| Amazon Cognito | Basic login tested on a physical iOS device; edge cases remain. |
|
|
27
|
+
| Auth0 | Configuration guidance only. |
|
|
28
|
+
| Keycloak | Configuration guidance only. |
|
|
29
|
+
| Okta | Configuration guidance only. |
|
|
30
|
+
| Microsoft Entra ID | Configuration guidance only. |
|
|
31
|
+
|
|
32
|
+
## Amazon Cognito
|
|
33
|
+
|
|
34
|
+
Cognito separates the OIDC issuer from the managed-login domain. Use the user
|
|
35
|
+
pool issuer as `authority` and provide explicit metadata pointing browser-facing
|
|
36
|
+
endpoints at the managed-login domain:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const region = 'eu-central-1';
|
|
40
|
+
const userPoolId = 'eu-central-1_example';
|
|
41
|
+
const clientId = 'public-client-id';
|
|
42
|
+
const domain = 'https://example.auth.eu-central-1.amazoncognito.com';
|
|
43
|
+
const issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
|
|
44
|
+
|
|
45
|
+
const manager = await CapacitorUserManager.create({
|
|
46
|
+
authority: issuer,
|
|
47
|
+
client_id: clientId,
|
|
48
|
+
redirect_uri: 'com.example.app://oauth',
|
|
49
|
+
scope: 'openid email profile aws.cognito.signin.user.admin',
|
|
50
|
+
automaticSilentRenew: true,
|
|
51
|
+
revokeTokensOnSignout: true,
|
|
52
|
+
metadata: {
|
|
53
|
+
issuer,
|
|
54
|
+
authorization_endpoint: `${domain}/oauth2/authorize`,
|
|
55
|
+
token_endpoint: `${domain}/oauth2/token`,
|
|
56
|
+
userinfo_endpoint: `${domain}/oauth2/userInfo`,
|
|
57
|
+
revocation_endpoint: `${domain}/oauth2/revoke`,
|
|
58
|
+
end_session_endpoint: `${domain}/logout`,
|
|
59
|
+
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Configure the Cognito app client without a secret and register
|
|
65
|
+
`com.example.app://oauth` as an allowed callback and sign-out URL.
|
|
66
|
+
|
|
67
|
+
Cognito logout uses `logout_uri` instead of the standard
|
|
68
|
+
`post_logout_redirect_uri`. Leave `post_logout_redirect_uri` unset in the manager
|
|
69
|
+
settings and pass Cognito's parameters when signing out:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
await manager.signout({
|
|
73
|
+
extraQueryParams: {
|
|
74
|
+
client_id: clientId,
|
|
75
|
+
logout_uri: 'com.example.app://oauth',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See the AWS documentation for the
|
|
81
|
+
[authorization endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html),
|
|
82
|
+
[token endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html),
|
|
83
|
+
and [logout endpoint](https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html).
|
|
84
|
+
|
|
85
|
+
## Auth0
|
|
86
|
+
|
|
87
|
+
Use the tenant or custom domain as the authority:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const settings = {
|
|
91
|
+
authority: 'https://example.eu.auth0.com',
|
|
92
|
+
client_id: 'native-application-client-id',
|
|
93
|
+
redirect_uri: 'com.example.app:/callback',
|
|
94
|
+
post_logout_redirect_uri: 'com.example.app:/logout-callback',
|
|
95
|
+
scope: 'openid profile email offline_access',
|
|
96
|
+
automaticSilentRenew: true,
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Create a Native application, register both callback URLs, enable refresh-token
|
|
101
|
+
rotation as appropriate, and add the Capacitor origin to the allowed web origins
|
|
102
|
+
used for CORS.
|
|
103
|
+
|
|
104
|
+
See Auth0's [Capacitor quickstart](https://auth0.com/docs/quickstart/native/ionic-react)
|
|
105
|
+
and [refresh-token rotation guidance](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation).
|
|
106
|
+
|
|
107
|
+
## Keycloak
|
|
108
|
+
|
|
109
|
+
Use the realm URL as the authority:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const settings = {
|
|
113
|
+
authority: 'https://identity.example.com/realms/example',
|
|
114
|
+
client_id: 'mobile-app',
|
|
115
|
+
redirect_uri: 'com.example.app:/callback',
|
|
116
|
+
post_logout_redirect_uri: 'com.example.app:/logout-callback',
|
|
117
|
+
scope: 'openid profile offline_access',
|
|
118
|
+
automaticSilentRenew: true,
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Create an OpenID Connect client with client authentication disabled, Standard
|
|
123
|
+
Flow enabled, an exact valid redirect URI, and the Capacitor origin in Web
|
|
124
|
+
Origins. Configure valid post-logout redirect URIs when using provider logout.
|
|
125
|
+
|
|
126
|
+
See the Keycloak documentation for
|
|
127
|
+
[managing OIDC clients](https://www.keycloak.org/docs/latest/server_admin/#_oidc_clients).
|
|
128
|
+
|
|
129
|
+
## Okta
|
|
130
|
+
|
|
131
|
+
Use the authorization-server issuer as the authority. For the default custom
|
|
132
|
+
authorization server this commonly has the following form:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const settings = {
|
|
136
|
+
authority: 'https://example.okta.com/oauth2/default',
|
|
137
|
+
client_id: 'native-application-client-id',
|
|
138
|
+
redirect_uri: 'com.example.app:/callback',
|
|
139
|
+
post_logout_redirect_uri: 'com.example.app:/logout-callback',
|
|
140
|
+
scope: 'openid profile offline_access',
|
|
141
|
+
automaticSilentRenew: true,
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Create a Native Application integration, require PKCE, and register exact sign-in
|
|
146
|
+
and sign-out redirect URIs.
|
|
147
|
+
|
|
148
|
+
See Okta's guide to
|
|
149
|
+
[signing users into a mobile app with redirect](https://developer.okta.com/docs/guides/sign-into-mobile-app-redirect/).
|
|
150
|
+
|
|
151
|
+
## Microsoft Entra ID
|
|
152
|
+
|
|
153
|
+
Use a tenant-specific v2 issuer when the application belongs to one tenant:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const settings = {
|
|
157
|
+
authority: 'https://login.microsoftonline.com/TENANT_ID/v2.0',
|
|
158
|
+
client_id: 'application-client-id',
|
|
159
|
+
redirect_uri: 'com.example.app:/callback',
|
|
160
|
+
post_logout_redirect_uri: 'com.example.app:/logout-callback',
|
|
161
|
+
scope: 'openid profile offline_access',
|
|
162
|
+
automaticSilentRenew: true,
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Configure a mobile and desktop application platform, register the native redirect
|
|
167
|
+
URI accepted by the app registration, and enable public-client flows required by
|
|
168
|
+
your tenant policy. Multi-tenant authorities and account selection require an
|
|
169
|
+
application-specific design rather than simply replacing `TENANT_ID` with a
|
|
170
|
+
shared authority.
|
|
171
|
+
|
|
172
|
+
See Microsoft Learn for
|
|
173
|
+
[mobile and desktop authentication flows](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-overview)
|
|
174
|
+
and [redirect URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url).
|
|
175
|
+
|
|
176
|
+
## Other providers
|
|
177
|
+
|
|
178
|
+
Start with the generic checklist and the provider's OpenID Connect discovery
|
|
179
|
+
document. If discovery does not describe a usable native flow, pass explicit
|
|
180
|
+
`metadata` through the upstream `UserManagerSettings` rather than adding
|
|
181
|
+
provider-specific code to this package.
|
|
182
|
+
|
|
183
|
+
Provider-specific compatibility reports and tested configuration examples are
|
|
184
|
+
welcome as focused contributions.
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -71,11 +71,15 @@ Stable production releases additionally require the checks in
|
|
|
71
71
|
3. Publish a GitHub release whose tag is exactly `vX.Y.Z` or `vX.Y.Z-prerelease`. Mark a prerelease version as a GitHub prerelease.
|
|
72
72
|
4. Approve the `npm-production` deployment after its checks are visible.
|
|
73
73
|
|
|
74
|
-
The workflow installs from the lockfile without a package-manager cache,
|
|
75
|
-
the package,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
The workflow first installs from the lockfile without a package-manager cache,
|
|
75
|
+
verifies the package, and creates the release tarball in a job without OIDC
|
|
76
|
+
permissions. After that succeeds, the `npm-production` environment gates a separate
|
|
77
|
+
job that downloads and publishes only that tarball, with package scripts disabled.
|
|
78
|
+
The privileged job does not check out source, install dependencies, or run repository
|
|
79
|
+
build scripts. Stable releases use `latest`; prereleases use `next`. The workflow is
|
|
80
|
+
skipped unless `NPM_PUBLISH_ENABLED` is exactly `true`. npm automatically generates
|
|
81
|
+
provenance for a public package published from this public repository through a
|
|
82
|
+
trusted publisher.
|
|
79
83
|
|
|
80
84
|
After the first trusted publication succeeds, configure npm to disallow traditional
|
|
81
85
|
token publishing and revoke unused automation tokens. Trusted publishing continues
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
`capacitor-oidc` is a native Capacitor adapter for `oidc-client-ts`. Start with
|
|
4
|
+
[Getting started](GETTING_STARTED.md), then complete the setup for both your
|
|
5
|
+
native platform and identity provider.
|
|
6
|
+
|
|
7
|
+
## Use the package
|
|
8
|
+
|
|
9
|
+
- [Getting started](GETTING_STARTED.md)
|
|
10
|
+
- [iOS and Android setup](PLATFORM_SETUP.md)
|
|
11
|
+
- [Provider configuration](PROVIDERS.md)
|
|
12
|
+
- [API and `oidc-client-ts` compatibility](API.md)
|
|
13
|
+
- [Sessions, secure storage, and widgets](SESSIONS_AND_WIDGETS.md)
|
|
14
|
+
- [Troubleshooting](TROUBLESHOOTING.md)
|
|
15
|
+
- [Security](../SECURITY.md)
|
|
16
|
+
|
|
17
|
+
## Contribute and release
|
|
18
|
+
|
|
19
|
+
- [Architecture](../ARCHITECTURE.md)
|
|
20
|
+
- [Requirements and acceptance criteria](../REQUIREMENTS.md)
|
|
21
|
+
- [Testing](TESTING.md)
|
|
22
|
+
- [Publishing](PUBLISHING.md)
|
|
23
|
+
- [Architecture decisions](adr)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Sessions, secure storage, and widgets
|
|
2
|
+
|
|
3
|
+
## Canonical session storage
|
|
4
|
+
|
|
5
|
+
The canonical `oidc-client-ts` user and authorization transaction state are
|
|
6
|
+
stored in separate native-secure namespaces. Values are never written to
|
|
7
|
+
`localStorage`.
|
|
8
|
+
|
|
9
|
+
- iOS stores generic-password items in Keychain with synchronization disabled.
|
|
10
|
+
- Android encrypts values with AES-GCM using a non-exportable Android Keystore
|
|
11
|
+
key and writes ciphertext to app-private no-backup storage.
|
|
12
|
+
|
|
13
|
+
The default iOS accessibility is `AfterFirstUnlockThisDeviceOnly`. Biometric
|
|
14
|
+
gating is not enabled because unattended foreground renewal must remain possible.
|
|
15
|
+
|
|
16
|
+
## Renewal lifecycle
|
|
17
|
+
|
|
18
|
+
When `automaticSilentRenew` is enabled, `oidc-client-ts` schedules renewal while
|
|
19
|
+
the JavaScript runtime is active. The adapter also checks the current user when
|
|
20
|
+
the application resumes.
|
|
21
|
+
|
|
22
|
+
Native silent renewal requires a refresh token and never falls back to an iframe.
|
|
23
|
+
Concurrent renewal requests share one operation so a rotating refresh token is
|
|
24
|
+
not used twice. A terminal `invalid_grant` removes the local user; a temporary
|
|
25
|
+
network failure preserves it.
|
|
26
|
+
|
|
27
|
+
The operating system can suspend or terminate the process. The package therefore
|
|
28
|
+
does not guarantee exact background refresh timing.
|
|
29
|
+
|
|
30
|
+
## Widget snapshot
|
|
31
|
+
|
|
32
|
+
Each successful canonical user write also updates a versioned `StoredSessionV1`
|
|
33
|
+
snapshot containing:
|
|
34
|
+
|
|
35
|
+
- issuer and client ID;
|
|
36
|
+
- access token and expiration;
|
|
37
|
+
- refresh token when issued;
|
|
38
|
+
- ID token when issued;
|
|
39
|
+
- token type and scope.
|
|
40
|
+
|
|
41
|
+
The refresh and ID tokens are intentionally retained so a future coordinated
|
|
42
|
+
native widget-refresh implementation does not require a storage-format migration.
|
|
43
|
+
This makes the widget extension part of the same credential trust boundary as
|
|
44
|
+
the host application.
|
|
45
|
+
|
|
46
|
+
The current plugin does **not** perform autonomous widget refresh. Widgets can use
|
|
47
|
+
a valid access token from the snapshot, but they must tolerate an expired, absent,
|
|
48
|
+
or temporarily stale snapshot. Independently rotating a refresh token in widget
|
|
49
|
+
code can invalidate the canonical application session and is not currently a
|
|
50
|
+
supported workflow.
|
|
51
|
+
|
|
52
|
+
The snapshot is an eventually consistent cache: the canonical OIDC user and the
|
|
53
|
+
snapshot are separate writes.
|
|
54
|
+
|
|
55
|
+
## iOS widget access
|
|
56
|
+
|
|
57
|
+
Configure the same Keychain Sharing entitlement on the application and WidgetKit
|
|
58
|
+
extension. Both targets normally declare
|
|
59
|
+
`$(AppIdentifierPrefix)group.com.example.app`; the JavaScript option uses the
|
|
60
|
+
expanded `TEAMID.group.com.example.app` value.
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
let vault = TokenVault(accessGroup: "TEAMID.group.com.example.app")
|
|
64
|
+
let session = try vault.loadSession(namespace: "primary")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Configuring `keychainAccessGroup` places the plugin's canonical session,
|
|
68
|
+
transaction state, and widget snapshot in that shared access group. Do not grant
|
|
69
|
+
the entitlement to an extension you do not trust with the complete OIDC session.
|
|
70
|
+
|
|
71
|
+
## Android widget access
|
|
72
|
+
|
|
73
|
+
An Android app widget in the same application package and UID can use the public
|
|
74
|
+
native vault:
|
|
75
|
+
|
|
76
|
+
```java
|
|
77
|
+
TokenVault vault = new TokenVault(context);
|
|
78
|
+
StoredSessionV1 session = vault.loadSession("primary");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Before using `session.accessToken`, compare `session.expiresAt` with the current
|
|
82
|
+
time and leave enough margin for the widget request to complete.
|
package/docs/TESTING.md
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Automated checks
|
|
4
4
|
|
|
5
|
-
Run
|
|
5
|
+
Run TypeScript linting, unit tests, and the package build:
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm run verify
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
match `contracts/native-api.json`.
|
|
13
|
-
|
|
14
|
-
Build the native libraries with:
|
|
11
|
+
Build and test the native libraries with:
|
|
15
12
|
|
|
16
13
|
```sh
|
|
17
14
|
npm run verify:ios
|
|
18
15
|
npm run verify:android
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
CI runs
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
CI currently runs:
|
|
19
|
+
|
|
20
|
+
- TypeScript linting, Vitest, package builds, and `npm pack --dry-run`;
|
|
21
|
+
- iOS XCTest in an iOS 18.5 simulator;
|
|
22
|
+
- Android unit tests, assembly, and lint with SDK 36;
|
|
23
|
+
- cross-platform decoding of the versioned `StoredSessionV1` fixture.
|
|
24
|
+
|
|
25
|
+
## Current manual validation
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Basic Amazon Cognito login has been tested in a packaged application on a
|
|
28
|
+
physical iOS device. The system authentication UI, callback, code exchange, and
|
|
29
|
+
resulting session work in that path.
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
This does not yet cover all iOS cancellation, logout, refresh, rotation, restart,
|
|
32
|
+
ephemeral-session, and error edge cases. Physical Android and the provider matrix
|
|
33
|
+
also remain incomplete.
|
|
34
|
+
|
|
35
|
+
## Required physical-device coverage
|
|
36
|
+
|
|
37
|
+
Before a stable release, verify on physical iOS and Android devices:
|
|
29
38
|
|
|
30
39
|
- system login and logout UI, provider-consent UI, cancellation, and callbacks;
|
|
31
40
|
- Web Crypto availability in the packaged Capacitor WebView;
|
|
32
|
-
- shared and ephemeral
|
|
41
|
+
- shared and ephemeral browser sessions;
|
|
33
42
|
- Keychain and Keystore persistence across app restarts;
|
|
34
|
-
-
|
|
43
|
+
- refresh before expiry and refresh after resuming beyond expiry;
|
|
44
|
+
- reads from iOS and Android widgets.
|
|
35
45
|
|
|
36
46
|
## Provider integration coverage
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
The target matrix contains at least two conforming providers, including a locally
|
|
49
|
+
configurable provider, and covers:
|
|
40
50
|
|
|
41
51
|
- discovery and explicit metadata;
|
|
42
52
|
- login, code exchange, UserInfo, and logout;
|
|
@@ -45,3 +55,6 @@ locally configurable provider:
|
|
|
45
55
|
- invalid state, nonce, callback, and ID-token claims;
|
|
46
56
|
- a provider that omits refresh tokens;
|
|
47
57
|
- a provider that rejects the configured Capacitor origin through CORS.
|
|
58
|
+
|
|
59
|
+
Provider configuration examples that have not been tested on physical devices are
|
|
60
|
+
labelled as guidance in [Provider configuration](PROVIDERS.md).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Login fails before system UI opens
|
|
4
|
+
|
|
5
|
+
- Confirm `crypto.subtle` and `crypto.getRandomValues` exist in the packaged
|
|
6
|
+
Capacitor WebView.
|
|
7
|
+
- Confirm the authorization endpoint uses HTTPS.
|
|
8
|
+
- Confirm discovery succeeds, or provide complete static `metadata`.
|
|
9
|
+
- Inspect the error `code`; `BROWSER_UNAVAILABLE` and `UNSUPPORTED_RUNTIME`
|
|
10
|
+
identify adapter failures.
|
|
11
|
+
|
|
12
|
+
## The provider reports a redirect mismatch
|
|
13
|
+
|
|
14
|
+
The authorization request's `redirect_uri` must exactly match a callback
|
|
15
|
+
registered for the provider client. Check scheme, host, port, path, case, and
|
|
16
|
+
trailing slashes. Wildcard callback hosts are commonly unsupported.
|
|
17
|
+
|
|
18
|
+
## Android does not return to the app
|
|
19
|
+
|
|
20
|
+
- Keep `MainActivity` in `singleTask` launch mode.
|
|
21
|
+
- Put the callback intent filter inside the existing `MainActivity` declaration.
|
|
22
|
+
- Match the `<data android:scheme>` value to `redirect_uri`.
|
|
23
|
+
- If using HTTPS, verify the App Link and Digital Asset Links association.
|
|
24
|
+
|
|
25
|
+
See [Android setup](PLATFORM_SETUP.md#android).
|
|
26
|
+
|
|
27
|
+
## iOS does not show the expected sheet or consent dialog
|
|
28
|
+
|
|
29
|
+
`ASWebAuthenticationSession` controls both presentation and the provider-consent
|
|
30
|
+
dialog. The operating system decides when consent is required and can remember a
|
|
31
|
+
previous decision. Verify that the Capacitor view controller has an active window
|
|
32
|
+
and that the callback scheme is registered in the target.
|
|
33
|
+
|
|
34
|
+
## Token, UserInfo, refresh, or revocation fails with a network error
|
|
35
|
+
|
|
36
|
+
These requests use the WebView's normal `fetch`. Configure the provider endpoint
|
|
37
|
+
to allow the app's Capacitor origin through CORS. Static discovery metadata does
|
|
38
|
+
not remove CORS requirements from token, UserInfo, refresh, or revocation calls.
|
|
39
|
+
|
|
40
|
+
## No refresh token is available
|
|
41
|
+
|
|
42
|
+
- Request the provider's offline-access scope when required.
|
|
43
|
+
- Enable the refresh-token grant for the public client.
|
|
44
|
+
- Check provider-specific consent and rotation settings.
|
|
45
|
+
|
|
46
|
+
When the current user is expired and has no refresh token, `getValidUser()`
|
|
47
|
+
removes the local user and returns `null`.
|
|
48
|
+
|
|
49
|
+
## Manager creation fails while offline
|
|
50
|
+
|
|
51
|
+
The current alpha validates the restored session during
|
|
52
|
+
`CapacitorUserManager.create()`. If the stored access token needs renewal, a
|
|
53
|
+
temporary token-endpoint failure can reject creation. Initialize the manager from
|
|
54
|
+
a place that can surface a retry action. A future implementation should make
|
|
55
|
+
creation succeed offline and report renewal failure through `silentRenewError`.
|
|
56
|
+
|
|
57
|
+
## Logout does not return to the app
|
|
58
|
+
|
|
59
|
+
Register the post-logout URI at the provider and in the native application. Some
|
|
60
|
+
providers use non-standard parameters. Amazon Cognito uses `logout_uri`; see
|
|
61
|
+
[Provider configuration](PROVIDERS.md#amazon-cognito).
|
|
62
|
+
|
|
63
|
+
## `USER_CANCELLED` appears but Android UI remains visible
|
|
64
|
+
|
|
65
|
+
Android does not provide an API for forcibly closing an already-open Auth Tab or
|
|
66
|
+
Custom Tab. `cancel()` rejects the pending JavaScript operation and clears plugin
|
|
67
|
+
state; the user may still need to close the browser UI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-oidc",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "A minimal Capacitor adapter for standards-based OpenID Connect authentication.",
|
|
5
5
|
"main": "dist/plugin.cjs",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"author": {
|
|
22
|
-
"name": "Jojo Pirker"
|
|
23
|
-
"email": "j.pirker@gmail.com"
|
|
22
|
+
"name": "Jojo Pirker"
|
|
24
23
|
},
|
|
25
24
|
"repository": {
|
|
26
25
|
"type": "git",
|