Skip to content

Commit

Permalink
Make token retrieval optionally async
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Bryant committed Feb 22, 2022
1 parent bf0426a commit 7a39401
Show file tree
Hide file tree
Showing 10 changed files with 55 additions and 45 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.0.0

- BREAKING: `TokenStorage` now provides the option to asynchronously retrieve tokens.
- Fix internal lint errors
- Upgrade dependencies

## 1.0.2

- Automatically refreshes expired client tokens
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Easily authenticate using OAuth 2.0 client/password grants.
Install passputter from [pub.dev](https://pub.dev/packages/passputter):

```yaml
passputter: ^1.0.2
passputter: ^2.0.0
```
## ✅ Prerequisites
Expand Down Expand Up @@ -47,7 +47,7 @@ class HiveTokenStorage implements TokenStorage {
static const _userTokenKey = 'userToken';
@override
OAuthToken? get clientToken {
FutureOr<OAuthToken?> get clientToken async {
final tokenMap = _box.get(_clientTokenKey);
if (tokenMap != null) {
return OAuthToken.fromJson(tokenMap);
Expand All @@ -57,7 +57,7 @@ class HiveTokenStorage implements TokenStorage {
}
@override
OAuthToken? get userToken {
FutureOr<OAuthToken?> get userToken async {
final tokenMap = _box.get(_userTokenKey);
if (tokenMap != null) {
return OAuthToken.fromJson(tokenMap);
Expand Down
3 changes: 1 addition & 2 deletions lib/src/client_token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 📦 Package imports:
import 'package:clock/clock.dart';
import 'package:dio/dio.dart';

// 🌎 Project imports:
import 'package:passputter/passputter.dart';
import 'package:passputter/src/oauth_api_interface.dart';
Expand Down Expand Up @@ -38,7 +37,7 @@ class ClientTokenInterceptor extends Interceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = tokenStorage.clientToken;
final token = await tokenStorage.clientToken;
if (token == null) {
try {
// No token saved; get another one.
Expand Down
13 changes: 6 additions & 7 deletions lib/src/oauth_api_impl.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// 📦 Package imports:
import 'package:dio/dio.dart';

// 🌎 Project imports:
import 'package:passputter/src/oauth_api_interface.dart';
import 'package:passputter/src/oauth_token.dart';
Expand All @@ -23,7 +22,7 @@ class OAuthApiImpl implements OAuthApiInterface {
required String clientId,
required String clientSecret,
}) async {
final r = await client.post(
final r = await client.post<String>(
endpoint,
data: <String, String>{
'client_id': clientId,
Expand All @@ -33,7 +32,7 @@ class OAuthApiImpl implements OAuthApiInterface {
options: Options(contentType: Headers.formUrlEncodedContentType),
);

return OAuthToken.fromJson(r.data);
return OAuthToken.fromJson(r.data!);
}

@override
Expand All @@ -42,7 +41,7 @@ class OAuthApiImpl implements OAuthApiInterface {
required String clientId,
required String clientSecret,
}) async {
final r = await client.post(
final r = await client.post<String>(
endpoint,
data: <String, String>{
'refresh_token': refreshToken,
Expand All @@ -53,7 +52,7 @@ class OAuthApiImpl implements OAuthApiInterface {
options: Options(contentType: Headers.formUrlEncodedContentType),
);

return OAuthToken.fromJson(r.data);
return OAuthToken.fromJson(r.data!);
}

@override
Expand All @@ -63,7 +62,7 @@ class OAuthApiImpl implements OAuthApiInterface {
required String clientId,
required String clientSecret,
}) async {
final r = await client.post(
final r = await client.post<String>(
endpoint,
data: <String, String>{
'username': username,
Expand All @@ -75,6 +74,6 @@ class OAuthApiImpl implements OAuthApiInterface {
options: Options(contentType: Headers.formUrlEncodedContentType),
);

return OAuthToken.fromJson(r.data);
return OAuthToken.fromJson(r.data!);
}
}
10 changes: 6 additions & 4 deletions lib/src/oauth_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import 'dart:convert';

// 📦 Package imports:
import 'package:clock/clock.dart';
import 'package:meta/meta.dart';

/// An authentication token.
@immutable
class OAuthToken {
/// Constructs an [OAuthToken]
const OAuthToken({
Expand All @@ -18,19 +20,19 @@ class OAuthToken {
Map<String, dynamic> map, [
Clock clock = const Clock(),
]) {
final expiresIn = map['expires_in'];
final expiresIn = map['expires_in'] as int?;
return OAuthToken(
token: map['access_token'],
token: map['access_token'] as String,
expiresAt: expiresIn != null
? clock.now().add(Duration(seconds: expiresIn))
: null,
refreshToken: map['refresh_token'],
refreshToken: map['refresh_token'] as String?,
);
}

/// Constructs an [OAuthToken] from a JSON [source]
factory OAuthToken.fromJson(String source) =>
OAuthToken.fromMap(json.decode(source));
OAuthToken.fromMap(json.decode(source) as Map<String, dynamic>);

/// The token used to authenticate requests.
///
Expand Down
6 changes: 4 additions & 2 deletions lib/src/token_storage.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// 🌎 Project imports:
import 'dart:async';

import 'oauth_token.dart';

/// Handles storage and retrieval of [OAuthToken]s.
abstract class TokenStorage {
/// Retrieves the currently saved client token if it exists, or none.
OAuthToken? get clientToken;
FutureOr<OAuthToken?> get clientToken;

/// Retrieves the currently saved user token if it exists, or none.
OAuthToken? get userToken;
FutureOr<OAuthToken?> get userToken;

/// Saves a new client [token].
///
Expand Down
4 changes: 1 addition & 3 deletions lib/src/user_token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 📦 Package imports:
import 'package:clock/clock.dart';
import 'package:dio/dio.dart';

// 🌎 Project imports:
import 'package:passputter/passputter.dart';
import 'package:passputter/src/oauth_api_interface.dart';
Expand Down Expand Up @@ -38,7 +37,7 @@ class UserTokenInterceptor extends Interceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = tokenStorage.userToken;
final token = await tokenStorage.userToken;
if (token != null) {
if (token.expiresAt != null && token.expiresAt!.isBefore(clock.now())) {
final refreshToken = token.refreshToken;
Expand All @@ -62,7 +61,6 @@ class UserTokenInterceptor extends Interceptor {
return handler.reject(
DioError(
requestOptions: options,
type: DioErrorType.other,
error: TokenExpiredException(token),
),
);
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: passputter
description: Easily authenticate using OAuth 2.0 client/password grants.
version: 1.0.2
version: 2.0.0
repository: https://github.com/netsells/passputter

environment:
Expand All @@ -13,7 +13,7 @@ dependencies:
dev_dependencies:
import_sorter: ^4.5.0
mock_web_server: ^5.0.0-nullsafety.1
mocktail: ^0.1.2
mocktail: ^0.2.0
pretty_dio_logger: ^1.2.0-beta-1
test: ^1.17.3
time: ^2.0.0
Expand Down
39 changes: 22 additions & 17 deletions test/src/oauth_api_impl_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// 📦 Package imports:
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

// 🌎 Project imports:
import 'package:passputter/src/oauth_api_impl.dart';
import 'package:passputter/src/oauth_token.dart';
import 'package:test/test.dart';

class MockDio extends Mock implements Dio {}

Expand All @@ -28,7 +27,7 @@ void main() {
group('getClientToken', () {
test('successfully returns token', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'client_id': clientId,
Expand All @@ -41,10 +40,12 @@ void main() {
(_) async => Response(
requestOptions: RequestOptions(path: endpoint),
statusCode: 200,
data: '''{
data: '''
{
"access_token": "token",
"refresh_token": "refresh"
}''',
}
''',
),
);

Expand All @@ -64,7 +65,7 @@ void main() {

test('throws DioError if one is thrown by request', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'client_id': clientId,
Expand All @@ -76,7 +77,7 @@ void main() {
).thenAnswer((_) => Future.error(tError));

expect(
() async => await oAuthApi.getClientToken(
() async => oAuthApi.getClientToken(
clientId: clientId,
clientSecret: clientSecret,
),
Expand All @@ -90,7 +91,7 @@ void main() {

test('successfully returns token', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'refresh_token': refreshToken,
Expand All @@ -104,10 +105,12 @@ void main() {
(_) async => Response(
requestOptions: RequestOptions(path: endpoint),
statusCode: 200,
data: '''{
data: '''
{
"access_token": "token",
"refresh_token": "refresh"
}''',
}
''',
),
);

Expand All @@ -128,7 +131,7 @@ void main() {

test('throws DioError if one is thrown by request', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'refresh_token': refreshToken,
Expand All @@ -141,7 +144,7 @@ void main() {
).thenAnswer((_) => Future.error(tError));

expect(
() async => await oAuthApi.getRefreshedToken(
() async => oAuthApi.getRefreshedToken(
refreshToken: refreshToken,
clientId: clientId,
clientSecret: clientSecret,
Expand All @@ -157,7 +160,7 @@ void main() {

test('successfully returns token', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'username': username,
Expand All @@ -172,10 +175,12 @@ void main() {
(_) async => Response(
requestOptions: RequestOptions(path: endpoint),
statusCode: 200,
data: '''{
data: '''
{
"access_token": "token",
"refresh_token": "refresh"
}''',
}
''',
),
);

Expand All @@ -197,7 +202,7 @@ void main() {

test('throws DioError if one is thrown by request', () async {
when(
() => dio.post(
() => dio.post<String>(
endpoint,
data: <String, String>{
'username': username,
Expand All @@ -211,7 +216,7 @@ void main() {
).thenAnswer((_) => Future.error(tError));

expect(
() async => await oAuthApi.getUserToken(
() async => oAuthApi.getUserToken(
username: username,
password: password,
clientId: clientId,
Expand Down
9 changes: 4 additions & 5 deletions test/src/user_token_interceptor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import 'package:clock/clock.dart';
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import 'package:time/time.dart';

// 🌎 Project imports:
import 'package:passputter/passputter.dart';
import 'package:passputter/src/oauth_api_interface.dart';
import 'package:passputter/src/oauth_token.dart';
import 'package:passputter/src/token_expired_exception.dart';
import 'package:test/test.dart';
import 'package:time/time.dart';

class MockOAuthApi extends Mock implements OAuthApiInterface {}

Expand All @@ -23,7 +22,7 @@ void main() {
late UserTokenInterceptor interceptor;

setUpAll(() {
registerFallbackValue<DioError>(
registerFallbackValue(
DioError(
requestOptions: RequestOptions(
path: 'path',
Expand All @@ -36,7 +35,7 @@ void main() {
tokenStorage = InMemoryTokenStorage();
oAuthApi = MockOAuthApi();
handler = MockHandler();
clock = Clock.fixed(DateTime(2021, 1, 1));
clock = Clock.fixed(DateTime(2021));
interceptor = UserTokenInterceptor(
tokenStorage: tokenStorage,
oAuthApi: oAuthApi,
Expand Down

0 comments on commit 7a39401

Please sign in to comment.