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 Sep 5, 2024
1 parent 8355052 commit 685a517
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void main() {
.having(
(c) => c.httpClient,
'httpClient',
isA<NeonHttpClient>().having((c) => c.interceptors, 'interceptors', hasLength(2)),
isA<NeonHttpClient>().having((c) => c.interceptors, 'interceptors', hasLength(3)),
),
);

Expand All @@ -81,7 +81,7 @@ void main() {
.having(
(c) => c.httpClient,
'httpClient',
isA<NeonHttpClient>().having((c) => c.interceptors, 'interceptors', hasLength(2)),
isA<NeonHttpClient>().having((c) => c.interceptors, 'interceptors', hasLength(3)),
),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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) {
final authorization = request.headers['authorization'];
return authorization != null && authorization.isNotEmpty && _blocked;
}

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

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

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

@override
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 @@ -48,6 +49,8 @@ final class NeonHttpClient with http.BaseClient {
builder.addAll(interceptors);
}

builder.add(AuthenticationInterceptor());

if (cookieStore != null) {
builder.add(
CookieStoreInterceptor(cookieStore: cookieStore),
Expand All @@ -64,9 +67,7 @@ final class NeonHttpClient with http.BaseClient {
);
}

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

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 @@ -2,6 +2,7 @@ import 'package:cookie_store/cookie_store.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:mocktail/mocktail.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/neon_http_client.dart';
import 'package:nextcloud/nextcloud.dart';
Expand Down Expand Up @@ -46,8 +47,8 @@ void main() {
);

expect(
client.interceptors.first,
isA<BaseHeaderInterceptor>(),
client.interceptors,
contains(isA<BaseHeaderInterceptor>()),
);
});

Expand All @@ -59,9 +60,23 @@ void main() {
cookieStore: cookieStore,
);

expect(
client.interceptors,
contains(isA<CookieStoreInterceptor>()),
);
});

test('adds authentication interceptor before cookie store', () {
final cookieStore = _MockCookieStore();

client = NeonHttpClient(
baseURL: Uri(),
cookieStore: cookieStore,
);

expect(
client.interceptors.first,
isA<CookieStoreInterceptor>(),
isA<AuthenticationInterceptor>(),
);
});

Expand Down

0 comments on commit 685a517

Please sign in to comment.