reis-mobile 0.7.3 → 0.9.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +100 -26
- package/agents/android-architect.md +3 -3
- package/agents/android-performance-engineer.md +1 -1
- package/agents/flutter-architect.md +3 -3
- package/agents/flutter-performance-engineer.md +1 -1
- package/agents/ios-architect.md +3 -3
- package/agents/ios-performance-engineer.md +1 -1
- package/agents/kmp-architect.md +3 -3
- package/agents/kmp-performance-engineer.md +1 -1
- package/agents/lead-mobile.md +3 -0
- package/agents/mobile-release-engineer.md +3 -3
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +3 -3
- package/agents/rn-performance-engineer.md +1 -1
- package/bin/reis-mobile.mjs +123 -1
- package/commands/consult.md +95 -0
- package/commands/debate.md +15 -11
- package/commands/reis-mobile.md +7 -2
- package/commands/review.md +2 -0
- package/core/config/project.mjs +1 -1
- package/core/diagnostics/doctor.mjs +23 -1
- package/core/providers/catalog.mjs +168 -0
- package/core/providers/consult.mjs +220 -0
- package/core/providers/providers.mjs +156 -0
- package/core/router/intents.mjs +15 -0
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +3 -0
- package/skills/mobile-accessibility/references/localization.md +77 -0
- package/skills/mobile-observability/SKILL.md +137 -0
- package/skills/mobile-observability/references/android.md +105 -0
- package/skills/mobile-observability/references/flutter.md +86 -0
- package/skills/mobile-observability/references/ios.md +88 -0
- package/skills/mobile-observability/references/kotlin-multiplatform.md +76 -0
- package/skills/mobile-observability/references/react-native.md +62 -0
- package/skills/mobile-offline-sync/SKILL.md +132 -0
- package/skills/mobile-offline-sync/references/android.md +123 -0
- package/skills/mobile-offline-sync/references/flutter.md +103 -0
- package/skills/mobile-offline-sync/references/ios.md +103 -0
- package/skills/mobile-offline-sync/references/kotlin-multiplatform.md +114 -0
- package/skills/mobile-offline-sync/references/react-native.md +108 -0
- package/skills/mobile-performance/SKILL.md +88 -0
- package/skills/mobile-performance/references/android.md +96 -0
- package/skills/mobile-performance/references/background.md +87 -0
- package/skills/mobile-performance/references/flutter.md +101 -0
- package/skills/mobile-performance/references/ios.md +82 -0
- package/skills/mobile-performance/references/kotlin-multiplatform.md +62 -0
- package/skills/mobile-performance/references/react-native.md +85 -0
- package/skills/mobile-release/SKILL.md +2 -1
- package/skills/mobile-release/references/billing.md +92 -0
- package/skills/mobile-security/SKILL.md +3 -0
- package/skills/mobile-security/references/auth.md +92 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# In-app purchases and subscription lifecycle
|
|
2
|
+
|
|
3
|
+
Applies to every stack. Billing bugs are expensive in a way other mobile bugs are not: a user who paid and did not receive the entitlement files a store complaint, a chargeback, and a one-star review, and the store holds *you* responsible for the reconciliation. The design that avoids this is the same on both platforms.
|
|
4
|
+
|
|
5
|
+
## The rule everything follows from
|
|
6
|
+
|
|
7
|
+
**The client never decides entitlement.** The app's job is to start a purchase and to hand the resulting receipt/token to your backend. The backend verifies it with the store's server, records the entitlement, and answers "what does this user own?". Every screen reads that answer.
|
|
8
|
+
|
|
9
|
+
An app that grants a feature because a local purchase callback returned success can be defeated with a patched binary, and — more commonly — simply gets it wrong: the callback is missed because the app was killed mid-purchase, or the subscription was cancelled from the store's settings and the app never hears about it.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
app ──buy──▶ store ──token/receipt──▶ app ──▶ your backend ──verify──▶ store server
|
|
13
|
+
│
|
|
14
|
+
app ◀────── entitlement state ─────────────────────┘
|
|
15
|
+
▲
|
|
16
|
+
store server ──server notifications──┘ (renewals, cancellations, refunds, billing retries)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Server notifications are not optional
|
|
20
|
+
|
|
21
|
+
The purchase callback covers the moment of purchase. Everything afterwards — a renewal at 3am, a cancellation, a refund, a failed payment entering a retry window, an upgrade, a family-sharing change — happens with the app closed and reaches you only through the store's server-to-server notifications:
|
|
22
|
+
|
|
23
|
+
- **Google Play:** Real-time developer notifications (Pub/Sub), plus the Google Play Developer API for the authoritative subscription state.
|
|
24
|
+
- **Apple:** App Store Server Notifications V2, plus the App Store Server API and signed transactions.
|
|
25
|
+
|
|
26
|
+
Without them, a cancelled subscription keeps working until the app next happens to check, and a refunded purchase is never revoked. "We poll on app launch" is not equivalent: the user who cancelled may not open the app for weeks, and a refunded purchase must be revoked immediately.
|
|
27
|
+
|
|
28
|
+
## The states to model
|
|
29
|
+
|
|
30
|
+
A subscription is not a boolean. At minimum, entitlement must distinguish:
|
|
31
|
+
|
|
32
|
+
| State | Behaviour |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Active | Full access |
|
|
35
|
+
| Active, cancelled (will not renew) | Full access until the period ends; a good place to ask why |
|
|
36
|
+
| In a billing retry / grace period | **Keep access** and prompt the user to fix payment — revoking here loses users who would have paid |
|
|
37
|
+
| On hold / suspended | Access revoked, recoverable if payment succeeds |
|
|
38
|
+
| Paused (Android) | Access revoked, scheduled to resume |
|
|
39
|
+
| Expired | No access |
|
|
40
|
+
| Refunded / revoked | Access removed immediately, on the notification |
|
|
41
|
+
| Trial / introductory offer | Active, but the eligibility rules differ and are per-account, not per-device |
|
|
42
|
+
|
|
43
|
+
Treating anything other than "active" as "no access" is the standard way to break grace periods and lose revenue.
|
|
44
|
+
|
|
45
|
+
## Purchase acknowledgement
|
|
46
|
+
|
|
47
|
+
Both stores require the purchase to be consumed or acknowledged, and both **refund automatically** if you do not.
|
|
48
|
+
|
|
49
|
+
- **Google Play:** a purchase not acknowledged (or consumed, for consumables) within three days is automatically refunded and revoked. Acknowledge only **after** your backend has recorded the entitlement, so a failure to record cannot produce a user who paid and owns nothing.
|
|
50
|
+
- **Apple:** a transaction that is not finished is re-delivered on every launch. Finish it only after the entitlement is recorded — the re-delivery is the recovery mechanism, and finishing early discards it.
|
|
51
|
+
|
|
52
|
+
The crash-in-the-middle case is the one to test: purchase succeeds, the app is killed before the backend call. On the next launch, the pending purchase must still be visible, sent, recorded and only then acknowledged.
|
|
53
|
+
|
|
54
|
+
## Restore and multi-device
|
|
55
|
+
|
|
56
|
+
- A **restore** path is mandatory on iOS (App Review rejects apps without one for non-consumables and subscriptions) and expected everywhere. It must be reachable without being logged in to *your* account if the purchase is tied to the store account.
|
|
57
|
+
- Entitlement follows the **store account**, but your product follows **your** user account. Decide explicitly what happens when they disagree: one store account buying for two of your users, or one of your users signing in on a device with a different store account. This is where fraud and support tickets concentrate; the backend must own the mapping, and it must be enforced there.
|
|
58
|
+
- Prices, currencies and availability come from the store per region. Never hard-code a price string; read it from the product query and display the localized formatted price the store returns.
|
|
59
|
+
|
|
60
|
+
## Testing
|
|
61
|
+
|
|
62
|
+
- **Google Play:** licence testers with test payment methods, internal testing track, and shortened subscription periods for renewal testing. Purchases must come from a build uploaded to a track with the same package name and signing key.
|
|
63
|
+
- **Apple:** StoreKit Testing in Xcode (a local `.storekit` configuration file, with time acceleration and error injection — the only practical way to test renewals, billing retry and refunds) and Sandbox accounts for the end-to-end path with real server notifications.
|
|
64
|
+
- Test on the backend, not only in the app: replay a server notification payload and assert the entitlement change. That is the code that actually decides access.
|
|
65
|
+
- Cases worth a test each: purchase interrupted by process death; a refund notification; a cancellation followed by a resubscribe; a user who already owns the product buying again; an expired subscription that renews after a grace period.
|
|
66
|
+
|
|
67
|
+
## Per-stack notes
|
|
68
|
+
|
|
69
|
+
| Stack | Client library |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Native Android | Google Play Billing Library — the deep migration and version material is in [`mobile-android/references/play.md`](../../mobile-android/references/play.md) |
|
|
72
|
+
| Native iOS | StoreKit 2 (`Product`, `Transaction`, `Transaction.updates`, `Transaction.currentEntitlements`) — the `Transaction.updates` listener must be started at launch, or transactions completed outside the app are missed |
|
|
73
|
+
| Flutter | `in_app_purchase` (federated, both stores) or a vendor SDK; the purchase stream must be subscribed before the first purchase and on every launch |
|
|
74
|
+
| React Native / Expo | `react-native-iap`, `expo-in-app-purchases` where available, or a vendor SDK; Expo managed projects need a development build — in-app purchases do not work in Expo Go |
|
|
75
|
+
| Kotlin Multiplatform | Billing is host-specific. Keep the entitlement *model* and the backend calls in `commonMain`, and the store client in each host |
|
|
76
|
+
|
|
77
|
+
A subscription infrastructure vendor (RevenueCat, Adapty and similar) exists precisely because the server-side half above is substantial. Using one is a legitimate decision; it does not remove the need to decide entitlement on a server rather than in the app — it moves that server.
|
|
78
|
+
|
|
79
|
+
## Store policy
|
|
80
|
+
|
|
81
|
+
Both stores require that digital content consumed in the app is sold through their billing system, with a narrow and changing set of exceptions that vary by region and by court ruling. Alternative payment links have specific, enforced requirements. Before designing anything that bypasses store billing, read the current policy for the target markets — this is the most volatile area of store policy and the most expensive one to get wrong, since the penalty is removal.
|
|
82
|
+
|
|
83
|
+
Release-time checks for the store submission itself are in [readiness.md](readiness.md).
|
|
84
|
+
|
|
85
|
+
## Official documentation
|
|
86
|
+
|
|
87
|
+
- Google Play Billing: https://developer.android.com/google/play/billing
|
|
88
|
+
- Real-time developer notifications: https://developer.android.com/google/play/billing/getting-ready#configure-rtdn
|
|
89
|
+
- Google Play Developer API, subscriptions: https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2
|
|
90
|
+
- StoreKit: https://developer.apple.com/documentation/storekit
|
|
91
|
+
- App Store Server Notifications V2: https://developer.apple.com/documentation/appstoreservernotifications
|
|
92
|
+
- Testing in-app purchases with StoreKit Test: https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode
|
|
@@ -18,6 +18,7 @@ Organized by the OWASP MASVS categories. Apply the general checks below, then th
|
|
|
18
18
|
| Native iOS (Swift/Objective-C), or `ios/` in any app | [references/ios.md](references/ios.md) |
|
|
19
19
|
| React Native or Expo | [references/react-native.md](references/react-native.md) |
|
|
20
20
|
| Kotlin Multiplatform shared code and platform implementations | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
21
|
+
| OAuth 2.0 / OIDC login, PKCE, tokens and biometric gating, on any stack | [references/auth.md](references/auth.md) |
|
|
21
22
|
|
|
22
23
|
Firebase-specific protection (security rules, App Check) is covered by `mobile-firebase`.
|
|
23
24
|
|
|
@@ -44,6 +45,8 @@ Every finding needs evidence in the code or configuration at `file:line`. Severi
|
|
|
44
45
|
- [ ] Sessions without expiration, or refresh tokens never invalidated on logout.
|
|
45
46
|
- [ ] Biometrics used only as a local boolean, without unlocking a key in the Keychain or Keystore.
|
|
46
47
|
|
|
48
|
+
For anything involving a login flow — OAuth 2.0, OpenID Connect, PKCE, redirect URIs, token rotation or biometric-gated keys — read [references/auth.md](references/auth.md) before reviewing the stack reference.
|
|
49
|
+
|
|
47
50
|
**MASVS-NETWORK**
|
|
48
51
|
- [ ] `http://` URLs for APIs.
|
|
49
52
|
- [ ] Certificate validation disabled or overridden to accept everything.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# OAuth 2.0, OIDC and PKCE in mobile apps
|
|
2
|
+
|
|
3
|
+
Applies to every stack. A mobile app is a **public client**: it cannot keep a secret, because anything shipped in the binary can be extracted. Every rule here follows from that one fact, and the authoritative source is **RFC 8252, "OAuth 2.0 for Native Apps"**, which the major identity providers implement and enforce.
|
|
4
|
+
|
|
5
|
+
## The three rules that decide whether the flow is correct
|
|
6
|
+
|
|
7
|
+
1. **Authorization Code flow with PKCE. Nothing else.** The Implicit flow and the Resource Owner Password Credentials flow are both disallowed for native apps — implicit returns tokens in a redirect where they leak, and ROPC makes the app handle the password, defeats federated login and blocks MFA. If the code sends a username and password to a `/token` endpoint, that is the finding.
|
|
8
|
+
2. **No client secret in the app.** A "confidential" client registration used by a mobile binary is a published secret. Register the app as a public client; PKCE is what replaces the secret.
|
|
9
|
+
3. **The authorization request runs in the system browser, not in a WebView.** RFC 8252 requires an external user-agent — `ASWebAuthenticationSession` on iOS, Custom Tabs on Android — because it shares the system's session (so SSO works), shows the real URL and certificate to the user (so phishing is detectable), and keeps the app out of the credential entry. An embedded `WebView` on the login page gives the app access to the user's typed password and is rejected by most identity providers.
|
|
10
|
+
|
|
11
|
+
## PKCE, concretely
|
|
12
|
+
|
|
13
|
+
PKCE (RFC 7636) stops an attacker who intercepts the redirect — a malicious app that registered the same custom scheme, or a log that captured the URL — from exchanging the authorization code.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
verifier = 43–128 chars from [A-Z a-z 0-9 - . _ ~], from a cryptographic RNG
|
|
17
|
+
challenge = BASE64URL( SHA256( verifier ) ) ← method "S256"
|
|
18
|
+
|
|
19
|
+
/authorize?...&code_challenge=<challenge>&code_challenge_method=S256
|
|
20
|
+
/token ...&code=<code>&code_verifier=<verifier>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
What to check in review:
|
|
24
|
+
|
|
25
|
+
- [ ] `code_challenge_method` is `S256`. The `plain` method provides no protection and exists only for constrained clients.
|
|
26
|
+
- [ ] The verifier comes from a **cryptographic** random source — `SecureRandom`, `SecRandomCopyBytes`, `Random.secure()`, `crypto.getRandomValues` — never `Math.random()`, a timestamp or a UUID v4 from a non-crypto source.
|
|
27
|
+
- [ ] A fresh verifier per authorization request, held only in memory, discarded after the exchange.
|
|
28
|
+
- [ ] The `state` parameter is present, random, and **verified** on the callback. `state` is CSRF protection and is separate from PKCE; PKCE does not replace it.
|
|
29
|
+
- [ ] With OIDC, the `nonce` is sent and checked against the `nonce` claim in the ID token.
|
|
30
|
+
|
|
31
|
+
## Redirects
|
|
32
|
+
|
|
33
|
+
The redirect URI is how the code gets back to the app, and it is the weak point.
|
|
34
|
+
|
|
35
|
+
- **Prefer verified app links**: `https` App Links on Android (with `assetlinks.json` on the domain) and Universal Links on iOS (with `apple-app-site-association`). These are the only redirect types the OS ties to a domain you own; another app cannot claim them.
|
|
36
|
+
- **Custom schemes are claimable** by any installed app. If one is used, it must be app-specific and unguessable-ish (the reverse-DNS form providers recommend, e.g. `com.example.app:/oauth2redirect`), and PKCE must be present — with PKCE, a stolen code is useless.
|
|
37
|
+
- **Loopback redirects** (`http://127.0.0.1:<port>`) are the desktop pattern and are not appropriate on mobile.
|
|
38
|
+
- Validate the callback: correct `state`, expected issuer, and no code reuse.
|
|
39
|
+
|
|
40
|
+
## Tokens
|
|
41
|
+
|
|
42
|
+
- **Storage:** the Keychain on iOS and Keystore-backed storage on Android — never `UserDefaults`, `SharedPreferences`, `AsyncStorage`, a plain file, `localStorage` in a WebView, or the JavaScript bundle. Per-stack APIs are in the platform references of this skill.
|
|
43
|
+
- **Access tokens are short-lived** (minutes to an hour). A long-lived access token is a finding by itself.
|
|
44
|
+
- **Refresh tokens must rotate.** Each refresh returns a new refresh token and invalidates the old one; a reused old token means theft, and the server must revoke the whole family. Rotation with reuse detection is the mobile standard because a public client cannot prove its identity any other way.
|
|
45
|
+
- **Never put a token in a URL**, a log line, an analytics event, a crash-report attribute or a breadcrumb — see [mobile-observability](../../mobile-observability/SKILL.md).
|
|
46
|
+
- **Validate the ID token** if the client uses its claims: signature against the provider's JWKS, `iss`, `aud`, `exp`, and `nonce`. An ID token is proof of authentication for the client; it is **not** an access token and must never be sent to your API as one.
|
|
47
|
+
- **Authorization is the backend's decision.** Claims decoded on the device decide what to render, never what is allowed. A client that reads `role: admin` from a token and unlocks a feature is a client-side authorization finding regardless of how well the token is stored.
|
|
48
|
+
- **Logout** must revoke the refresh token at the provider (`revocation_endpoint`), clear the token store, and end the browser session where the provider supports RP-initiated logout. Deleting the local copy alone leaves a valid session on the server.
|
|
49
|
+
|
|
50
|
+
## Biometrics do not authenticate to your server
|
|
51
|
+
|
|
52
|
+
A biometric prompt that returns `true` and unlocks a screen proves nothing to the backend and can be bypassed on a compromised device. Biometrics are correct when they **gate access to a key**: the token or a key that decrypts it is stored with biometric-backed access control (`kSecAccessControlBiometryCurrentSet` on iOS, `setUserAuthenticationRequired` on an Android Keystore key), so failing the prompt makes the data undecryptable rather than just hiding a screen.
|
|
53
|
+
|
|
54
|
+
Invalidate on enrolment change: adding a fingerprint or face must invalidate the key, or a coerced enrolment grants access.
|
|
55
|
+
|
|
56
|
+
## Use the platform libraries
|
|
57
|
+
|
|
58
|
+
Hand-written OAuth is where these mistakes accumulate. Prefer a maintained implementation that already does RFC 8252 correctly, and keep the verifier, `state` and browser handling out of your own code:
|
|
59
|
+
|
|
60
|
+
| Stack | Typical choice |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Android | AppAuth for Android, or the identity provider's own SDK; Custom Tabs for the browser |
|
|
63
|
+
| iOS | AppAuth for iOS, or the provider's SDK; `ASWebAuthenticationSession` for the browser |
|
|
64
|
+
| Flutter | `flutter_appauth`, or the provider's Flutter SDK; secure storage for tokens |
|
|
65
|
+
| React Native / Expo | `react-native-app-auth`, or `expo-auth-session` (which implements PKCE); never a `WebView` |
|
|
66
|
+
| Kotlin Multiplatform | The host apps own the browser flow; keep only token *use* and refresh scheduling in shared code |
|
|
67
|
+
|
|
68
|
+
Check what the chosen library defaults to. Some have an option to use an embedded WebView or to disable PKCE for legacy providers — both turn a compliant flow into a non-compliant one silently.
|
|
69
|
+
|
|
70
|
+
## Review checklist
|
|
71
|
+
|
|
72
|
+
- [ ] Authorization Code + PKCE (`S256`); no implicit, no password grant.
|
|
73
|
+
- [ ] No client secret in the app, the repository, or the CI configuration that builds it.
|
|
74
|
+
- [ ] System browser (`ASWebAuthenticationSession` / Custom Tabs), not a `WebView`.
|
|
75
|
+
- [ ] Cryptographic RNG for the verifier and `state`; `state` and `nonce` verified.
|
|
76
|
+
- [ ] Redirect URI is an App Link / Universal Link, or an app-specific scheme with PKCE.
|
|
77
|
+
- [ ] Tokens in Keychain/Keystore-backed storage, short-lived access token, rotating refresh token with reuse detection.
|
|
78
|
+
- [ ] No token in logs, URLs, analytics or crash reports.
|
|
79
|
+
- [ ] ID token validated if used; never sent as an access token.
|
|
80
|
+
- [ ] Authorization enforced by the backend on every request.
|
|
81
|
+
- [ ] Logout revokes server-side.
|
|
82
|
+
- [ ] Biometrics gate a key, not a boolean; enrolment changes invalidate it.
|
|
83
|
+
|
|
84
|
+
## Official documentation
|
|
85
|
+
|
|
86
|
+
- RFC 8252, OAuth 2.0 for Native Apps: https://datatracker.ietf.org/doc/html/rfc8252
|
|
87
|
+
- RFC 7636, Proof Key for Code Exchange: https://datatracker.ietf.org/doc/html/rfc7636
|
|
88
|
+
- OAuth 2.0 Security Best Current Practice: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
|
|
89
|
+
- OpenID Connect Core: https://openid.net/specs/openid-connect-core-1_0.html
|
|
90
|
+
- OWASP MASVS, authentication and session management: https://mas.owasp.org/MASVS/05-MASVS-AUTH/
|
|
91
|
+
- ASWebAuthenticationSession: https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession
|
|
92
|
+
- Android Custom Tabs: https://developer.chrome.com/docs/android/custom-tabs
|