@lunch-money/developer-docs 2.11.2-preview.5 → 2.11.2-preview.7
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/docs/oauth/authorization-code.md +20 -6
- package/docs/oauth/concepts.md +55 -0
- package/docs/oauth/development.md +15 -3
- package/docs/oauth/index.md +3 -1
- package/docs/oauth/native-apps.md +25 -1
- package/docs/oauth/register-client.md +2 -6
- package/docs/oauth/review-and-approval.md +0 -4
- package/docs/oauth/sample-applications.md +66 -15
- package/docs/oauth/scopes.md +1 -1
- package/docs/oauth/security.md +24 -4
- package/docs/oauth/tokens.md +8 -4
- package/docs/oauth/troubleshooting.md +2 -14
- package/package.json +1 -1
- package/v2/images/oauth-authorization-flow.svg +8 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Implement the authorization-code flow
|
|
2
2
|
|
|
3
|
-
After you [register an OAuth client](/oauth/register-client), your application uses its settings—including its client ID, client secret, and registered redirect URI—to implement the authorization flow. As shown in the [OAuth overview](/oauth), your application sends the user to Lunch Money, Lunch Money returns an authorization code to your callback, and your application exchanges that code for tokens.
|
|
3
|
+
After you [register an OAuth client](/oauth/register-client), your application uses its settings—including its client ID, client secret, if configured, and a registered redirect URI—to implement the authorization flow. As shown in the [OAuth overview](/oauth), your application sends the user to Lunch Money, Lunch Money returns an authorization code to your callback, and your application exchanges that code for tokens. Step 4 explains how to identify the Lunch Money user and budgeting account associated with the resulting connection.
|
|
4
4
|
|
|
5
|
-
This guide walks through that process for a **Confidential web client**. Your server handles the callback and token exchange and keeps the client secret and refresh tokens secure. If you are building a standalone mobile or desktop application, read the [native application guidance](/oauth/native-apps) instead.
|
|
5
|
+
This guide walks through that process for a **Confidential web client**. Your server handles the callback and token exchange and keeps the client secret and refresh tokens secure. If you are building a standalone mobile or desktop application, read the [native application guidance](/oauth/native-apps) instead. Expo developers can also follow the [native Expo sample](/oauth/sample-applications#native-expo-sample).
|
|
6
6
|
|
|
7
7
|
## Choose an OAuth library
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ OAuth libraries handle much of the protocol work for you, including generating P
|
|
|
15
15
|
|
|
16
16
|
For Node.js and TypeScript, [`openid-client`](https://github.com/panva/openid-client) is one maintained option. Despite its name, using it does not mean that Lunch Money supports OpenID Connect identity scopes, ID tokens, or UserInfo.
|
|
17
17
|
|
|
18
|
-
The [confidential Node.js sample application](/oauth/sample-applications#understand-
|
|
18
|
+
The [confidential Node.js sample application](/oauth/sample-applications#understand-the-confidential-teaching-code) shows this library in a complete Lunch Money flow. Begin with its [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/README.md), then follow the listed reading order from protocol types and discovery through authorization, callback validation, credential storage, API access, optional refresh, and revocation.
|
|
19
19
|
|
|
20
20
|
## Configure Lunch Money with discovery
|
|
21
21
|
|
|
@@ -42,6 +42,8 @@ On your server, generate:
|
|
|
42
42
|
|
|
43
43
|
Redirect the browser to the discovered authorization endpoint with `response_type=code`, your `client_id`, exact `redirect_uri`, `state`, `code_challenge`, and `code_challenge_method=S256`.
|
|
44
44
|
|
|
45
|
+
Lunch Money handles sign-in, budgeting-account selection, and consent. It may reuse an existing browser login session, and the person may be able to continue with a different Lunch Money user from the authorization screen. A user may have multiple budgeting accounts, but this authorization is for the one they select. Keep the authorization attempt bound to the authenticated application user who started it; do not assume which Lunch Money user will complete it.
|
|
46
|
+
|
|
45
47
|
> [!NOTE] Scopes come from the client registration
|
|
46
48
|
> You do not need to send `scope`. If your OAuth library includes it, Lunch Money ignores the requested value and uses the client's complete registered scope set. An authorization request cannot narrow or expand that set; changing scopes requires a replacement client.
|
|
47
49
|
|
|
@@ -61,9 +63,9 @@ Treat `access_denied` as a normal user decision. Do not log the callback query s
|
|
|
61
63
|
|
|
62
64
|
Send a form-encoded request to the discovered token endpoint with `grant_type=authorization_code`, the code, exact `redirect_uri`, and original `code_verifier`. A confidential client authenticates with HTTP Basic using its client ID and secret. Do this server-side.
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
Keep the returned credentials in temporary server-side state while you identify the authorization. Do not make an unidentified credential set available to the application or commit it to the normal connection store yet. If a `refresh_token` is returned, treat it as a high-value credential even during this temporary step.
|
|
65
67
|
|
|
66
|
-
## 4.
|
|
68
|
+
## 4. Identify and store the connection
|
|
67
69
|
|
|
68
70
|
Send the access token in the header:
|
|
69
71
|
|
|
@@ -75,8 +77,20 @@ Authorization: Bearer YOUR_ACCESS_TOKEN
|
|
|
75
77
|
|
|
76
78
|
The operation must be allowed by the token's scope and by the user's access to the selected budgeting account. See the [scope catalog](/oauth/scopes).
|
|
77
79
|
|
|
80
|
+
Call `GET /v2/me` immediately after the token exchange, before normal credential persistence. It requires `me:read`. Strictly validate the response: `id` identifies the Lunch Money user, `account_id` identifies the selected budgeting account, and `budget_name` is its display name. When reconnecting, use this response to establish the account identity rather than assuming the user selected the same budget. See [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens).
|
|
81
|
+
|
|
82
|
+
Store the credential set under the authenticated application user's ID and the returned Lunch Money user `id` and `account_id`. Those three fields establish the owner and provider identity for the connection, even if your database also assigns an internal connection ID for routing. Atomically create a complete record when that combination is new, or replace the complete credential set when the same application user reauthorizes the same Lunch Money user and budgeting-account pair. Persist the access token, refresh token when present, scope, expiration metadata, Lunch Money user `id`, `account_id`, and `budget_name` together, then make that Lunch Money identity and connection active. Do not use `budget_name` as a key or try to derive identity by parsing the opaque access token.
|
|
83
|
+
|
|
84
|
+
If `GET /v2/me` fails validation or the complete record cannot be persisted, discard the temporary credentials and leave existing connections unchanged. A successful token exchange alone must not create an unidentified or partially saved connection.
|
|
85
|
+
|
|
86
|
+
Your application establishes ownership through its own authenticated session. When authorization starts, store the authenticated application user's internal ID in the server-side authorization attempt associated with `state`. After validating the callback and calling `GET /v2/me`, store the credentials with that application-user ID and the returned Lunch Money `id`/`account_id` pair.
|
|
87
|
+
|
|
88
|
+
For later requests, look up the selected connection using both its internal connection ID and the authenticated application user's internal ID. Then verify that its stored Lunch Money user `id` and `account_id` belong to the active identity before loading or decrypting credentials. Values supplied by the browser select only a candidate record; they do not establish ownership or provider identity.
|
|
89
|
+
|
|
90
|
+
When reauthorization completes, replace credentials only in the record matching the authenticated application user and the returned Lunch Money `id`/`account_id` pair. A different Lunch Money user authorizing the same shared `account_id` must create or update a separate owned connection. After an identity change, hide the previous Lunch Money user's connections and clear or partition displayed profile, configuration, and cached financial data. See [Associate credentials with your application user](/oauth/security#associate-credentials-with-your-application-user) for the storage and ownership rules.
|
|
91
|
+
|
|
78
92
|
## 5. Continue or recover
|
|
79
93
|
|
|
80
|
-
Use the response's `expires_in` value to
|
|
94
|
+
Use the response's `expires_in` value to track the expected lifetime of the access token. You can refresh slightly before expiry as a proactive optimization, or wait for an API request to return `401` and refresh then. In either case, coordinate refreshes for each connection, retry the failed request at most once after a successful refresh, and reauthorize after a terminal refresh failure. Read [token lifecycle and recovery](/oauth/tokens) before implementing refresh, logout, or revocation.
|
|
81
95
|
|
|
82
96
|
Next: [Develop and test your OAuth application](/oauth/development).
|
package/docs/oauth/concepts.md
CHANGED
|
@@ -10,6 +10,61 @@ These terms describe who participates in Lunch Money OAuth and what each credent
|
|
|
10
10
|
- **Authorization server:** the Lunch Money-owned service that presents the Lunch Money sign-in and consent screens, records the user's decision, and issues authorization codes and tokens. It also handles token requests, including exchanging authorization codes and refresh tokens for new tokens. The user signs in directly with Lunch Money; the third-party application never receives or sees their Lunch Money login credentials.
|
|
11
11
|
- **Lunch Money API service:** the Lunch Money-owned V2 API, which receives the application's API requests, accepts access tokens, and checks that each request is permitted by the client's scopes.
|
|
12
12
|
|
|
13
|
+
## Users, budgeting accounts, and access tokens
|
|
14
|
+
|
|
15
|
+
A **Lunch Money user** is the person who signs in to Lunch Money. One user can have access to multiple **budgeting accounts**, such as a personal budget, a household budget, and a test budget. Each budgeting account contains its own financial data. Here, “budgeting account” means that collection of data, not an individual bank or manually managed account within it.
|
|
16
|
+
|
|
17
|
+
During OAuth authorization, the user selects one budgeting account and approves your client's permissions. The resulting **OAuth access token is specific to that Lunch Money user and budgeting account pair**, for your client. It does not grant access to every budgeting account the user can access.
|
|
18
|
+
|
|
19
|
+
For example, Alex can access both “Household” and “API testing.” If Alex selects “API testing” during authorization, your application's access token operates on that budgeting account. It cannot use the same token to read or update “Household.” Scopes determine which operations the application may perform within the selected account; they do not add access to other budgeting accounts.
|
|
20
|
+
|
|
21
|
+
When applications store OAuth credentials, they should associate the authorization with the application user who started the flow and identify the connection by the Lunch Money user and budgeting account pair. Two Lunch Money users can both authorize access to the same shared budgeting account, so `account_id` alone is not a complete connection identity.
|
|
22
|
+
|
|
23
|
+
### Identify the authorized user and account
|
|
24
|
+
|
|
25
|
+
If your client has the `me:read` scope, call `GET /v2/me` with the access token. The response identifies both sides of the connection:
|
|
26
|
+
|
|
27
|
+
| Field | Meaning |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `id` | The Lunch Money user's ID |
|
|
30
|
+
| `account_id` | The selected budgeting account's ID |
|
|
31
|
+
| `budget_name` | The selected budgeting account's display name |
|
|
32
|
+
|
|
33
|
+
Use `account_id` as the stable identifier for the budgeting account and `id` as the identifier for the Lunch Money user who completed authorization. Together they identify the Lunch Money side of the connection. For an application with its own accounts, store that pair within the authenticated application user's ownership boundary. A standalone native application without its own account system uses the `id` and `account_id` pair as its local connection identity.
|
|
34
|
+
|
|
35
|
+
Show `budget_name` so the application user can see which budget they are using, but do not use it as a key: two budgeting accounts can have the same name. When names collide, add a secondary label such as the account ID only where the UI needs to distinguish them. Access tokens are opaque credentials; obtain this information from the API response rather than parsing the token.
|
|
36
|
+
|
|
37
|
+
### Identify the user who completed authorization
|
|
38
|
+
|
|
39
|
+
Lunch Money may reuse an existing browser login session, and the authorization screen may let the person continue with a different Lunch Money user. Shared browser state therefore means that a login can be reused when available; it does not guarantee which user will complete authorization. A fresh or ephemeral browser request is also only an intent, not proof of identity.
|
|
40
|
+
|
|
41
|
+
After every successful code exchange, call and strictly validate `GET /v2/me`. Treat its `id` as the Lunch Money user who actually completed authorization and its `account_id` as the selected budgeting account. Do not infer that identity from browser cookies, callback fields, `budget_name`, the user who was active before authorization, or token contents.
|
|
42
|
+
|
|
43
|
+
### Select a different budgeting account
|
|
44
|
+
|
|
45
|
+
Applications that may retain more than one Lunch Money identity should show the active Lunch Money user and which saved connection is the **active budget**. Provide two distinct actions:
|
|
46
|
+
|
|
47
|
+
- Select another saved connection belonging to the active Lunch Money user to switch the active budget without starting OAuth again.
|
|
48
|
+
- Choose **Authorize another budget** to start OAuth and connect a budgeting account that is not yet saved.
|
|
49
|
+
|
|
50
|
+
To connect to a new, previously unauthorized, budgeting account, send the user through authorization again and have them select it. Changing a locally stored account ID does not change which account an existing token can access. Refreshing a token renews access for its existing authorization; it does not select another budgeting account.
|
|
51
|
+
|
|
52
|
+
Applications do not need a separate client-side “use another user” action. The ordinary **Authorize another budget** action starts authorization; Lunch Money handles any login reuse or user switching. After `/v2/me` identifies the result, make that Lunch Money identity active and show only its connections. Clear or partition the previously displayed profile, configuration, and cached financial data before showing the new identity.
|
|
53
|
+
|
|
54
|
+
Authorizing a second budgeting account for the same Lunch Money user creates or reuses that user's connection and does not revoke the first one. Update each connection's credentials and identity together, and keep cached data from one account separate from data loaded for another.
|
|
55
|
+
|
|
56
|
+
### Reauthorize an existing budgeting account
|
|
57
|
+
|
|
58
|
+
If authorization returns the same Lunch Money user `id` and budgeting-account `account_id` as an existing owned connection, treat it as a reconnection. Atomically replace that connection's stored access-token and refresh-token set with the credentials from the successful exchange. A different Lunch Money user authorizing the same shared `account_id` creates or updates a separate connection; it must not overwrite the first user's credentials.
|
|
59
|
+
|
|
60
|
+
When the user chooses to switch between existing authorized budgets, select a saved connection only from the active Lunch Money identity and use that connection's stored access token to make API calls. All account-specific actions must use the active connection. Clear or partition cached financial data when the active connection changes so data from the previous budget is not shown under the new selection. If the active connection is removed, select a documented remaining connection for the same Lunch Money user or enter a disconnected state. Never fall back to another user's hidden connection.
|
|
61
|
+
|
|
62
|
+
### Retain connections for an inactive Lunch Money user
|
|
63
|
+
|
|
64
|
+
An application may retain credentials for a Lunch Money user who is not currently active when its application-user ownership model permits it. Switching identities should hide the previous user's connections and clear or partition that user's displayed data, but hiding a connection does not revoke it or delete its stored credentials. If that Lunch Money user authorizes again later, the application may make the user's retained connections visible again.
|
|
65
|
+
|
|
66
|
+
An application may instead delete stored credentials as part of its own sign-out or user-switch policy. Make that policy understandable in the interface. In particular, distinguish disconnecting or remotely revoking a budgeting-account authorization from forgetting its local credential without revoking remote access.
|
|
67
|
+
|
|
13
68
|
## Client types and credentials
|
|
14
69
|
|
|
15
70
|
When a developer registers a client, they must choose a client type based on whether the application code can keep a client secret secure. This choice cannot be changed after the client is registered. The developer selects either **Confidential web client** or **Native or public client**.
|
|
@@ -34,23 +34,35 @@ The user's browser—not Lunch Money's server—connects to the loopback listene
|
|
|
34
34
|
|
|
35
35
|
## Repeat or reset authorization
|
|
36
36
|
|
|
37
|
-
During development, the client owner can run authorization again whenever they want to repeat the sign-in, budgeting-account selection, consent, and callback flow. They do not need to revoke the current token first
|
|
37
|
+
During development, the client owner can run authorization again whenever they want to repeat the sign-in, budgeting-account selection, consent, and callback flow. They do not need to revoke the current token first. The authorization server may reuse the current browser login or let the person continue with a different Lunch Money user. Treat the validated `/v2/me` user/account pair—not previous browser state—as the completed authorization's identity.
|
|
38
38
|
|
|
39
39
|
To test how the application responds after access is revoked, revoke the current token and make another API request with it. Lunch Money should reject the request, and the application should discard its saved credentials and begin authorization again. The [token revocation guide](/oauth/tokens#revoke-access) includes copyable `curl` requests for development testing.
|
|
40
40
|
|
|
41
41
|
## Test the complete client
|
|
42
42
|
|
|
43
|
+
Test with one Lunch Money user authorizing multiple budgeting accounts, then with a second Lunch Money user authorizing through the same browser or device. For a client registered with `me:read`, strictly validate the returned `id` and `account_id` after every exchange. Store credentials under the authenticated application user and that Lunch Money user/account pair. For a standalone app without its own accounts, use the Lunch Money user/account pair as the local connection identity.
|
|
44
|
+
|
|
43
45
|
1. Complete authorization. For each attempt, your application should create a random `state` value, save it with the application user's session, and verify that Lunch Money returns the same value to the callback. Rejecting a missing, changed, or reused value prevents a callback started elsewhere from being attached to the wrong session.
|
|
44
46
|
2. Keep the PKCE verifier with that same authorization attempt and use it when exchanging the returned code. Confirm that an exchange with a missing or changed verifier fails; this prevents someone who intercepts the code from using it.
|
|
45
47
|
3. Read the token response's `scope` field and confirm that it contains the complete scope set registered for the client. Do not inspect or parse the access token itself.
|
|
46
48
|
4. Call every scope-dependent feature, including negative permission cases.
|
|
47
|
-
5. Test user denial, missing or changed `state`, callback errors, and interrupted authorization attempts.
|
|
49
|
+
5. Test user denial, missing or changed `state`, callback errors, and interrupted authorization attempts. Confirm that denial or a failed `/v2/me` validation leaves all existing connections unchanged.
|
|
50
|
+
6. Make `GET /v2/me` fail or return an invalid shape after a successful exchange. Confirm that the application saves no unidentified or partial connection.
|
|
51
|
+
7. Reauthorize an already connected Lunch Money user/account pair. Confirm that the application atomically replaces that connection's credentials, makes it active, and does not create a duplicate.
|
|
52
|
+
8. Authorize as a second Lunch Money user through the same browser or device. Confirm that the previously displayed profile, configuration, saved-budget choices, and cached financial data disappear, while any retained credentials remain hidden.
|
|
53
|
+
9. Return to the first Lunch Money user. Confirm that only that user's retained connections become visible again and that the other user's connections cannot be selected.
|
|
54
|
+
10. Have two different Lunch Money users independently authorize the same shared `account_id`. Confirm that the application stores separate credentials keyed by Lunch Money user `id` and `account_id`, without overwriting either connection.
|
|
55
|
+
11. Give two visible budgets the same `budget_name`. Confirm that they remain separate by `account_id` and that the UI disambiguates them only where needed without using the name as a key.
|
|
56
|
+
12. Attempt to select a connection belonging to an inactive Lunch Money user. Confirm that the request is rejected before credentials or cached data are loaded.
|
|
57
|
+
13. Switch the active budget and exercise API calls, refresh, revocation, and local deletion. Confirm that each action and every cache entry affects only the active user/account connection.
|
|
58
|
+
14. Remove the active Lunch Money user's final budget. Confirm that the application enters a disconnected state rather than exposing or selecting another user's hidden connection.
|
|
59
|
+
15. Test remote disconnect and local-only credential deletion separately. Confirm local-only deletion, explain that the remote authorization remains active, and keep unrelated connections unchanged.
|
|
48
60
|
|
|
49
61
|
## Test reauthorization before refresh
|
|
50
62
|
|
|
51
63
|
First make sure your application can recover without a refresh token. Record when the token response arrives and use `expires_in` to determine when the access token is expected to expire. After it expires, make a harmless API request and confirm that Lunch Money returns an authentication failure identifying the token as invalid.
|
|
52
64
|
|
|
53
|
-
Your application should discard the unusable token and start authorization again. Confirm that the same application user can complete the flow, select
|
|
65
|
+
Your application should discard the unusable token for that connection and start authorization again. Confirm that the same application user can complete the flow, select the intended budgeting account, and continue using that connection with the replacement token. This recovery path is required when a client does not have `offline_access`, and it remains the fallback when a refresh token expires, is revoked, or otherwise stops working. Keep other connections usable while this one recovers.
|
|
54
66
|
|
|
55
67
|
After reauthorization works, clients registered with `offline_access` can add and test refresh-token handling. Verify replacement-token storage, concurrent-refresh protection, revocation, and terminal refresh recovery before relying on unattended operation. See [token lifecycle and recovery](/oauth/tokens).
|
|
56
68
|
|
package/docs/oauth/index.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
OAuth lets your application access Lunch Money on behalf of a user without asking that person to copy and share a personal access token. **If you are building an application for anyone other than yourself, use OAuth.** It gives each user a familiar Lunch Money sign-in and consent experience while keeping their personal access token out of your application.
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
## Choose OAuth or a personal access token
|
|
8
8
|
|
|
@@ -21,6 +21,8 @@ Personal access tokens remain useful for quick work on your own account. OAuth i
|
|
|
21
21
|
|
|
22
22
|
An OAuth client is an application's registration with Lunch Money. When a developer registers the client, they select the scopes—the specific permissions—the application needs. Later, when a Lunch Money user authorizes the application, Lunch Money shows them that complete permission set before they approve access. Choose only the permissions the application's features require. A focused permission request is easier for users to understand and trust, and reduces the risk of the application reading or changing data it does not need.
|
|
23
23
|
|
|
24
|
+
One Lunch Money user can have multiple budgeting accounts and multiple connections to the same OAuth client. Each OAuth access token represents that user and the single budgeting account they selected during authorization. It does not provide access to all of their budgets. Read [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens) for an example and how to identify the connected account.
|
|
25
|
+
|
|
24
26
|
## Before you start
|
|
25
27
|
|
|
26
28
|
You need an active Lunch Money account to [register and manage an OAuth client](/oauth/applications) for your application. During development, only the client owner can authorize the application. Approval is required before other Lunch Money users can authorize it.
|
|
@@ -21,6 +21,30 @@ Native clients may use an HTTP loopback redirect with `localhost`, `127.0.0.1`,
|
|
|
21
21
|
|
|
22
22
|
Store tokens in Keychain on Apple platforms. On Android, use current Keystore-backed storage guidance from your maintained library; do not start new work with deprecated `EncryptedSharedPreferences` APIs. Coordinate refresh-token use, persist replacements, and fall back to interactive authorization after terminal failure.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
After every code exchange, call and strictly validate `GET /v2/me` before adding the credentials to the app's normal connection store. For an app without its own user accounts, identify each local connection by the returned Lunch Money user `id` and budgeting-account `account_id` pair. Atomically create a record for a new pair or replace the credential set for an existing pair. Two Lunch Money users may authorize the same shared `account_id`; their credentials must remain in separate records. If identification or persistence fails, discard the temporary credentials instead of saving a partial or unidentified connection. Retain `budget_name` for display, but never use it as a key because different budgets can share a name.
|
|
25
|
+
|
|
26
|
+
The platform authentication browser may reuse an existing Lunch Money login, and Lunch Money may let the person use a different user from the authorization screen. An ephemeral-session request is only an intent to avoid shared browser state; it does not prove which user completed authorization. Do not infer identity from browser cookies, callback fields, budget names, or token contents. The validated `/v2/me` response is authoritative.
|
|
27
|
+
|
|
28
|
+
Show the active Lunch Money identity when the app may retain more than one, show which authorized budget is active, and provide one separate **Authorize another budget** action that starts OAuth. Do not require another client-side action for switching Lunch Money users. Selecting an existing connection must not start authorization again, and the app must allow selection only among saved budgets for the active Lunch Money user.
|
|
29
|
+
|
|
30
|
+
All API calls, refreshes, revocations, local deletions, and cached data must apply to the active user/account connection. Clear or partition profile, configuration, and cached financial data when the identity or budget changes. When the active connection is removed, choose a deterministic remaining connection for that Lunch Money user or enter a disconnected state; never expose another user's hidden connection as a fallback. Use a secondary label such as the account ID only when duplicate visible budget names need disambiguation.
|
|
31
|
+
|
|
32
|
+
The app may retain credentials for inactive Lunch Money users in secure storage. Switching users hides the previous identity's connections without revoking or deleting them, and authorizing as that identity later may restore its saved budgets. Alternatively, an app may delete credentials during its own sign-out or user-switch lifecycle. Make the chosen behavior understandable without prescribing a particular native control.
|
|
33
|
+
|
|
34
|
+
Present remote disconnect or revocation separately from forgetting a credential only on the device. Confirm local-only deletion and explain that it leaves the remote authorization active.
|
|
35
|
+
|
|
36
|
+
### Keep application users' connections separate
|
|
37
|
+
|
|
38
|
+
A native app can support multiple application users on the same device. If your app has its own sign-in system, keep each user's saved Lunch Money connections in a separate storage namespace. Bind the authorization attempt to the application user who started it, and retain the Lunch Money `id` and `account_id` returned by `GET /v2/me` with each credential set. Only load a connection after verifying that it belongs to the currently authenticated application user. Secure device storage does not perform that application-level ownership check for you.
|
|
39
|
+
|
|
40
|
+
On application sign-out or application-user switching, stop work using the previous application user's credentials and clear in-memory tokens, the active connection selection, and cached financial data. Remove saved credentials if your app does not retain connections across sign-ins; otherwise, make them available only after their application owner signs in again. Do not attach an OAuth callback or refresh result from the previous session to the newly signed-in application user.
|
|
41
|
+
|
|
42
|
+
If your app has no separate user accounts, the device's OS account and app storage protections provide the local access boundary. Anyone allowed to use the unlocked app may be able to use its saved connections; separate budgeting-account records alone do not authenticate different people.
|
|
43
|
+
|
|
44
|
+
Sending tokens to an application backend introduces server-side credential storage. If your architecture does that, apply the [credential ownership rules](/oauth/security#associate-credentials-with-your-application-user) on the backend as well as protecting credentials on the device.
|
|
45
|
+
|
|
46
|
+
Platform code should delegate protocol validation to the maintained library while your application supplies safe configuration, lifecycle storage, API calls, revocation, and user-facing recovery.
|
|
47
|
+
|
|
48
|
+
Lunch Money does not currently provide complete Swift or Kotlin samples; the upstream AppAuth projects provide maintained platform examples. Expo developers can use the [native Expo sample](/oauth/sample-applications#native-expo-sample) for a complete Lunch Money walkthrough and reference implementation.
|
|
25
49
|
|
|
26
50
|
Next: [Review OAuth security guidance](/oauth/security).
|
|
@@ -11,7 +11,7 @@ Before you add OAuth to your application code, register an OAuth client with Lun
|
|
|
11
11
|
|
|
12
12
|
List the Lunch Money features your application will provide, then use the [scope catalog](/oauth/scopes) to choose the smallest complete set of permissions those features require. Select `offline_access` only if the application needs to continue making API requests after the initial access token expires and while the user is away.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
If your application may support more than one Lunch Money user or budgeting account, always include `me:read`. After authorization, `GET /v2/me` lets your application record both the Lunch Money user's `id` and the selected budgeting account's `account_id`, then store the connection under the complete application-owner and Lunch Money user/account identity. The same scope is also required when your application only needs to identify one connected budgeting account.
|
|
15
15
|
|
|
16
16
|
Scopes cannot be changed after registration. While you are developing your application, creating a replacement client is inexpensive, so let the scope set evolve as you learn what the application needs. Aim to have the application's core functionality—and its required permissions—settled before you submit the client for review.
|
|
17
17
|
|
|
@@ -34,8 +34,6 @@ Web applications normally use an HTTPS callback. If you need a callback for loca
|
|
|
34
34
|
|
|
35
35
|
After the client is approved, you can add or replace redirect URIs in the Developer Portal without submitting another initial review. An approved confidential web client must keep a production HTTPS callback. See [after approval](/oauth/review-and-approval#after-approval).
|
|
36
36
|
|
|
37
|
-
Each client can have up to 10 redirect URIs, and each URI can be up to 2,048 characters. If you reach the limit, remove a callback the application no longer uses before adding another.
|
|
38
|
-
|
|
39
37
|
## Register the OAuth client
|
|
40
38
|
|
|
41
39
|
Open the [new OAuth client form](/oauth/applications/new) in the Developer Portal. Then:
|
|
@@ -47,13 +45,11 @@ Open the [new OAuth client form](/oauth/applications/new) in the Developer Porta
|
|
|
47
45
|
5. Select the planned scopes.
|
|
48
46
|
6. Accept the current Lunch Money API Terms of Use and select **Register OAuth client**.
|
|
49
47
|
|
|
50
|
-
You can own up to 25 non-deleted OAuth clients. If you reach the limit, delete a client you no longer use before registering another.
|
|
51
|
-
|
|
52
48
|
After registration, record the client ID in your application configuration. A client ID identifies the registration but is not a secret.
|
|
53
49
|
|
|
54
50
|
If you registered a **Confidential web client**, create a client secret from its details page. Lunch Money shows the secret value only once. Copy it immediately into a secret manager or protected server configuration; it cannot be retrieved later. Never commit it or expose it in browser or mobile code.
|
|
55
51
|
|
|
56
|
-
A confidential web client can have
|
|
52
|
+
A confidential web client can have multiple active secrets. This allows you to introduce a replacement secret, update and verify the deployed application, and then revoke the old secret without interrupting authorization. See [OAuth security guidance](/oauth/security#operate-credentials-safely) when planning ongoing rotation.
|
|
57
53
|
|
|
58
54
|
You now have the client settings needed to connect your application to Lunch Money.
|
|
59
55
|
|
|
@@ -52,10 +52,6 @@ Native clients may be reviewed with loopback or private-use scheme redirects.
|
|
|
52
52
|
|
|
53
53
|
Submitting the request moves the client to `pending_review` and temporarily freezes review-critical fields. Reviews are usually completed within a couple of business days. You can continue developing and testing the application as the client owner while you wait. If you need to change a frozen field, cancel the request to return the client to `development`, make the change, and submit it again.
|
|
54
54
|
|
|
55
|
-
You can make up to five initial review submissions across all your clients in a rolling 24-hour period. Cancelled submissions and submissions made again after a rejection still count. Deleting a client does not remove its submissions from the window. Reviews opened automatically after you update the identity of an active client do not count toward this limit.
|
|
56
|
-
|
|
57
|
-
If you reach the limit, the Developer Portal tells you when you can submit again. Wait until that time before retrying; cancelling, deleting, or replacing a client does not reset the window.
|
|
58
|
-
|
|
59
55
|
Lunch Money emails you when the review is complete. You can also check the current status and review history on the client's page in the Developer Portal.
|
|
60
56
|
|
|
61
57
|
## If review is denied
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# OAuth sample applications
|
|
2
2
|
|
|
3
|
-
Use a sample application to see Lunch Money OAuth working end to end before you adapt the flow to your own architecture.
|
|
3
|
+
Use a sample application to see Lunch Money OAuth working end to end before you adapt the flow to your own architecture. Choose the confidential sample when a trusted server will hold credentials, or the native sample when an installed iOS or Android application will hold them. The two architectures have different security boundaries and are documented separately.
|
|
4
4
|
|
|
5
|
-
> [!TIP] Open-source
|
|
6
|
-
> [
|
|
5
|
+
> [!TIP] Open-source samples
|
|
6
|
+
> - [Confidential Node.js and TypeScript sample](https://github.com/lunch-money/lm-oauth-confidential-node-example) and its [README](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/README.md)
|
|
7
|
+
> - [Native Expo sample for iOS and Android](https://github.com/lunch-money/lm-oauth-native-expo-example) and its [README](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/README.md)
|
|
7
8
|
|
|
8
9
|
## Confidential Node.js and TypeScript sample
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
> [!NOTE] This walkthrough is for confidential applications
|
|
12
|
+
> The walkthrough and teaching-code guide below apply to the confidential Node.js sample, where a trusted server holds the client secret and credentials. For an installed iOS or Android application that holds its own credentials without a client secret, skip to the [Native Expo sample](#native-expo-sample).
|
|
13
|
+
|
|
14
|
+
[`lm-oauth-confidential-node-example`](https://github.com/lunch-money/lm-oauth-confidential-node-example) is a teaching sample for a **confidential, server-side web client**. It uses Node.js, TypeScript, and the maintained [`openid-client`](https://github.com/panva/openid-client) library to demonstrate:
|
|
11
15
|
|
|
12
16
|
- configuring a registered Lunch Money OAuth client and discovering authorization-server metadata;
|
|
13
17
|
- starting authorization with single-use `state` and S256 Proof Key for Code Exchange (PKCE);
|
|
@@ -21,7 +25,7 @@ Use a sample application to see Lunch Money OAuth working end to end before you
|
|
|
21
25
|
> The sample uses a fixed demonstration user and in-memory stores. It does not provide real application authentication, durable multi-user isolation, production credential storage, deployment configuration, or operational controls. Review its [security guidance](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/SECURITY.md) and [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/PRODUCTION_CHECKLIST.md) before adapting any part of it.
|
|
22
26
|
|
|
23
27
|
<!-- BEGIN GENERATED OAUTH SAMPLE WALKTHROUGH -->
|
|
24
|
-
<!-- Source: lunch-money/lm-oauth-confidential-node-example@
|
|
28
|
+
<!-- Source: lunch-money/lm-oauth-confidential-node-example@018125fb2a5869a63c7d16d760af79cef3843429:docs/WALKTHROUGH.md -->
|
|
25
29
|
## Try OAuth with your Lunch Money account
|
|
26
30
|
|
|
27
31
|
Use this walkthrough to authorize a real Lunch Money OAuth client, call the Lunch Money API, optionally refresh its credentials, revoke access, and repeat the flow. The sample is a confidential, server-side web client built with Node.js and TypeScript.
|
|
@@ -100,6 +104,8 @@ The Developer Portal replaces `{{LUNCH_MONEY_API_BASE_URL}}` with the API base U
|
|
|
100
104
|
|
|
101
105
|
`SESSION_SECRET` is also optional for this local sample. If it is absent, the process creates a new random cookie-signing secret when it starts. Because all browser sessions, authorization attempts, and credentials are held in memory and cleared on restart, the generated secret can be cleared at the same time. A production application must instead provide a strong `SESSION_SECRET`, keep it stable across restarts, and not rotate it for each OAuth authorization.
|
|
102
106
|
|
|
107
|
+
Alternatively, copy [`.env.example`](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/.env.example) to `.env` as described in [Load settings from a `.env` file](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/README.md#load-settings-from-a-env-file).
|
|
108
|
+
|
|
103
109
|
> [!WARNING]
|
|
104
110
|
> **Keep credentials private**
|
|
105
111
|
> Keep the client secret in a private local environment or secret manager. Never paste it into documentation, AI chats or prompts, committed files, screenshots, browser code, logs, support requests, or commands retained in shared shell history.
|
|
@@ -119,10 +125,24 @@ If a form reports **Invalid CSRF token**, reload the page and try again. This co
|
|
|
119
125
|
1. Choose **Connect Lunch Money**.
|
|
120
126
|
2. Sign into Lunch Money as the user who created the development client.
|
|
121
127
|
3. Select one of that user's budgeting accounts and approve access.
|
|
122
|
-
4. After the browser returns to `http://localhost:4002/oauth/callback`, choose **Call /v2/me**.
|
|
128
|
+
4. After the browser returns to `http://localhost:4002/oauth/callback`, expand **How this authorization was processed** to follow the callback, server-side code exchange, `/v2/me` identity check, and resulting active-user decision. Then choose **Call /v2/me**.
|
|
123
129
|
|
|
124
130
|
The sample calls `GET /v2/me` from its Node.js server using the server-held access token. It validates the response against the documented `userObject` schema and displays the returned profile fields. The access token is never sent to the browser.
|
|
125
131
|
|
|
132
|
+
The processing panel links each teaching step to its implementation. It deliberately does not reveal credential values. To see the normalized credential response while developing locally, set a debugger breakpoint immediately after `exchangeCallback()` returns in `completeAuthorization`, then inspect `tokenSet`. Do not print tokens in browser output or logs.
|
|
133
|
+
|
|
134
|
+
If you want to verify the access token outside the sample, copy it from the local debugger and read it silently into a temporary shell variable. This keeps the credential itself out of shell history:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
read -s LM_ACCESS_TOKEN
|
|
138
|
+
curl --fail-with-body \
|
|
139
|
+
--header "Authorization: Bearer ${LM_ACCESS_TOKEN}" \
|
|
140
|
+
"${LUNCH_MONEY_API_BASE_URL}/v2/me"
|
|
141
|
+
unset LM_ACCESS_TOKEN
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Press <kbd>Enter</kbd> after pasting the token at the hidden prompt. Treat it as a live secret: use a private local terminal, do not paste it directly into a command, and clear the variable when finished. This debugger workflow is for understanding and testing the sample. A production application should inspect encrypted credential records only through privileged operational tooling that redacts secret values.
|
|
145
|
+
|
|
126
146
|
Open Lunch Money's [Connected Apps](https://my.lunchmoney.app/connected-apps) page in another tab, keeping the sample available so you can return to it. The client you just authorized should appear with the details you registered. If you plan to exercise refresh, return to the sample without revoking access yet; revocation ends this authorization's continuing access.
|
|
127
147
|
|
|
128
148
|
### 6. Optionally refresh access
|
|
@@ -135,9 +155,9 @@ If you registered only `me:read`, Lunch Money does not issue a refresh token and
|
|
|
135
155
|
|
|
136
156
|
The sample allows an immediate refresh so you can observe the flow. A production application normally decides when to refresh using expiration information from the token response or after an API authentication failure.
|
|
137
157
|
|
|
138
|
-
### 7.
|
|
158
|
+
### 7. Disconnect the active budget
|
|
139
159
|
|
|
140
|
-
Choose **
|
|
160
|
+
Choose **Disconnect active budget**.
|
|
141
161
|
|
|
142
162
|
- If the connection has a refresh token, the sample revokes it to revoke the entire grant.
|
|
143
163
|
- If the connection has only an access token, the sample revokes that access token.
|
|
@@ -155,13 +175,13 @@ To see what happens when a Lunch Money user disconnects the client:
|
|
|
155
175
|
3. Revoke the client's access there.
|
|
156
176
|
4. Return to the sample and choose **Call /v2/me**.
|
|
157
177
|
|
|
158
|
-
The request should fail because Lunch Money no longer accepts the stored access token. Choose **
|
|
178
|
+
The request should fail because Lunch Money no longer accepts the stored access token. Choose **Forget local credential only** to remove the now-unusable local credential before starting again.
|
|
159
179
|
|
|
160
180
|
### Local reset is not revocation
|
|
161
181
|
|
|
162
|
-
**
|
|
182
|
+
**Forget local credential only** clears the active budget's locally stored credential after confirmation. It does not contact Lunch Money and does not revoke active access.
|
|
163
183
|
|
|
164
|
-
When access is still active, revoke it first through **
|
|
184
|
+
When access is still active, revoke it first through **Disconnect active budget** or Lunch Money's Connected Apps page. Forget the local credential by itself only when the remote authorization has already been revoked or you deliberately want to clear this disposable local demonstration.
|
|
165
185
|
|
|
166
186
|
### Why `localhost` works
|
|
167
187
|
|
|
@@ -175,13 +195,15 @@ When access is still active, revoke it first through **Revoke and verify** or Lu
|
|
|
175
195
|
|
|
176
196
|
After the flow succeeds, connect each action you performed to the code that implemented it, then decide how those responsibilities fit into your own application.
|
|
177
197
|
|
|
178
|
-
Before adapting the code, read the repository's [OAuth code guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/
|
|
198
|
+
Before adapting the code, read the repository's [OAuth code guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/src/oauth/README.md), [security model](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/SECURITY.md), [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/PRODUCTION_CHECKLIST.md), and [troubleshooting guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/018125fb2a5869a63c7d16d760af79cef3843429/TROUBLESHOOTING.md).
|
|
179
199
|
<!-- END GENERATED OAUTH SAMPLE WALKTHROUGH -->
|
|
180
200
|
|
|
181
|
-
## Understand
|
|
201
|
+
## Understand the confidential teaching code
|
|
182
202
|
|
|
183
203
|
The open-source sample you just ran is designed to make the Lunch Money integration code easy to find, understand, and adapt. The modules under `src/oauth` show the OAuth responsibilities that a confidential server-side application needs: discovery, authorization attempts, callback validation, credential ownership and storage, API access, optional refresh, revocation, and safe error handling.
|
|
184
204
|
|
|
205
|
+
The sample represents an authenticated application user with a fixed local demo identity. After authorization, it uses the Lunch Money user `id` returned by `GET /v2/me` to choose which saved budgets are visible; credentials retained for another Lunch Money user remain hidden. A production confidential application replaces the fixed demo identity with the application user from its authenticated server-side session and identifies each owned connection by the returned Lunch Money user `id` and budgeting-account `account_id` pair.
|
|
206
|
+
|
|
185
207
|
Treat these modules as a working reference, not as a package to copy unchanged into production. The in-memory implementation demonstrates serialized refresh and complete credential replacement for one `applicationUserId` and `connectionId`, but it does not provide durable encrypted storage, database transactions, distributed per-connection locking, failed-persistence recovery across restarts, multi-instance coordination, or tenant isolation. The sample keeps those application-specific decisions outside the teaching code so you can see exactly where your implementation must provide them.
|
|
186
208
|
|
|
187
209
|
When refresh succeeds, the sample stores the replacement access token, refresh token, scope, and expiration metadata together without displaying token values. Your refresh-capable application must preserve that boundary, serialize refreshes for each application user and connection, and atomically store the complete rotated credential set. Retry transient failures according to your application's policy, but treat `invalid_grant` as terminal: stop refreshing, discard unusable credentials, and require reauthorization. Disconnect must invalidate the refresh token as well as the current access token so continuing access cannot resume.
|
|
@@ -192,8 +214,37 @@ Start with the [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confi
|
|
|
192
214
|
|
|
193
215
|
The OAuth teaching code is isolated under `src/oauth`. Hono, sessions, cookies, pages, and other runnable presentation code live under `src/scaffolding`, so you can replace the framework without obscuring the protocol flow. The sample's [architecture guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/ARCHITECTURE.md) explains this boundary, and its [OAuth tests](https://github.com/lunch-money/lm-oauth-confidential-node-example/tree/main/tests/oauth) mirror the recommended reading order, including focused [refresh tests](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/tests/oauth/refresh.test.ts). If your result differs, compare the stage that failed with the sample's [troubleshooting guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/TROUBLESHOOTING.md) and the public [OAuth troubleshooting guide](/oauth/troubleshooting).
|
|
194
216
|
|
|
195
|
-
## Native
|
|
217
|
+
## Native Expo sample
|
|
218
|
+
|
|
219
|
+
Choose [`lm-oauth-native-expo-example`](https://github.com/lunch-money/lm-oauth-native-expo-example) for an iOS or Android application installed
|
|
220
|
+
on the user's device when there is no trusted backend that can hold a client
|
|
221
|
+
secret. It uses Expo, the platform authentication browser, authorization code
|
|
222
|
+
with S256 PKCE, and platform secure storage as a native/public client. Its
|
|
223
|
+
optional `offline_access` path demonstrates rotated refresh credentials and
|
|
224
|
+
terminal reauthorization without introducing a backend or client secret.
|
|
225
|
+
|
|
226
|
+
Do not choose it merely because your interface is responsive. Choose it because
|
|
227
|
+
the installed application performs OAuth and owns its credential. If a trusted
|
|
228
|
+
server performs the code exchange and stores credentials, use the confidential
|
|
229
|
+
Node.js sample above instead.
|
|
230
|
+
|
|
231
|
+
Unlike the confidential sample, the native sample has no separate application
|
|
232
|
+
account or server-side application-user boundary. It identifies local
|
|
233
|
+
connections directly by the Lunch Money user `id` and budgeting-account
|
|
234
|
+
`account_id` returned by `GET /v2/me`. It can retain connections for more than
|
|
235
|
+
one Lunch Money user on the device, but only the active user's budgets are
|
|
236
|
+
visible or selectable. Applications that add their own sign-in system must put
|
|
237
|
+
that pair inside the authenticated application user's storage and ownership
|
|
238
|
+
boundary.
|
|
239
|
+
|
|
240
|
+
The native repository's [walkthrough](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/docs/WALKTHROUGH.md) is the canonical hands-on guide;
|
|
241
|
+
complete the registration, development-build, authorization, `/v2/me`,
|
|
242
|
+
optional refresh, revocation, and local-reset walkthrough there.
|
|
243
|
+
|
|
244
|
+
### Follow the native flow through the source
|
|
245
|
+
|
|
246
|
+
Start with the native sample's [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/src/oauth/README.md). It provides the maintained reading order from public configuration and Expo's system-browser adapter through callback validation, secure credential storage, optional refresh, revocation, and the workflow connected to the sample screen.
|
|
196
247
|
|
|
197
|
-
|
|
248
|
+
The OAuth teaching code is isolated under `src/oauth`; React Native UI and platform storage adapters live under `src/scaffolding`. Use the sample's [OAuth tests](https://github.com/lunch-money/lm-oauth-native-expo-example/tree/main/tests) to follow the expected success and failure behavior, then review its [security guidance](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/SECURITY.md) and [production checklist](https://github.com/lunch-money/lm-oauth-native-expo-example/blob/main/PRODUCTION_CHECKLIST.md) before adapting it.
|
|
198
249
|
|
|
199
250
|
Next: [Develop and test your OAuth application](/oauth/development) and [operate it securely](/oauth/security).
|
package/docs/oauth/scopes.md
CHANGED
|
@@ -7,7 +7,7 @@ Choose the smallest complete set of permissions your application needs. This pag
|
|
|
7
7
|
|
|
8
8
|
## Scope groups
|
|
9
9
|
|
|
10
|
-
Groups are selection shortcuts, not extra permissions. A group expands to the exact scopes listed below.
|
|
10
|
+
Groups are selection shortcuts, not extra permissions. Every predefined group includes `me:read` so an application can identify the authorized Lunch Money user and budgeting account. A group expands to the exact scopes listed below.
|
|
11
11
|
|
|
12
12
|
### Read only
|
|
13
13
|
|
package/docs/oauth/security.md
CHANGED
|
@@ -26,11 +26,31 @@ If your browser code uses access tokens, keep them available for no longer than
|
|
|
26
26
|
|
|
27
27
|
A confidential application must authenticate its own user before starting Lunch Money OAuth. Bind each authorization attempt to that authenticated user's server-side session, then recover the owner from the consumed server-side attempt after the callback is verified. Do not let the callback choose who owns the resulting credentials.
|
|
28
28
|
|
|
29
|
-
Store
|
|
29
|
+
Store each budgeting-account connection under a stable internal application-user ID. Within that owner, use both the Lunch Money user `id` and budgeting-account `account_id` returned by `GET /v2/me` as the provider identity. Your database may assign a separate internal connection ID for routing and references; it does not replace the `(application user ID, Lunch Money user id, account_id)` fields used to create or update a provider connection. Do not use an email address, callback parameter, form field, browser cookie, or other browser-supplied value to establish the credential owner or provider identity.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
An application user may own connections for more than one Lunch Money user, although applications do not have to expose that capability. If your application supports only one Lunch Money identity per application user, detect a different `id` after `GET /v2/me` and handle it explicitly; never overwrite the existing identity's credentials.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
```text
|
|
34
|
+
Authenticated application user
|
|
35
|
+
-> owned connection (Lunch Money user id + account_id)
|
|
36
|
+
-> encrypted credential set
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When the user returns to your application, authenticate them through your application's own sign-in system before listing or using their saved connections. Knowing a Lunch Money `id`, `account_id`, or internal connection ID does not prove ownership. Limit connection lookups to the authenticated application user's records, and reject requests for a connection they do not own before loading its credentials.
|
|
40
|
+
|
|
41
|
+
For example, if Alex and Sam both use your application, Sam must not be able to use Alex's saved credentials by submitting Alex's connection ID. This remains true even if they both have access to the same Lunch Money budgeting account. Each application user must authorize their own connection. If Alex has authorized two budgeting accounts, both connections belong to Alex's application user and may be listed and selected within Alex's authenticated session. Each connection still uses the credential set for its own Lunch Money `id`/`account_id` pair.
|
|
42
|
+
|
|
43
|
+
Enforce this ownership and identity check for every API request, refresh, reauthorization, revocation, and deletion involving a saved connection. Background jobs must carry the connection's stored owner and identity so they use the same checks without an interactive session. Updating credentials after reauthorization must remain within the original application user's connections and match both the returned Lunch Money `id` and `account_id`. A different Lunch Money user authorizing the same shared account must not silently overwrite an existing connection.
|
|
44
|
+
|
|
45
|
+
Durable credentials must be encrypted at rest, and backend access should be limited to the services and operators that need it. Encryption protects stored secrets; your application must still enforce ownership when it reads or uses them. Never return refresh tokens or client secrets to browser JavaScript. List connections using non-secret metadata such as `budget_name`, and keep their credentials in the protected store.
|
|
46
|
+
|
|
47
|
+
Apply API calls, refresh, revocation, local deletion, and caches only to the active Lunch Money user and budgeting-account connection. Disconnecting a budgeting account should revoke continuing access for that connection and remove its locally stored credentials while keeping the active user's other connections intact. If the active user's final connection is removed, enter a disconnected state rather than falling back to another Lunch Money user's hidden connection.
|
|
48
|
+
|
|
49
|
+
On application sign-out or Lunch Money user switching, clear session-specific selections and displayed or cached data so the newly active identity cannot see the previous identity's information. An application may retain the previous Lunch Money user's encrypted credentials when its ownership model permits it, but those connections must remain hidden and unavailable until that identity becomes active again. It may instead delete credentials during its own sign-out or switching lifecycle; that is an application policy.
|
|
50
|
+
|
|
51
|
+
Keep remote revocation and local credential deletion separate. A local-only “forget” action does not end the remote authorization, so confirm that action and explain that access remains active at Lunch Money. Signing out of your application, hiding a retained connection, forgetting a local credential, and disconnecting Lunch Money are distinct lifecycle events.
|
|
52
|
+
|
|
53
|
+
The [confidential Node.js sample](/oauth/sample-applications#understand-the-confidential-teaching-code) makes this boundary explicit in its [`CredentialStore`](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/tokens.ts) abstraction. Its [security guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/SECURITY.md) explains the threat model, and its [production checklist](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/PRODUCTION_CHECKLIST.md) lists the storage, tenant-isolation, lifecycle, and operational work the teaching sample deliberately omits. Those boundaries matter, but their implementation and database schema are specific to your application.
|
|
34
54
|
|
|
35
55
|
## Secure authorization and callbacks
|
|
36
56
|
|
|
@@ -47,7 +67,7 @@ Choose only the resource scopes needed for expected features. Add `offline_acces
|
|
|
47
67
|
|
|
48
68
|
## Operate credentials safely
|
|
49
69
|
|
|
50
|
-
Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed.
|
|
70
|
+
Rotate client secrets on a schedule appropriate for your application's risk and immediately when one may have been exposed. Use overlapping active secrets during a controlled rotation: create the replacement, deploy and verify it, and then revoke the old secret. Coordinate refreshes so two workers do not replay the same rotating token, and atomically persist every complete replacement credential set. Production storage needs database transactions, distributed per-connection locking, recovery when persistence fails, multi-instance coordination, and tenant isolation. Make disconnect invalidate continuing refresh access as well as clear local state.
|
|
51
71
|
|
|
52
72
|
If a credential may have been exposed, stop using it, revoke it, rotate the relevant client secret, review logs without copying secret values, and require reauthorization when the grant can no longer be trusted.
|
|
53
73
|
|
package/docs/oauth/tokens.md
CHANGED
|
@@ -8,6 +8,8 @@ Teal boxes show the normal token lifecycle. The amber box marks a terminal failu
|
|
|
8
8
|
|
|
9
9
|
## Determine expiration
|
|
10
10
|
|
|
11
|
+
Keep each token set associated with the application user and the authorized Lunch Money user and budgeting account pair. Renewing credentials does not change that pair; selecting another budgeting account requires a new authorization. See [users, budgeting accounts, and access tokens](/oauth/concepts#users-budgeting-accounts-and-access-tokens).
|
|
12
|
+
|
|
11
13
|
Access tokens are deliberately short-lived. Record when the token response arrives and add its `expires_in` duration to determine the expected expiry. Refresh slightly before that point while accounting for clock skew. Do not parse the opaque access token or assume a fixed lifetime.
|
|
12
14
|
|
|
13
15
|
## Refresh access
|
|
@@ -16,13 +18,13 @@ A refresh token is issued after a successful authorization-code exchange only wh
|
|
|
16
18
|
|
|
17
19
|
The [confidential Node.js sample](/oauth/sample-applications#try-oauth-with-your-lunch-money-account) keeps refresh optional within its first-run walkthrough. Register the sample client with `offline_access` to exercise the refresh branch, or omit it to focus on authorization, `GET /v2/me`, revocation, and reauthorization.
|
|
18
20
|
|
|
19
|
-
Send a form-encoded `grant_type=refresh_token` request to the discovered token endpoint. Confidential clients authenticate with `client_secret_basic`; public clients send their client ID without a secret. A refresh never expands or switches the grant's registered scopes.
|
|
21
|
+
Send a form-encoded `grant_type=refresh_token` request to the discovered token endpoint. Confidential clients authenticate with `client_secret_basic`; public clients send their client ID without a secret. A refresh never expands or switches the grant's registered scopes, Lunch Money user, or budgeting-account identity.
|
|
20
22
|
|
|
21
|
-
Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each application-user connection. Lunch Money rotates refresh tokens; replay of a consumed token can invalidate the token family. Treat transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials and start a fresh authorization.
|
|
23
|
+
Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each active application-user and Lunch Money user/account connection. Lunch Money rotates refresh tokens; replay of a consumed token can invalidate the token family. Treat transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials for that connection and start a fresh authorization.
|
|
22
24
|
|
|
23
25
|
## Revoke access
|
|
24
26
|
|
|
25
|
-
Send the token to the discovered revocation endpoint using the authentication method appropriate to the client. Revocation returns success without revealing unnecessary token state. Locally discard
|
|
27
|
+
Send the active connection's token to the discovered revocation endpoint using the authentication method appropriate to the client. Revocation returns success without revealing unnecessary token state. Locally discard that connection's access token, refresh token, and related session state even if the network response is ambiguous. Keep other user/account connections unchanged.
|
|
26
28
|
|
|
27
29
|
> [!TIP] Revocation endpoint
|
|
28
30
|
> The currently resolved endpoint is `https://api.lunchmoney.dev/oauth/revoke`. Use authorization-server discovery to configure deployed applications so they receive the current endpoint automatically.
|
|
@@ -57,7 +59,9 @@ Do not paste a real client secret or token directly into a command where it may
|
|
|
57
59
|
|
|
58
60
|
Revocation is different from expiration: expiration ends one credential naturally, while revocation deliberately removes access. Deleting or disabling an application and loss of user access can also make tokens unusable.
|
|
59
61
|
|
|
60
|
-
A Lunch Money user can also revoke an application's access from the [Connected Apps page](https://my.lunchmoney.app/connected-apps). Your application should treat the resulting authentication failure as terminal and ask the user to authorize again when they want to reconnect.
|
|
62
|
+
A Lunch Money user can also revoke an application's access from the [Connected Apps page](https://my.lunchmoney.app/connected-apps). User revocation applies to the selected Lunch Money user and budgeting-account authorization. Your application should treat the resulting authentication failure as terminal for that connection and ask the user to authorize it again when they want to reconnect. Keep every stored Lunch Money user/account connection independent, including when two users have authorized the same shared `account_id`.
|
|
63
|
+
|
|
64
|
+
Remote revocation is different from local credential deletion. If an application offers a local-only “forget” action, confirm it and explain that the remote authorization remains active. Removing the active user's final local connection should leave the application disconnected rather than expose a hidden connection belonging to another Lunch Money user.
|
|
61
65
|
|
|
62
66
|
## Recovery rules
|
|
63
67
|
|
|
@@ -84,12 +84,12 @@ If a successful authorization-code exchange does not include a refresh token, co
|
|
|
84
84
|
Treat an OAuth `401` as an unknown access-token failure unless a refresh request establishes what happened. When an eligible refresh token is available, attempt one refresh and handle the token endpoint's result, which uses the OAuth format shown above rather than the v2 API format:
|
|
85
85
|
|
|
86
86
|
1. On success, save the replacement tokens and retry the API request once. The access token had merely expired.
|
|
87
|
-
2. On `invalid_grant`, treat
|
|
87
|
+
2. On `invalid_grant`, treat that connection as revoked: mark the connection for the selected user/account as inactive and discard its tokens. Prompt the user to authorize that connection again the next time they need it rather than repeatedly interrupting them. Leave the user's other budgeting-account connections unchanged.
|
|
88
88
|
3. On `invalid_client`, stop requests for every user of the client and contact [developer support](mailto:developer-support@lunchmoney.app). Lunch Money may have disabled the client; retrying or asking users to authorize again will not help.
|
|
89
89
|
|
|
90
90
|
If no eligible refresh token is available, discard the rejected access token and ask the user to authorize again.
|
|
91
91
|
|
|
92
|
-
An OAuth access token can fail because it expired, access was revoked, the client was disabled, the
|
|
92
|
+
An OAuth access token can fail because it expired, access was revoked, the client was disabled, the token was revoked or rotated, or the user lost access to the budgeting account. Every OAuth access-token failure returns the same generic `401` response:
|
|
93
93
|
|
|
94
94
|
```http
|
|
95
95
|
HTTP/1.1 401
|
|
@@ -135,18 +135,6 @@ The `scope` parameter names every scope the endpoint requires, including any the
|
|
|
135
135
|
|
|
136
136
|
Adding `scope` to an authorization URL cannot upgrade or narrow a grant. Lunch Money ignores the requested value and uses the client's complete registered scope set. To change scopes, create a replacement client, test all functionality, complete any required review, update the deployed client ID and credentials, and send every existing user through authorization again.
|
|
137
137
|
|
|
138
|
-
## Client-management failures
|
|
139
|
-
|
|
140
|
-
The Developer Portal can return structured errors when you register clients, create client secrets, or submit a client for review. These errors use the Lunch Money response format, not OAuth protocol errors such as `invalid_grant`.
|
|
141
|
-
|
|
142
|
-
| Status and code | Meaning | What to do |
|
|
143
|
-
| --- | --- | --- |
|
|
144
|
-
| `422 quota_exceeded`, field `clients` | You already own 25 non-deleted OAuth clients. | Delete a client you no longer use before registering another. |
|
|
145
|
-
| `422 quota_exceeded`, field `secrets` | The client already has three non-revoked secrets. Expired secrets still count. | Revoke an obsolete or expired secret before creating another. |
|
|
146
|
-
| `429 review_quota_exceeded` | You have made five initial review submissions across your clients in the rolling 24-hour window. | Wait until the time shown in the Developer Portal before submitting again. API clients should also respect the `Retry-After` response header. |
|
|
147
|
-
|
|
148
|
-
Cancelled submissions, rejected-client resubmissions, and submissions for clients later deleted still count toward the review limit. Reviews opened automatically after an active client's identity changes do not count.
|
|
149
|
-
|
|
150
138
|
## Local callback failures
|
|
151
139
|
|
|
152
140
|
For local development, use a supported loopback host: `localhost`, `127.0.0.1`, or `[::1]`. Matching depends on the client type:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="960" viewBox="0 0 1120 960" role="img" aria-labelledby="title desc">
|
|
2
2
|
<title id="title">Lunch Money OAuth authorization-code flow</title>
|
|
3
|
-
<desc id="desc">A sequence diagram with three actors: your application, the Lunch Money user, and Lunch Money. The application redirects the user's browser to Lunch Money. When necessary, Lunch Money presents its sign-in screen and the user signs in directly with Lunch Money. Lunch Money then presents the budgeting-account selector and requested permissions, the user selects an account and approves access, Lunch Money returns an authorization code, and the application exchanges it for tokens and calls the API.</desc>
|
|
3
|
+
<desc id="desc">A sequence diagram with three actors: your application, the Lunch Money user, and Lunch Money. The application redirects the user's browser to Lunch Money. When necessary, Lunch Money presents its sign-in screen and the user signs in directly with Lunch Money. Lunch Money then presents the budgeting-account selector and requested permissions, the user selects an account and approves access, Lunch Money returns an authorization code, and the application exchanges it for tokens. The application calls GET /v2/me, saves the identified connection, and calls the API for that account.</desc>
|
|
4
4
|
<defs>
|
|
5
5
|
<marker id="app-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#b45309"/></marker>
|
|
6
6
|
<marker id="user-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"><path d="M0,0 L0,6 L9,3 z" fill="#9a6700"/></marker>
|
|
@@ -18,15 +18,18 @@
|
|
|
18
18
|
<text class="actor-label" x="560" y="56" text-anchor="middle">Lunch Money user</text><text class="actor-detail" x="560" y="79" text-anchor="middle">browser, sign-in, and approval</text>
|
|
19
19
|
<rect class="actor lm" x="820" y="25" width="260" height="72" rx="12"/>
|
|
20
20
|
<text class="actor-label" x="950" y="56" text-anchor="middle">Lunch Money</text><text class="actor-detail" x="950" y="79" text-anchor="middle">authorization server and API</text>
|
|
21
|
-
<path class="lifeline" d="M170 105
|
|
21
|
+
<path class="lifeline" d="M170 105 V930"/><path class="lifeline" d="M560 105 V930"/><path class="lifeline" d="M950 105 V930"/>
|
|
22
22
|
<path class="flow app-flow" d="M170 140 H550"/><rect class="step-bg" x="278" y="118" width="180" height="28" rx="6"/><text class="step" x="368" y="137" text-anchor="middle">1. Redirect the user</text>
|
|
23
23
|
<path class="flow user-flow" d="M560 200 H940"/><rect class="step-bg" x="652" y="178" width="196" height="28" rx="6"/><text class="step" x="750" y="197" text-anchor="middle">2. Request authorization</text>
|
|
24
24
|
<path class="flow lm-flow" d="M950 260 H570"/><rect class="step-bg" x="640" y="238" width="240" height="28" rx="6"/><text class="step" x="760" y="257" text-anchor="middle">3. Show sign-in, when needed</text>
|
|
25
25
|
<path class="flow user-flow" d="M560 320 H940"/><rect class="step-bg" x="605" y="298" width="290" height="28" rx="6"/><text class="step" x="750" y="317" text-anchor="middle">4. Sign in directly with Lunch Money</text>
|
|
26
|
-
<path class="flow lm-flow" d="M950 380 H570"/><rect class="step-bg" x="
|
|
26
|
+
<path class="flow lm-flow" d="M950 380 H570"/><rect class="step-bg" x="545" y="358" width="430" height="28" rx="6"/><text class="step" x="760" y="377" text-anchor="middle">5. Present budgeting-account selector and permissions</text>
|
|
27
27
|
<path class="flow user-flow" d="M560 440 H940"/><rect class="step-bg" x="585" y="418" width="330" height="28" rx="6"/><text class="step" x="750" y="437" text-anchor="middle">6. Select budgeting account and approve</text>
|
|
28
28
|
<path class="flow lm-flow" d="M950 500 H180"/><rect class="step-bg" x="385" y="478" width="360" height="28" rx="6"/><text class="step" x="565" y="497" text-anchor="middle">7. Redirect to callback with authorization code</text>
|
|
29
29
|
<path class="flow app-flow" d="M170 580 H940"/><rect class="step-bg" x="335" y="558" width="460" height="28" rx="6"/><text class="step" x="565" y="577" text-anchor="middle">8. Exchange code with PKCE (+ client secret for confidential client)</text>
|
|
30
30
|
<path class="flow lm-flow" d="M950 640 H180"/><rect class="step-bg" x="385" y="618" width="360" height="28" rx="6"/><text class="step" x="565" y="637" text-anchor="middle">9. Return access token + optional refresh token</text>
|
|
31
|
-
<path class="flow app-flow" d="M170
|
|
31
|
+
<path class="flow app-flow" d="M170 700 H940"/><rect class="step-bg" x="430" y="678" width="270" height="28" rx="6"/><text class="step" x="565" y="697" text-anchor="middle">10. Call GET /v2/me</text>
|
|
32
|
+
<path class="flow lm-flow" d="M950 760 H180"/><rect class="step-bg" x="350" y="738" width="430" height="28" rx="6"/><text class="step" x="565" y="757" text-anchor="middle">11. Return user id, account_id, and budget_name</text>
|
|
33
|
+
<path class="flow app-flow" d="M170 800 H330 V840 H180"/><rect class="step-bg" x="285" y="798" width="330" height="28" rx="6"/><text class="step" x="450" y="817" text-anchor="middle">12. Save connection and make it active</text>
|
|
34
|
+
<path class="flow app-flow" d="M170 890 H940"/><rect class="step-bg" x="385" y="868" width="360" height="28" rx="6"/><text class="step" x="565" y="887" text-anchor="middle">13. Call the V2 API for the active account</text>
|
|
32
35
|
</svg>
|