|
| 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 | +``` |
0 commit comments