From b660eef2db0cd57ebc1ac4a1e07876d09523f642 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Fri, 4 Jan 2019 10:47:01 -0500 Subject: [PATCH] external auth opts --- CHANGELOG.md | 3 + analysis_options.yaml | 1 + lib/angel_auth.dart | 1 + lib/src/configuration.dart | 116 +++++++++++++++++++++++++++ pubspec.yaml | 8 +- test/config_test.dart | 158 +++++++++++++++++++++++++++++++++++++ 6 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 lib/src/configuration.dart create mode 100644 test/config_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e5b0e7..9e2eb756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.1.0 +* Added `ExternalAuthOptions`. + # 2.0.4 * `successRedirect` was previously explicitly returning a `200`; remove this and allow the default `302`. diff --git a/analysis_options.yaml b/analysis_options.yaml index eae1e42a..c230cee7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,4 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false \ No newline at end of file diff --git a/lib/angel_auth.dart b/lib/angel_auth.dart index a27a64d5..f6476797 100644 --- a/lib/angel_auth.dart +++ b/lib/angel_auth.dart @@ -3,6 +3,7 @@ library angel_auth; export 'src/middleware/require_auth.dart'; export 'src/strategies/strategies.dart'; export 'src/auth_token.dart'; +export 'src/configuration.dart'; export 'src/options.dart'; export 'src/plugin.dart'; export 'src/popup_page.dart'; diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart new file mode 100644 index 00000000..f1f02bb1 --- /dev/null +++ b/lib/src/configuration.dart @@ -0,0 +1,116 @@ +import 'package:charcode/ascii.dart'; +import 'package:meta/meta.dart'; +import 'package:quiver_hashcode/hashcode.dart'; + +/// A common class containing parsing and validation logic for third-party authentication configuration. +class ExternalAuthOptions { + /// The user's identifier, otherwise known as an "application id". + final String clientId; + + /// The user's secret, other known as an "application secret". + final String clientSecret; + + /// The user's redirect URI. + final Uri redirectUri; + + ExternalAuthOptions._(this.clientId, this.clientSecret, this.redirectUri) { + if (clientId == null) { + throw new ArgumentError.notNull('clientId'); + } else if (clientSecret == null) { + throw new ArgumentError.notNull('clientSecret'); + } + } + + factory ExternalAuthOptions( + {@required String clientId, + @required String clientSecret, + @required redirectUri}) { + if (redirectUri is String) { + return new ExternalAuthOptions._( + clientId, clientSecret, Uri.parse(redirectUri)); + } else if (redirectUri is Uri) { + return new ExternalAuthOptions._(clientId, clientSecret, redirectUri); + } else { + throw new ArgumentError.value( + redirectUri, 'redirectUri', 'must be a String or Uri'); + } + } + + /// Returns a JSON-friendly representation of this object. + /// + /// Parses the following fields: + /// * `client_id` + /// * `client_secret` + /// * `redirect_uri` + factory ExternalAuthOptions.fromMap(Map map) { + return new ExternalAuthOptions( + clientId: map['client_id'] as String, + clientSecret: map['client_secret'] as String, + redirectUri: map['redirect_uri'], + ); + } + + @override + int get hashCode => hash3(clientId, clientSecret, redirectUri); + + @override + bool operator ==(other) => + other is ExternalAuthOptions && + other.clientId == clientId && + other.clientSecret == other.clientSecret && + other.redirectUri == other.redirectUri; + + /// Creates a copy of this object, with the specified changes. + ExternalAuthOptions copyWith( + {String clientId, String clientSecret, redirectUri}) { + return new ExternalAuthOptions( + clientId: clientId ?? this.clientId, + clientSecret: clientSecret ?? this.clientSecret, + redirectUri: redirectUri ?? this.redirectUri, + ); + } + + /// Returns a JSON-friendly representation of this object. + /// + /// Contains the following fields: + /// * `client_id` + /// * `client_secret` + /// * `redirect_uri` + /// + /// If [obscureSecret] is `true` (default), then the [clientSecret] will + /// be replaced by the string ``. + Map toJson({bool obscureSecret = true}) { + return { + 'client_id': clientId, + 'client_secret': obscureSecret ? '' : clientSecret, + 'redirect_uri': redirectUri.toString(), + }; + } + + /// Returns a [String] representation of this object. + /// + /// If [obscureText] is `true` (default), then the [clientSecret] will be + /// replaced by asterisks in the output. + /// + /// If no [asteriskCount] is given, then the number of asterisks will equal the length of + /// the actual [clientSecret]. + @override + String toString({bool obscureSecret = true, int asteriskCount}) { + String secret; + + if (!obscureSecret) { + secret = clientSecret; + } else { + var codeUnits = + new List.filled(asteriskCount ?? clientSecret.length, $asterisk); + secret = new String.fromCharCodes(codeUnits); + } + + var b = new StringBuffer('ExternalAuthOptions('); + b.write('clientId=$clientId'); + b.write(', clientSecret=$secret'); + b.write(', redirectUri=$redirectUri'); + b.write(')'); + return b.toString(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index adb5dd6c..81da26c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,20 @@ name: angel_auth description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more. -version: 2.0.4 +version: 2.1.0 author: Tobe O homepage: https://github.com/angel-dart/angel_auth environment: sdk: ">=2.0.0-dev <3.0.0" dependencies: angel_framework: ^2.0.0-alpha + charcode: ^1.0.0 crypto: ^2.0.0 http_parser: ^3.0.0 + meta: ^1.0.0 + quiver_hashcode: ^2.0.0 dev_dependencies: - http: ^0.11.0 + http: ^0.12.0 io: ^0.3.2 logging: ^0.11.0 + pedantic: ^1.0.0 test: ^1.0.0 diff --git a/test/config_test.dart b/test/config_test.dart new file mode 100644 index 00000000..be0e3e33 --- /dev/null +++ b/test/config_test.dart @@ -0,0 +1,158 @@ +import 'package:angel_auth/angel_auth.dart'; +import 'package:test/test.dart'; + +void main() { + var options = new ExternalAuthOptions( + clientId: 'foo', + clientSecret: 'bar', + redirectUri: 'http://example.com', + ); + + test('parses uri', () { + expect(options.redirectUri, Uri(scheme: 'http', host: 'example.com')); + }); + + group('copyWith', () { + test('empty produces exact copy', () { + expect(options.copyWith(), options); + }); + + test('all fields', () { + expect( + options.copyWith( + clientId: 'hey', + clientSecret: 'hello', + redirectUri: 'https://yes.no', + ), + new ExternalAuthOptions( + clientId: 'hey', + clientSecret: 'hello', + redirectUri: 'https://yes.no', + ), + ); + }); + + test('not equal to original if different', () { + expect(options.copyWith(clientId: 'hey'), isNot(options)); + }); + }); + + group('new()', () { + test('accepts uri', () { + expect( + new ExternalAuthOptions( + clientId: 'foo', + clientSecret: 'bar', + redirectUri: Uri.parse('http://example.com'), + ), + options, + ); + }); + + test('accepts string', () { + expect( + new ExternalAuthOptions( + clientId: 'foo', + clientSecret: 'bar', + redirectUri: 'http://example.com', + ), + options, + ); + }); + + test('rejects invalid redirectUri', () { + expect( + () => new ExternalAuthOptions( + clientId: 'foo', clientSecret: 'bar', redirectUri: 24.5), + throwsArgumentError, + ); + }); + + test('ensures id not null', () { + expect( + () => new ExternalAuthOptions( + clientId: null, + clientSecret: 'bar', + redirectUri: 'http://example.com'), + throwsArgumentError, + ); + }); + + test('ensures secret not null', () { + expect( + () => new ExternalAuthOptions( + clientId: 'foo', + clientSecret: null, + redirectUri: 'http://example.com'), + throwsArgumentError, + ); + }); + }); + + group('fromMap()', () { + test('rejects invalid map', () { + expect( + () => new ExternalAuthOptions.fromMap({'yes': 'no'}), + throwsArgumentError, + ); + }); + + test('accepts correct map', () { + expect( + new ExternalAuthOptions.fromMap({ + 'client_id': 'foo', + 'client_secret': 'bar', + 'redirect_uri': 'http://example.com', + }), + options, + ); + }); + }); + + group('toString()', () { + test('produces correct string', () { + expect( + options.toString(obscureSecret: false), + 'ExternalAuthOptions(clientId=foo, clientSecret=bar, redirectUri=http://example.com)', + ); + }); + + test('obscures secret', () { + expect( + options.toString(), + 'ExternalAuthOptions(clientId=foo, clientSecret=***, redirectUri=http://example.com)', + ); + }); + + test('asteriskCount', () { + expect( + options.toString(asteriskCount: 7), + 'ExternalAuthOptions(clientId=foo, clientSecret=*******, redirectUri=http://example.com)', + ); + }); + }); + + group('toJson()', () { + test('obscures secret', () { + expect( + options.toJson(), + { + 'client_id': 'foo', + 'client_secret': '', + 'redirect_uri': 'http://example.com', + }, + ); + }); + + test('produces correct map', () { + expect( + options.toJson(obscureSecret: false), + { + 'client_id': 'foo', + 'client_secret': 'bar', + 'redirect_uri': 'http://example.com', + }, + ); + }); + }); +}