algolia / algolia/algoliasearch-client-dart

Secure API Key Refresh Mechanism

Open
#9 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Dart
Stars
17
Forks
10
PR merge metrics
No merged PRs in 30d

Description

**Feature Request**
There is an issue where we are receiving validUntil errors from Algolia because of our secure api key expiry.

Currently, we create a HitSearcher instance and it expects application id and api Key, this works perfectly fine but in our case we are using a secure api key which has a specific validity period and needs to be refreshed. So when that api key expires the only way through current implementation is to re-instantiate HitSearcher and this has associated streams as well, and we need to dispose of previous resources which don't really work well in some use cases. If there is a better way, please do share.

I see that there is DioRequester
https://github.com/algolia/algoliasearch-client-dart/blob/main/packages/client_core/lib/src/transport/dio/dio_requester.dart
which implements Requester https://github.com/algolia/algoliasearch-client-dart/blob/main/packages/client_core/lib/src/transport/requester.dart.
This abstract requester can expose final Iterable interceptors as well.
Then, this requester can have one implementation which is extendable as well, where the client can be initialized along with other interceptors and this interceptor .e.g (AuthInterceptor, AgentInterceptor, LogInterceptor, ...intecptors allowing developers to implement custom requesters, where they can add their own interceptors as well. In our case, that interceptor can be used to proactively check for api-key validity and fetch a new one if needed.
And lastly, this requester can also be exposed from client options https://github.com/algolia/algoliasearch-helper-flutter/blob/e4162d951e367bdf198576db4a738046ab32acd1/helper/lib/src/client_options.dart.

Currently such abstraction doesn't exists so to acheive this we had to use the requester as it is from algolia.SearchClient, as below

```
class AlgoliaClient {
final String appId;
final String apiKey;

AlgoliaClient({
required this.appId,
required this.apiKey,
});

SearchClient get instance {
const connectTimeout = Duration(seconds: 60);
final agentSegments = [
const AgentSegment(
value: 'algolia-helper-flutter',
version: libVersion,
),
];
return SearchClient(
appId: appId,
apiKey: apiKey,
options: ClientOptions(
connectTimeout: connectTimeout,
requester: AlgoliaRequester(
appId: appId,
apiKey: apiKey,
connectTimeout: connectTimeout,
clientSegments: [
const AgentSegment(value: "Search", version: packageVersion),
...agentSegments
],
),
agentSegments: agentSegments,
),
);
}
}
```

Using AlgoliaRequester to add AlgoliaApiKeyInterceptor
```
class AlgoliaRequester implements Requester {
final Dio _client;

AlgoliaRequester({
required String appId,
required String apiKey,
Map? headers,
Duration? connectTimeout,
Iterable? clientSegments,
Function(Object?)? logger,
}) : _client = Dio(
BaseOptions(
headers: headers,
connectTimeout: connectTimeout,
),
)..interceptors.addAll([
AuthInterceptor(appId: appId, apiKey: apiKey),
AlgoliaApiKeyInterceptor(), <---------------------------------- Algolia api key inteceptor
AgentInterceptor(
agent: AlgoliaAgent(packageVersion)
..addAll(clientSegments ?? const [])
..addAll(Platform.agentSegments()),
),
if (logger != null)
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: logger,
),
]);

... over-rides go here
}
```

Inteceptor

```
class AlgoliaApiKeyInterceptor extends Interceptor {
@override
Future onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final authTokenService = getIt();
final chopperApiClient = ChopperApiClient.instance;
// Fetch refresh token from prefs
final authToken = await authTokenService.getAuthToken();

ResponseAuthDto? refreshAuthToken;

if (authToken != null) {
final isRefreshTokenExpired =
JwtDecoder.isExpired(authToken.refreshToken);

if (isRefreshTokenExpired) {
refreshAuthToken = (await chopperApiClient.handshakePost()).body;
} else {
final isAccessTokenExpired =
JwtDecoder.isExpired(authToken.accessToken);

if (isAccessTokenExpired) {
refreshAuthToken = (await chopperApiClient.handshakeRefreshPost(
body: HandshakeRefreshDto(refreshToken: authToken.refreshToken),
))
.body;
} else {
refreshAuthToken = authToken;
}
}
} else {
refreshAuthToken = (await chopperApiClient.handshakePost()).body;
}

// Save new access token to prefs
unawaited(authTokenService.saveToLocal(refreshAuthToken!));
options.headers['x-algolia-api-key'] =
refreshAuthToken.algoliaConfig?.apiKey;

super.onRequest(options, handler);
}
}
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Read packages/client_core/lib/src/transport/dio/dio_requester.dart and packages/client_core/lib/src/transport/requester.dart first, then inspect the client options entry point linked in the issue. Compare the existing requester and interceptor interfaces with the proposed customization; done means a client can refresh the secure API key without recreating HitSearcher or disposing its streams.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
api, authentication
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.