Skip to content

Commit

Permalink
Merge pull request #24 from Bdaya-Dev/manual-refresh
Browse files Browse the repository at this point in the history
feat: Support manually refreshing tokens
  • Loading branch information
ahmednfwela authored Sep 27, 2023
2 parents 2d891c8 + 56b5b14 commit 9d6ca94
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 60 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## 2023-09-27

### Changes

---

Packages with breaking changes:

- There are no breaking changes in this release.

Packages with other changes:

- [`oidc` - `v0.4.3`](#oidc---v043)

---

#### `oidc` - `v0.4.3`

- **FEAT**: add refreshToken to OidcUserManager.


## 2023-09-25

### Changes
Expand Down
13 changes: 13 additions & 0 deletions docs/oidc-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ This is similar to firebase auth, and can be used to track the current session.
You can also get access to the current authenticated user via `currentUser` property.
### Refreshing the token manually
You can refresh the token manually by calling `manager.refreshToken()`.
You can also override the refresh token `manager.refreshToken(overrideRefreshToken: 'my_refresh_token')`.
It will either return `OidcUser` with the new token, `null` or throw an [OidcException].
`null` is returned in the following cases:
- The discovery document doesn't have `grant_types_supported` include `refresh_token`
- The current user is null.
- The current user's refresh token is null.
### Dispose
If you aren't maintaining a single instance of the `OidcUserManager` class, you might want to `dispose()` it when you are done with the instance.
Expand Down
4 changes: 4 additions & 0 deletions packages/oidc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.4.3

- **FEAT**: add refreshToken to OidcUserManager.

## 0.4.2

- **FIX**: incorrect state handling.
Expand Down
22 changes: 2 additions & 20 deletions packages/oidc/example/integration_test/app_test.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,17 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:oidc_example/app_state.dart' as app_state;
import 'package:oidc_example/main.dart' as app;
import 'package:oidc_example/main.dart' as example;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('E2E', () {
testWidgets('manager initializes correctly', (tester) async {
app.main();
example.main();
expect(app_state.currentManager.didInit, false);
await tester.pumpAndSettle();
expect(app_state.currentManager.didInit, true);
// await tester.tap(find.text('Get Platform Name'));
// await tester.pumpAndSettle();
// final expected = expectedPlatformName();
// await tester.ensureVisible(find.text('Platform Name: $expected'));
});
});
}

String expectedPlatformName() {
if (isWeb) return 'Web';
if (Platform.isAndroid) return 'Android';
if (Platform.isIOS) return 'iOS';
if (Platform.isLinux) return 'Linux';
if (Platform.isMacOS) return 'MacOS';
if (Platform.isWindows) return 'Windows';
throw UnsupportedError('Unsupported platform ${Platform.operatingSystem}');
}

bool get isWeb => identical(0, 0.0);
2 changes: 1 addition & 1 deletion packages/oidc/example/lib/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import 'package:oidc_default_store/oidc_default_store.dart';
final exampleLogger = Logger('oidc.example');

/// Gets the current manager used in the example.
OidcUserManager get currentManager => duendeManager;
OidcUserManager currentManager = duendeManager;

final duendeManager = OidcUserManager.lazy(
discoveryDocumentUri: OidcUtils.getOpenIdConfigWellKnownUri(
Expand Down
127 changes: 89 additions & 38 deletions packages/oidc/example/lib/pages/secret_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,45 +99,96 @@ class _SecretPageState extends State<SecretPage> {
child: const Text('Reauthorize with prompt none'),
),
const Divider(),
DropdownButton<OidcPlatformSpecificOptions_Web_NavigationMode>(
hint: const Text('Web Navigation Mode'),
items: OidcPlatformSpecificOptions_Web_NavigationMode.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.name),
),
)
.toList(),
value: webNavigationMode,
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
webNavigationMode = value;
});
},
),
ElevatedButton(
onPressed: () async {
await app_state.currentManager.logout(
//after logout, go back to home
originalUri: Uri.parse('/'),
options: OidcPlatformSpecificOptions(
web: OidcPlatformSpecificOptions_Web(
navigationMode: webNavigationMode,
if (kIsWeb) ...[
Text(
'Logout Web Navigation Mode',
style: Theme.of(context).textTheme.headlineSmall,
),
DropdownButtonHideUnderline(
child: DropdownButton<
OidcPlatformSpecificOptions_Web_NavigationMode>(
items: OidcPlatformSpecificOptions_Web_NavigationMode.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.name),
),
)
.toList(),
value: webNavigationMode,
onChanged: (value) {
if (value == null) {
return;
}
setState(() {
webNavigationMode = value;
});
},
),
),
ElevatedButton(
onPressed: () async {
await app_state.currentManager.logout(
//after logout, go back to home
originalUri: Uri.parse('/'),
options: OidcPlatformSpecificOptions(
web: OidcPlatformSpecificOptions_Web(
navigationMode: webNavigationMode,
),
),
),
);
},
child: const Text('Logout'),
),
ElevatedButton(
onPressed: () async {
await app_state.currentManager.forgetUser();
},
child: const Text('Forget User'),
);
},
child: const Text('Logout'),
),
const Divider(),
],
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () async {
try {
final res =
await app_state.currentManager.refreshToken();
if (res == null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'It is not possible to refresh the token.',
),
),
);
return;
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Manually refreshed token!'),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'An error occurred trying to '
'refresh the token',
),
),
);
}
}
},
child: const Text('Refresh token manually'),
),
ElevatedButton(
onPressed: () async {
await app_state.currentManager.forgetUser();
},
child: const Text('Forget User'),
),
],
),
const Divider(),
Text('user id: ${user.uid}'),
Expand Down
52 changes: 52 additions & 0 deletions packages/oidc/lib/src/managers/user_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,58 @@ class OidcUserManager {
getExpiringNotificationTime: settings.refreshBefore,
);

/// Refreshes the token manually.
///
/// If token can't be refreshed `null` will be returned.
///
/// Token can be refreshed in the following cases:
/// 1. grant_types_supported MUST include refresh_token
/// 2. the [currentUser] MUST NOT be null
/// 3. the `currentUser.token` MUST include refreshToken
///
/// If any of these conditions are not met, null is returned.
///
/// An [OidcException] will be thrown if the server returns an error.
Future<OidcUser?> refreshToken({String? overrideRefreshToken}) async {
if (!discoveryDocument.grantTypesSupportedOrDefault
.contains(OidcConstants_GrantType.refreshToken)) {
//Server doesn't support refresh_token grant.
return null;
}
final user = currentUser;
if (user == null) {
return null;
}
final refreshToken = overrideRefreshToken ?? user.token.refreshToken;
if (refreshToken == null) {
// Can't refresh the access token anyway.
return null;
}

final tokenResponse = await OidcEndpoints.token(
tokenEndpoint: discoveryDocument.tokenEndpoint!,
credentials: clientCredentials,
client: httpClient,
headers: settings.extraTokenHeaders,
request: OidcTokenRequest.refreshToken(
refreshToken: refreshToken,
clientId: clientCredentials.clientId,
clientSecret: clientCredentials.clientSecret,
extra: settings.extraTokenParameters,
scope: settings.scope,
),
);
return _createUserFromToken(
token: OidcToken.fromResponse(
tokenResponse,
overrideExpiresIn: settings.getExpiresIn?.call(tokenResponse),
sessionState: user.token.sessionState,
),
nonce: null,
attributes: null,
);
}

Future<void> _listenToTokenRefreshIfSupported(
OidcTokenEventsManager tokenEventsManager,
OidcUser? user,
Expand Down
2 changes: 1 addition & 1 deletion packages/oidc/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: oidc
description: A comprehensive OpenIdConnect plugin that works on all platforms (android, ios, windows, linux, web, macos)
version: 0.4.2
version: 0.4.3
repository: https://github.com/Bdaya-Dev/oidc/tree/main/packages/oidc
topics: ['oidc', 'openidconnect', 'oauth', 'authentication']
homepage: https://bdaya-dev.github.io/oidc/
Expand Down

0 comments on commit 9d6ca94

Please sign in to comment.