Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secure API Key Refresh Mechanism #9

Open
SdxCoder opened this issue May 7, 2024 · 0 comments
Open

Secure API Key Refresh Mechanism #9

SdxCoder opened this issue May 7, 2024 · 0 comments

Comments

@SdxCoder
Copy link

SdxCoder commented May 7, 2024

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<String, dynamic>? headers,
    Duration? connectTimeout,
    Iterable<AgentSegment>? 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<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final authTokenService = getIt<AuthTokenService>();
    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);
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant