Skip to content

Commit

Permalink
[SDK-4001] Logout for web platform (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevehobbsdev authored Mar 10, 2023
1 parent dc1b2dc commit 7e3ce1b
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 36 deletions.
6 changes: 5 additions & 1 deletion auth0_flutter/example/lib/example_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ class _ExampleAppState extends State<ExampleApp> {
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
await webAuth.logout();
if (kIsWeb) {
await auth0Web.logout(returnToUrl: 'http://localhost:3000');
} else {
await webAuth.logout();
}
output = 'Logged out.';

setState(() {
Expand Down
15 changes: 14 additions & 1 deletion auth0_flutter/lib/auth0_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interfac
import 'src/version.dart';

export 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'
show CacheLocation;
show CacheLocation, LogoutOptions;

/// Primary interface for interacting with Auth0 on web platforms.
class Auth0Web {
Expand Down Expand Up @@ -94,6 +94,19 @@ class Auth0Web {
scopes: scopes ?? {},
idTokenValidationConfig: IdTokenValidationConfig(maxAge: maxAge)));

/// Redirects the browser to the Auth0 logout endpoint to end the user's
/// session.
///
/// * Use [returnToUrl] to tell Auth0 where it should redirect back to once
/// the user has logged out. This URL must be registered in **Allowed
/// Logout URLs** in your Auth0 client settings. [Read more about how redirecting after logout works](https://auth0.com/docs/logout/guides/redirect-users-after-logout).
/// * Use [federated] to log the user out of their identity provider
/// (e.g. Google) as well as Auth0. Only applicable if the user authenticated
/// using an identity provider. [Read more about how federated logout works at Auth0](https://auth0.com/docs/logout/guides/logout-idps)
Future<void> logout({final bool? federated, final String? returnToUrl}) =>
Auth0FlutterWebPlatform.instance
.logout(LogoutOptions(federated: federated, returnTo: returnToUrl));

Future<Credentials> credentials() =>
Auth0FlutterWebPlatform.instance.credentials();

Expand Down
43 changes: 19 additions & 24 deletions auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import 'dart:async';
import 'dart:html';
import 'dart:js_util';

import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'auth0_flutter_web_platform_proxy.dart';
import 'extensions/client_options_extensions.dart';
import 'extensions/credentials_extension.dart';
import 'js_interop.dart';
import 'extensions/logout_options.extension.dart';
import 'js_interop.dart' as interop;
import 'js_interop_utils.dart';

typedef UrlSearchProvider = String? Function();

Expand All @@ -22,8 +25,8 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
Future<void> initialize(
final ClientOptions clientOptions, final UserAgent userAgent) {
clientProxy ??= Auth0FlutterWebClientProxy(
client: Auth0Client(
_stripNulls(clientOptions.toAuth0ClientOptions(userAgent))));
client: interop.Auth0Client(JsInteropUtils.stripNulls(
clientOptions.toAuth0ClientOptions(userAgent))));

final search = urlSearchProvider();

Expand All @@ -40,7 +43,7 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
Future<void> loginWithRedirect(final LoginOptions? options) {
final client = _ensureClient();

final authParams = _stripNulls(AuthorizationParams(
final authParams = JsInteropUtils.stripNulls(interop.AuthorizationParams(
audience: options?.audience,
redirect_uri: options?.redirectUrl,
organization: options?.organizationId,
Expand All @@ -50,15 +53,24 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
? options?.scopes.join(' ')
: null));

final loginOptions = RedirectLoginOptions(authorizationParams: authParams);
final loginOptions =
interop.RedirectLoginOptions(authorizationParams: authParams);

return client.loginWithRedirect(loginOptions);
}

@override
Future<void> logout(final LogoutOptions? options) async {
final client = _ensureClient();
final logoutOptions = options?.toClientLogoutOptions();

return client.logout(logoutOptions);
}

@override
Future<Credentials> credentials() async {
final clientProxy = _ensureClient();
final options = GetTokenSilentlyOptions(detailedResponse: true);
final options = interop.GetTokenSilentlyOptions(detailedResponse: true);

final result = await clientProxy.getTokenSilently(options);

Expand All @@ -68,23 +80,6 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
@override
Future<bool> hasValidCredentials() => clientProxy!.isAuthenticated();

/// Rebuilds the input object, omitting values that are null
T _stripNulls<T extends Object>(final T obj) {
final keys = objectKeys(obj);
final output = newObject<Object>();

for (var i = 0; i < keys.length; i++) {
final key = keys[i] as String;
final value = getProperty(obj, key) as dynamic;

if (value != null) {
setProperty(output, key, value);
}
}

return output as T;
}

Auth0FlutterWebClientProxy _ensureClient() {
if (clientProxy == null) {
throw ArgumentError('Auth0Client has not been initialized');
Expand Down
11 changes: 8 additions & 3 deletions auth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'js_interop.dart';
import 'js_interop.dart' as interop;

class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
Auth0FlutterPlugin({final Auth0Client? client});
// ignore: avoid_unused_constructor_parameters
Auth0FlutterPlugin({final interop.Auth0Client? client});

static void registerWith(final Registrar registrar) {}

Expand All @@ -30,4 +30,9 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
throw UnsupportedError(
'hasValidCredentials is only supported on the web platform');
}

@override
Future<void> logout(final LogoutOptions? options) {
throw UnsupportedError('logout is only supported on the web platform');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ class Auth0FlutterWebClientProxy {
promiseToFuture(client.handleRedirectCallback());

Future<bool> isAuthenticated() => promiseToFuture(client.isAuthenticated());

Future<void> logout(final LogoutOptions? options) =>
promiseToFuture(client.logout(options));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import '../../../auth0_flutter_web.dart';
import '../js_interop.dart' as interop;
import '../js_interop_utils.dart';

extension LogoutOptionsExtension on LogoutOptions {
interop.LogoutOptions toClientLogoutOptions() => interop.LogoutOptions(
logoutParams: JsInteropUtils.stripNulls(
interop.LogoutParams(federated: federated, returnTo: returnTo)));
}
19 changes: 19 additions & 0 deletions auth0_flutter/lib/src/web/js_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ class WebCredentials {
final String? scope});
}

@JS()
@anonymous
class LogoutParams {
external String? get returnTo;
external bool? get federated;

external factory LogoutParams(
{final String? returnTo, final bool? federated});
}

@JS()
@anonymous
class LogoutOptions {
external LogoutParams? get logoutParams;

external factory LogoutOptions({final LogoutParams? logoutParams});
}

@JS()
class Auth0Client {
external Auth0Client(final Auth0ClientOptions options);
Expand All @@ -134,4 +152,5 @@ class Auth0Client {
external Future<WebCredentials> getTokenSilently(
[final GetTokenSilentlyOptions? options]);
external Future<bool> isAuthenticated();
external Future<void> logout([final LogoutOptions? logoutParams]);
}
20 changes: 20 additions & 0 deletions auth0_flutter/lib/src/web/js_interop_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:js_util';

class JsInteropUtils {
/// Rebuilds the input object, omitting values that are null
static T stripNulls<T extends Object>(final T obj) {
final keys = objectKeys(obj);
final output = newObject<Object>();

for (var i = 0; i < keys.length; i++) {
final key = keys[i] as String;
final value = getProperty(obj, key) as dynamic;

if (value != null) {
setProperty(output, key, value);
}
}

return output as T;
}
}
21 changes: 19 additions & 2 deletions auth0_flutter/test/web/auth0_fluter_web_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import 'package:auth0_flutter/auth0_flutter_web.dart';
import 'package:auth0_flutter/src/web/auth0_flutter_plugin_real.dart';
import 'package:auth0_flutter/src/web/auth0_flutter_web_platform_proxy.dart';
import 'package:auth0_flutter/src/web/js_interop.dart';
import 'package:auth0_flutter/src/web/js_interop.dart' as interop;
import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -17,7 +17,7 @@ void main() {
final mockClientProxy = MockAuth0FlutterWebClientProxy();
final jwtPayload = {'sub': 'auth0:1'};
final jwt = JWT(jwtPayload).sign(SecretKey('secret'));
final WebCredentials webCredentials = WebCredentials(
final interop.WebCredentials webCredentials = interop.WebCredentials(
access_token: jwt,
id_token: jwt,
refresh_token: jwt,
Expand Down Expand Up @@ -153,4 +153,21 @@ void main() {

expect(() async => auth0.credentials(), throwsException);
});

test('logout is called and succeeds', () async {
when(mockClientProxy.logout(any)).thenAnswer((final _) => Future.value());
await auth0.logout(federated: true, returnToUrl: 'http://returnto.url');

final params =
verify(mockClientProxy.logout(captureAny)).captured.first.logoutParams;

expect(params.federated, true);
expect(params.returnTo, 'http://returnto.url');
});

test('logout is called and throws', () async {
when(mockClientProxy.logout(any)).thenThrow(Exception());

expect(() async => auth0.logout(), throwsException);
});
}
15 changes: 12 additions & 3 deletions auth0_flutter/test/web/auth0_fluter_web_test.mocks.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Mocks generated by Mockito 5.3.2 from annotations
// in auth0_flutter/test/web/auth0_fluter_web_test.dart.
// in auth0_flutter/example/ios/.symlinks/plugins/auth0_flutter/test/web/auth0_fluter_web_test.dart.
// Do not manually edit this file.

// ignore_for_file: no_leading_underscores_for_library_prefixes
Expand Down Expand Up @@ -95,10 +95,10 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock
)),
) as _i4.Future<_i2.WebCredentials>);
@override
_i4.Future<void> handleRedirectCallback([String? url]) => (super.noSuchMethod(
_i4.Future<void> handleRedirectCallback() => (super.noSuchMethod(
Invocation.method(
#handleRedirectCallback,
[url],
[],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
Expand All @@ -111,4 +111,13 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock
),
returnValue: _i4.Future<bool>.value(false),
) as _i4.Future<bool>);
@override
_i4.Future<void> logout(_i2.LogoutOptions? options) => (super.noSuchMethod(
Invocation.method(
#logout,
[options],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export 'src/web-auth/web_auth_logout_options.dart';
export 'src/web-auth/web_authentication_exception.dart';
export 'src/web/cache_location.dart';
export 'src/web/client_options.dart';
export 'src/web/logout_options.dart';
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import '../auth0_flutter_platform_interface.dart';
import 'web/client_options.dart';

class StubAuth0FlutterWeb extends Auth0FlutterWebPlatform {}

Expand Down Expand Up @@ -34,4 +32,8 @@ abstract class Auth0FlutterWebPlatform extends PlatformInterface {
throw UnimplementedError(
'web.hasValidCredentials has not been implemented');
}

Future<void> logout(final LogoutOptions? options) {
throw UnimplementedError('web.logout has not been implemented');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class LogoutOptions {
final String? returnTo;
final bool? federated;

LogoutOptions({this.returnTo, this.federated});
}

0 comments on commit 7e3ce1b

Please sign in to comment.