···
2
+
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
5
+
import '../dpop/fetch_dpop.dart';
import '../errors/token_invalid_error.dart';
import '../errors/token_revoked_error.dart';
import '../oauth/oauth_server_agent.dart';
···
/// The session getter for retrieving and refreshing tokens
final SessionGetterInterface sessionGetter;
140
-
/// HTTP client for making requests
141
-
final http.Client _httpClient;
142
+
/// Dio instance with DPoP interceptor for authenticated requests
/// Creates a new OAuth session.
···
/// - [server]: The OAuth server agent
/// - [sub]: The subject (user's DID)
/// - [sessionGetter]: The session getter for token management
149
-
/// - [httpClient]: Optional HTTP client (defaults to http.Client())
required this.sessionGetter,
154
-
http.Client? httpClient,
155
-
}) : _httpClient = httpClient ?? http.Client();
155
+
}) : _dio = Dio() {
156
+
// Add DPoP interceptor for authenticated requests to resource servers
157
+
_dio.interceptors.add(
158
+
createDpopInterceptor(
159
+
DpopFetchWrapperOptions(
160
+
key: server.dpopKey,
161
+
nonces: server.dpopNonces,
162
+
sha256: server.runtime.sha256,
163
+
isAuthServer: false, // Resource server requests (PDS)
AtprotoDid get did => sub;
···
'Authorization': initialAuth,
260
-
// TODO: In later chunks, add DPoP header generation
261
-
// For now, just make the request with Authorization header
263
-
final initialResponse = await _makeRequest(
272
+
// Make request with DPoP - the interceptor will automatically add DPoP header
273
+
final initialResponse = await _makeDpopRequest(
···
'Authorization': finalAuth,
294
-
final finalResponse = await _makeRequest(
304
+
final finalResponse = await _makeDpopRequest(
···
312
-
/// Makes an HTTP request.
313
-
Future<http.Response> _makeRequest(
322
+
/// Makes an HTTP request with DPoP authentication.
324
+
/// Uses Dio with DPoP interceptor which automatically adds:
325
+
/// - DPoP header with proof JWT
326
+
/// - Access token hash (ath) binding
328
+
/// Throws [DioException] for network errors, timeouts, and cancellations.
329
+
Future<http.Response> _makeDpopRequest(
Map<String, String>? headers,
319
-
final request = http.Request(method, url);
336
+
// Make request with Dio - interceptor will add DPoP header
337
+
final response = await _dio.requestUri(
342
+
responseType: ResponseType.bytes, // Get raw bytes for compatibility
343
+
validateStatus: (status) =>
344
+
true, // Don't throw on any status code
321
-
if (headers != null) {
322
-
request.headers.addAll(headers);
325
-
if (body != null) {
326
-
if (body is String) {
327
-
request.body = body;
328
-
} else if (body is List<int>) {
329
-
request.bodyBytes = body;
331
-
throw ArgumentError('Body must be String or List<int>');
349
+
// Convert Dio Response to http.Response for compatibility
350
+
return http.Response.bytes(
351
+
response.data as List<int>,
352
+
response.statusCode!,
353
+
headers: response.headers.map.map(
354
+
(key, value) => MapEntry(key, value.join(', ')),
356
+
reasonPhrase: response.statusMessage,
358
+
} on DioException catch (e) {
359
+
// If we have a response (4xx/5xx), convert it to http.Response
360
+
if (e.response != null) {
361
+
final errorResponse = e.response!;
362
+
return http.Response.bytes(
363
+
errorResponse.data is List<int>
364
+
? errorResponse.data as List<int>
365
+
: (errorResponse.data?.toString() ?? '').codeUnits,
366
+
errorResponse.statusCode!,
367
+
headers: errorResponse.headers.map.map(
368
+
(key, value) => MapEntry(key, value.join(', ')),
370
+
reasonPhrase: errorResponse.statusMessage,
373
+
// Network errors, timeouts, cancellations - rethrow
335
-
final streamedResponse = await _httpClient.send(request);
336
-
return await http.Response.fromStream(streamedResponse);
/// Checks if a response indicates an invalid token.
···
/// Disposes of resources used by this session.
355
-
_httpClient.close();