Skip to content

Commit 79fd9ae

Browse files
committed
✨ Added AI Skills
1 parent d0b1022 commit 79fd9ae

3 files changed

Lines changed: 360 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Added `==`, `hashCode`, and `toString()` to `OAuthToken` for value-based equality and easier debugging.
55
- Exported `OnErrorCallback` and `MemoryStorage`
66
- Fixed exports for `oauth2` package
7+
- Added skills for: setup and storage
78
- Updated dependencies:
89
- `sdk` to `^3.7.0`
910
- `meta` added
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
name: oauth-chopper-setup
3+
description: Add and configure OAuth2 authentication for a Chopper HTTP client using `oauth_chopper`. Use when asked to "add OAuth to Chopper", "setup oauth_chopper", "integrate OAuth2 with Chopper", "add authentication to my API client", or when setting up any of the supported grant types (resource owner password, client credentials, authorization code).
4+
---
5+
# Setting Up OAuth2 Authentication with Chopper
6+
7+
## Contents
8+
- [Package Overview](#package-overview)
9+
- [Grant Type Selection](#grant-type-selection)
10+
- [Interceptor Behavior](#interceptor-behavior)
11+
- [Error Handling](#error-handling)
12+
- [Workflow: Integrate oauth_chopper](#workflow-integrate-oauth_chopper)
13+
- [Examples](#examples)
14+
15+
## Package Overview
16+
`oauth_chopper` bridges the Chopper HTTP client with the `oauth2` package to provide turnkey OAuth2 authentication. It manages token acquisition, storage, proactive refresh of expired tokens, and automatic retry on 401 responses.
17+
18+
* Import the package with `import 'package:oauth_chopper/oauth_chopper.dart';`.
19+
* The central class is `OAuthChopper`. Create an instance with the authorization server's endpoint, client identifier, and optional secret.
20+
* Call `oauthChopper.interceptor()` to get an `OAuthInterceptor` and pass it to `ChopperClient(interceptors: [...])`.
21+
* Call `oauthChopper.requestGrant(grant)` to authenticate and obtain an `OAuthToken`.
22+
* Access the current token at any time with `await oauthChopper.token`.
23+
24+
## Grant Type Selection
25+
Choose the correct OAuth2 grant type based on the application context.
26+
27+
* **Use `ResourceOwnerPasswordGrant`** for first-party apps where the user provides their username and password directly. Requires `username` and `password` parameters. Suitable for trusted mobile/desktop apps.
28+
* **Use `ClientCredentialsGrant`** for server-to-server (machine-to-machine) communication where no user interaction is needed. Requires only the client `identifier` and `secret` on the `OAuthChopper` instance.
29+
* **Use `AuthorizationCodeGrant`** for browser-based or redirect-based login flows. Requires a `tokenEndpoint`, `redirectUrl`, a `redirect` callback (to open the authorization URL), and a `listen` callback (to capture the redirect response). Supports PKCE via the optional `codeVerifier` parameter.
30+
31+
## Interceptor Behavior
32+
Understand how the `OAuthInterceptor` manages the token lifecycle automatically.
33+
34+
* Before each request, the interceptor reads the current token from storage.
35+
* If the token exists and is expired (`isExpired == true`), it proactively calls `refresh()` before sending the request.
36+
* If no token is available, the request proceeds without an `Authorization` header.
37+
* If the server responds with HTTP 401 and a token was present, the interceptor refreshes once and retries the request.
38+
* Concurrent refresh calls are deduplicated internally. Multiple simultaneous 401 responses result in a single refresh request.
39+
40+
## Error Handling
41+
Handle authentication errors to prevent crashes and guide the user back to login.
42+
43+
* `AuthorizationException` is thrown when the authorization server rejects credentials (e.g., invalid grant, revoked token). On refresh failure with this exception, storage is automatically cleared.
44+
* `ExpirationException` is thrown on token expiration issues from the `oauth2` package.
45+
* Pass an `onError` callback to `oauthChopper.interceptor(onError: ...)` to catch errors without throwing. Use this to redirect the user to a login screen on token failure.
46+
* If no `onError` is provided, exceptions propagate to the caller.
47+
48+
## Workflow: Integrate oauth_chopper
49+
50+
Follow this sequential workflow to add OAuth2 authentication to an existing Chopper-based project.
51+
52+
**Task Progress:**
53+
- [ ] 1. Add the dependency: run `dart pub add oauth_chopper`.
54+
- [ ] 2. Create an `OAuthChopper` instance with the authorization server's `authorizationEndpoint`, `identifier`, and optional `secret`.
55+
- [ ] 3. Add `oauthChopper.interceptor()` to the `ChopperClient`'s `interceptors` list. Optionally pass an `onError` callback for graceful error handling.
56+
- [ ] 4. Choose the appropriate grant type and call `oauthChopper.requestGrant(grant)` to authenticate.
57+
- [ ] 5. If using `AuthorizationCodeGrant`, implement the `redirect` and `listen` callbacks for the browser-based flow.
58+
- [ ] 6. Wrap the `requestGrant` call in a try-catch to handle `AuthorizationException` and other errors.
59+
- [ ] 7. **Feedback Loop**: Run `dart analyze` -> review for missing imports or type errors -> fix -> re-run until clean.
60+
61+
## Examples
62+
63+
### Resource Owner Password Grant
64+
The simplest grant type for first-party apps with direct username/password input.
65+
66+
```dart
67+
import 'package:chopper/chopper.dart';
68+
import 'package:oauth_chopper/oauth_chopper.dart';
69+
70+
Future<void> main() async {
71+
// 1. Create the OAuthChopper instance.
72+
final oauthChopper = OAuthChopper(
73+
authorizationEndpoint: Uri.parse('https://auth.example.com/oauth/token'),
74+
identifier: 'my_client_id',
75+
secret: 'my_client_secret',
76+
);
77+
78+
// 2. Wire the interceptor into the Chopper client.
79+
final chopperClient = ChopperClient(
80+
baseUrl: Uri.parse('https://api.example.com'),
81+
interceptors: [
82+
oauthChopper.interceptor(
83+
onError: (error, stackTrace) {
84+
// Handle token errors (e.g., redirect to login screen).
85+
print('Auth error: $error');
86+
},
87+
),
88+
],
89+
);
90+
91+
// 3. Authenticate with username and password.
92+
try {
93+
final token = await oauthChopper.requestGrant(
94+
ResourceOwnerPasswordGrant(
95+
username: 'user@example.com',
96+
password: 'password123',
97+
),
98+
);
99+
print('Authenticated. Access token: ${token.accessToken}');
100+
} on AuthorizationException catch (e) {
101+
print('Login failed: ${e.message}');
102+
}
103+
}
104+
```
105+
106+
### Client Credentials Grant
107+
For server-to-server authentication where no user interaction is required.
108+
109+
```dart
110+
import 'package:chopper/chopper.dart';
111+
import 'package:oauth_chopper/oauth_chopper.dart';
112+
113+
Future<void> main() async {
114+
final oauthChopper = OAuthChopper(
115+
authorizationEndpoint: Uri.parse('https://auth.example.com/oauth/token'),
116+
identifier: 'service_client_id',
117+
secret: 'service_client_secret',
118+
scopes: ['read', 'write'],
119+
);
120+
121+
final chopperClient = ChopperClient(
122+
baseUrl: Uri.parse('https://api.example.com'),
123+
interceptors: [oauthChopper.interceptor()],
124+
);
125+
126+
try {
127+
final token = await oauthChopper.requestGrant(
128+
const ClientCredentialsGrant(),
129+
);
130+
print('Service authenticated. Expires: ${token.expiration}');
131+
} on AuthorizationException catch (e) {
132+
print('Authentication failed: ${e.message}');
133+
}
134+
}
135+
```
136+
137+
### Authorization Code Grant with PKCE
138+
For browser-based login flows with redirect handling. Commonly used in mobile and web apps.
139+
140+
```dart
141+
import 'package:chopper/chopper.dart';
142+
import 'package:oauth_chopper/oauth_chopper.dart';
143+
144+
Future<void> main() async {
145+
final oauthChopper = OAuthChopper(
146+
authorizationEndpoint: Uri.parse('https://auth.example.com/authorize'),
147+
identifier: 'my_client_id',
148+
secret: 'my_client_secret',
149+
scopes: ['openid', 'profile', 'email'],
150+
);
151+
152+
final chopperClient = ChopperClient(
153+
baseUrl: Uri.parse('https://api.example.com'),
154+
interceptors: [oauthChopper.interceptor()],
155+
);
156+
157+
try {
158+
final token = await oauthChopper.requestGrant(
159+
AuthorizationCodeGrant(
160+
tokenEndpoint: Uri.parse('https://auth.example.com/oauth/token'),
161+
redirectUrl: Uri.parse('myapp://callback'),
162+
redirect: (authorizationUrl) async {
163+
// Open the authorization URL in a browser or webview.
164+
// For example, using url_launcher:
165+
// await launchUrl(authorizationUrl);
166+
print('Open in browser: $authorizationUrl');
167+
},
168+
listen: (redirectUrl) async {
169+
// Listen for the redirect and return the full response URI.
170+
// Implementation depends on your platform (e.g., uni_links,
171+
// app_links, or a local HTTP server for desktop).
172+
return Uri.parse('myapp://callback?code=auth_code&state=xyz');
173+
},
174+
// Optional: provide a PKCE code verifier for enhanced security.
175+
// If omitted, one is generated automatically by the oauth2 package.
176+
// codeVerifier: 'your_pkce_code_verifier',
177+
),
178+
);
179+
print('Logged in. ID token: ${token.idToken}');
180+
} on AuthorizationException catch (e) {
181+
print('Login failed: ${e.message}');
182+
}
183+
}
184+
```
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
name: oauth-chopper-storage
3+
description: Implement persistent token storage for `oauth_chopper` by creating a custom `OAuthStorage` class. Use when asked to "persist OAuth tokens", "save OAuth credentials", "implement OAuthStorage", "store tokens securely", or "oauth_chopper storage".
4+
---
5+
# Implementing Persistent OAuth Token Storage
6+
7+
## Contents
8+
- [Why Custom Storage](#why-custom-storage)
9+
- [The OAuthStorage Interface](#the-oauthstorage-interface)
10+
- [Choosing a Storage Backend](#choosing-a-storage-backend)
11+
- [Workflow: Implement Custom OAuthStorage](#workflow-implement-custom-oauthstorage)
12+
- [Examples](#examples)
13+
14+
## Why Custom Storage
15+
Understand when and why the default in-memory storage must be replaced.
16+
17+
* `oauth_chopper` uses `MemoryStorage` by default. Tokens are lost when the app restarts or the process ends.
18+
* For any production application, implement a custom `OAuthStorage` to persist credentials across app launches.
19+
* The storage handles raw credential JSON strings. The `oauth2` package serializes and deserializes the full credential payload (access token, refresh token, expiration, scopes, etc.).
20+
* Storage is called automatically: `saveCredentials` on successful grant or refresh, `clear` when an `AuthorizationException` occurs during refresh.
21+
22+
## The OAuthStorage Interface
23+
The `OAuthStorage` abstract interface defines three methods that must be implemented.
24+
25+
* **`FutureOr<String?> fetchCredentials()`** -- Return the stored credentials JSON string, or `null` if no credentials are stored. Called before every request by the interceptor.
26+
* **`FutureOr<void> saveCredentials(String? credentialsJson)`** -- Persist the credentials JSON string. Called after a successful `requestGrant` or `refresh`. The value may be `null`.
27+
* **`FutureOr<void> clear()`** -- Delete all stored credentials. Called when the authorization server rejects a refresh attempt.
28+
* All methods support both synchronous and asynchronous return types via `FutureOr`.
29+
30+
## Choosing a Storage Backend
31+
Select the appropriate persistence mechanism based on the platform and security requirements.
32+
33+
* **Mobile apps (iOS/Android):** Use `flutter_secure_storage` for encrypted storage backed by the platform keychain/keystore. This is the recommended approach for apps handling sensitive OAuth tokens.
34+
* **Mobile/desktop apps (less sensitive):** Use `shared_preferences` for simple key-value storage. Suitable when tokens are short-lived or the threat model is low.
35+
* **Web apps:** Use `shared_preferences_web` (backed by `localStorage`) or a secure cookie-based approach. Be aware that `localStorage` is accessible to JavaScript and vulnerable to XSS.
36+
* **Server-side Dart:** Use file-based storage, a database, or an in-memory cache with persistence (e.g., Redis). Implement the storage interface around your preferred backend.
37+
* Never log or print credential JSON in production.
38+
39+
## Workflow: Implement Custom OAuthStorage
40+
41+
Follow this sequential workflow to create and integrate a persistent storage implementation.
42+
43+
**Task Progress:**
44+
- [ ] 1. Add the storage backend dependency (e.g., `dart pub add flutter_secure_storage` or `dart pub add shared_preferences`).
45+
- [ ] 2. Create a new class that implements `OAuthStorage`.
46+
- [ ] 3. Implement `fetchCredentials()` to read the stored JSON string from the backend.
47+
- [ ] 4. Implement `saveCredentials(String? credentialsJson)` to write the JSON string. Handle `null` values by deleting the entry.
48+
- [ ] 5. Implement `clear()` to delete the stored credentials.
49+
- [ ] 6. Pass the custom storage to `OAuthChopper(storage: MyCustomStorage(...))`.
50+
- [ ] 7. **Feedback Loop**: Run `dart analyze` -> review for missing imports or async issues -> fix -> re-run until clean.
51+
52+
## Examples
53+
54+
### Secure Storage with flutter_secure_storage
55+
Recommended for mobile apps. Credentials are encrypted using the platform keychain (iOS) or keystore (Android).
56+
57+
```dart
58+
import 'dart:async';
59+
60+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
61+
import 'package:oauth_chopper/oauth_chopper.dart';
62+
63+
/// Persists OAuth credentials using encrypted platform storage.
64+
class SecureOAuthStorage implements OAuthStorage {
65+
const SecureOAuthStorage(this._storage);
66+
67+
static const _key = 'oauth_credentials';
68+
final FlutterSecureStorage _storage;
69+
70+
@override
71+
FutureOr<String?> fetchCredentials() async {
72+
return _storage.read(key: _key);
73+
}
74+
75+
@override
76+
FutureOr<void> saveCredentials(String? credentialsJson) async {
77+
if (credentialsJson == null) {
78+
await _storage.delete(key: _key);
79+
} else {
80+
await _storage.write(key: _key, value: credentialsJson);
81+
}
82+
}
83+
84+
@override
85+
FutureOr<void> clear() async {
86+
await _storage.delete(key: _key);
87+
}
88+
}
89+
```
90+
91+
Usage:
92+
93+
```dart
94+
import 'package:chopper/chopper.dart';
95+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
96+
import 'package:oauth_chopper/oauth_chopper.dart';
97+
98+
void main() {
99+
final storage = SecureOAuthStorage(const FlutterSecureStorage());
100+
101+
final oauthChopper = OAuthChopper(
102+
authorizationEndpoint: Uri.parse('https://auth.example.com/oauth/token'),
103+
identifier: 'my_client_id',
104+
secret: 'my_client_secret',
105+
storage: storage, // Credentials persist across app restarts.
106+
);
107+
108+
final chopperClient = ChopperClient(
109+
baseUrl: Uri.parse('https://api.example.com'),
110+
interceptors: [oauthChopper.interceptor()],
111+
);
112+
}
113+
```
114+
115+
### Simple Storage with shared_preferences
116+
Suitable for less sensitive scenarios or when encrypted storage is not needed.
117+
118+
```dart
119+
import 'dart:async';
120+
121+
import 'package:oauth_chopper/oauth_chopper.dart';
122+
import 'package:shared_preferences/shared_preferences.dart';
123+
124+
/// Persists OAuth credentials using SharedPreferences.
125+
class PrefsOAuthStorage implements OAuthStorage {
126+
const PrefsOAuthStorage(this._prefs);
127+
128+
static const _key = 'oauth_credentials';
129+
final SharedPreferences _prefs;
130+
131+
@override
132+
FutureOr<String?> fetchCredentials() {
133+
// SharedPreferences is synchronous for reads.
134+
return _prefs.getString(_key);
135+
}
136+
137+
@override
138+
FutureOr<void> saveCredentials(String? credentialsJson) async {
139+
if (credentialsJson == null) {
140+
await _prefs.remove(_key);
141+
} else {
142+
await _prefs.setString(_key, credentialsJson);
143+
}
144+
}
145+
146+
@override
147+
FutureOr<void> clear() async {
148+
await _prefs.remove(_key);
149+
}
150+
}
151+
```
152+
153+
Usage:
154+
155+
```dart
156+
import 'package:chopper/chopper.dart';
157+
import 'package:oauth_chopper/oauth_chopper.dart';
158+
import 'package:shared_preferences/shared_preferences.dart';
159+
160+
Future<void> main() async {
161+
final prefs = await SharedPreferences.getInstance();
162+
final storage = PrefsOAuthStorage(prefs);
163+
164+
final oauthChopper = OAuthChopper(
165+
authorizationEndpoint: Uri.parse('https://auth.example.com/oauth/token'),
166+
identifier: 'my_client_id',
167+
storage: storage, // Credentials persist across app restarts.
168+
);
169+
170+
final chopperClient = ChopperClient(
171+
baseUrl: Uri.parse('https://api.example.com'),
172+
interceptors: [oauthChopper.interceptor()],
173+
);
174+
}
175+
```

0 commit comments

Comments
 (0)