Skip to content

Commit

Permalink
feat(neon_http_client): Add AuthenticationInterceptor
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <[email protected]>
  • Loading branch information
provokateurin committed Aug 31, 2024
1 parent 06dbc21 commit 4e41991
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:async';

import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:neon_http_client/src/interceptors/http_interceptor.dart';
import 'package:nextcloud/nextcloud.dart';

@internal
final class AuthenticationInterceptor implements HttpInterceptor {
var _blocked = false;

@override
bool shouldInterceptRequest(BaseRequest request) {
return (request.headers['authorization'] ?? '').isNotEmpty && _blocked;
}

@override
FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) {
throw DynamiteStatusCodeException(Response('', 401));
}

@override
bool shouldInterceptResponse(StreamedResponse response) {
if ((response.request?.headers['authorization'] ?? '').isEmpty) {
return false;
}

return response.statusCode == 401 || (response.request?.url.path.endsWith('/index.php/login/v2/poll') ?? false);
}

@override
FutureOr<StreamedResponse> interceptResponse({required StreamedResponse response, required Uri url}) {
_blocked = response.statusCode == 401;

return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart';
import 'package:cookie_store/cookie_store.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:neon_http_client/src/interceptors/authentication_interceptor.dart';
import 'package:neon_http_client/src/interceptors/interceptors.dart';
import 'package:neon_http_client/src/utils/utils.dart';
import 'package:universal_io/io.dart';
Expand Down Expand Up @@ -64,9 +65,13 @@ final class NeonHttpClient with http.BaseClient {
);
}

builder.add(
CSRFInterceptor(client: baseClient, baseURL: baseURL),
);
builder
..add(
CSRFInterceptor(client: baseClient, baseURL: baseURL),
)
..add(
AuthenticationInterceptor(),
);
});

return NeonHttpClient._(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import 'package:http/http.dart';
import 'package:neon_http_client/src/interceptors/authentication_interceptor.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:test/test.dart';

void main() {
group(AuthenticationInterceptor, () {
test('blocks after unauthenticated response', () async {
final interceptor = AuthenticationInterceptor();

final authorizedRequest = Request('GET', Uri());
authorizedRequest.headers['authorization'] = 'test';
final authorizedResponse = StreamedResponse(
const Stream.empty(),
401,
request: authorizedRequest,
);

expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isFalse,
);

expect(
interceptor.shouldInterceptResponse(authorizedResponse),
isTrue,
);
expect(
interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url),
isA<StreamedResponse>(),
);

expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isTrue,
);
await expectLater(
() async => interceptor.interceptRequest(request: authorizedRequest),
throwsA(isA<DynamiteStatusCodeException>().having((e) => e.response.statusCode, 'response.statusCode', 401)),
);
});

test('unblocks after successful poll', () async {
final interceptor = AuthenticationInterceptor();

final authorizedRequest = Request('GET', Uri());
authorizedRequest.headers['authorization'] = 'test';
final authorizedResponse = StreamedResponse(
const Stream.empty(),
401,
request: authorizedRequest,
);

expect(
interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url),
isA<StreamedResponse>(),
);
expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isTrue,
);

final pollRequest = Request(
'POST',
Uri.parse('https://cloud.example.com:8443/nextcloud/index.php/login/v2/poll'),
);
final pollResponse = StreamedResponse(
const Stream.empty(),
200,
request: pollRequest,
);

expect(
interceptor.interceptResponse(response: pollResponse, url: pollRequest.url),
isA<StreamedResponse>(),
);
expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isFalse,
);
});

test('never blocks requests without authorization', () async {
final interceptor = AuthenticationInterceptor();

final unauthorizedRequest = Request('GET', Uri());
final unauthorizedResponse = StreamedResponse(
const Stream.empty(),
401,
request: unauthorizedRequest,
);

expect(
interceptor.shouldInterceptRequest(unauthorizedRequest),
isFalse,
);
expect(
interceptor.shouldInterceptResponse(unauthorizedResponse),
isFalse,
);

final authorizedRequest = Request('GET', Uri());
authorizedRequest.headers['authorization'] = 'test';
final authorizedResponse = StreamedResponse(
const Stream.empty(),
401,
request: authorizedRequest,
);

expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isFalse,
);
expect(
interceptor.shouldInterceptResponse(authorizedResponse),
isTrue,
);
expect(
interceptor.interceptResponse(response: unauthorizedResponse, url: unauthorizedRequest.url),
isA<StreamedResponse>(),
);
expect(
interceptor.shouldInterceptRequest(authorizedRequest),
isTrue,
);

expect(
interceptor.shouldInterceptRequest(unauthorizedRequest),
isFalse,
);
expect(
interceptor.shouldInterceptResponse(unauthorizedResponse),
isFalse,
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ void main() {
);

expect(
client.interceptors.last,
isA<CSRFInterceptor>(),
client.interceptors.indexWhere((i) => i is CSRFInterceptor),
greaterThan(client.interceptors.indexWhere((i) => i is CookieStoreInterceptor)),
);
});

Expand Down

0 comments on commit 4e41991

Please sign in to comment.