diff --git a/simple_auth/lib/simple_auth.dart b/simple_auth/lib/simple_auth.dart index 4ab4519..0181827 100644 --- a/simple_auth/lib/simple_auth.dart +++ b/simple_auth/lib/simple_auth.dart @@ -19,6 +19,8 @@ export 'src/oauth/oauthApi.dart'; export 'src/oauth/oauthApiKeyApi.dart'; export 'src/oauth/oauthAuthenticator.dart'; export 'src/oauth/oauthResponse.dart'; +export 'src/oauth/oauthPasswordApi.dart'; +export 'src/oauth/oauthPasswordAuthenticator.dart'; export 'src/providers/amazon.dart'; export 'src/providers/azureAD.dart'; export 'src/providers/azureADV2.dart'; diff --git a/simple_auth/lib/src/annotations.dart b/simple_auth/lib/src/annotations.dart index 0a5942c..6c47aad 100755 --- a/simple_auth/lib/src/annotations.dart +++ b/simple_auth/lib/src/annotations.dart @@ -72,6 +72,19 @@ class OAuthApiDeclaration extends ApiDeclaration { : super(name, baseUrl: baseUrl); } +@immutable +class OAuthPasswordApiDeclaration extends ApiDeclaration { + final String clientId; + final String clientSecret; + final String tokenUrl; + final String loginUrl; + const OAuthPasswordApiDeclaration(String name, this.clientId, + this.clientSecret, this.loginUrl , this.tokenUrl, + {String baseUrl = "/"}) + : super(name, baseUrl: baseUrl); +} + + @immutable class AmazonApiDeclaration extends ApiDeclaration { final String clientId; diff --git a/simple_auth/lib/src/oauth/oauthPasswordApi.dart b/simple_auth/lib/src/oauth/oauthPasswordApi.dart new file mode 100644 index 0000000..edead1a --- /dev/null +++ b/simple_auth/lib/src/oauth/oauthPasswordApi.dart @@ -0,0 +1,96 @@ +import "dart:async"; +import "package:simple_auth/simple_auth.dart"; +import "package:http/http.dart" as http; +import "dart:convert" as convert; + +typedef void ShowOauthPasswordAuthenticator( + OauthPasswordAuthenticator authenticator); + +class OAuthPasswordApi extends OAuthApi { + String loginUrl; + String tokenUrl; + OauthPasswordAuthenticator currentAuthenticator; + static ShowOauthPasswordAuthenticator sharedShowAuthenticator; + + + OAuthPasswordApi(String identifier, + this.loginUrl, + this.tokenUrl, + String clientId, + String clientSecret, + {List scopes, + http.Client client, + Converter converter, + AuthStorage authStorage}) + : super.fromIdAndSecret(identifier, clientId, clientSecret, + client: client, + scopes: scopes, + converter: converter, + authStorage: authStorage) { + this.scopesRequired = false; + } + + OAuthAccount get currentOauthAccount => currentAccount as OAuthAccount; + + + @override + Future performAuthenticate() async { + if (scopesRequired && (scopes?.length ?? 0) == 0) { + throw Exception("Scopes are required"); + } + OAuthAccount account = + currentOauthAccount ?? await loadAccountFromCache(); + if (account != null && + ((account.refreshToken?.isNotEmpty ?? false) || + (account.expiresIn != null && account.expiresIn <= 0))) { + var valid = account.isValid(); + if (!valid || forceRefresh ?? false) { + //If there is no interent, give them the current expired account + if (!await pingUrl(tokenUrl)) { + return account; + } + if (await refreshAccount(account)) + account = currentOauthAccount ?? loadAccountFromCache(); + } + if (account.isValid()) { + saveAccountToCache(account); + currentAccount = account; + return account; + } + } + + var _authenticator = getAuthenticator(); + await _authenticator.resetAuthenticator(); + if (sharedShowAuthenticator != null) + sharedShowAuthenticator(_authenticator); + else + throw new Exception( + "You are required to implement the 'showAuthenticator or sharedShowAuthenticator"); + var token = await _authenticator.getAuthCode(); + if (token?.isEmpty ?? true) { + throw new Exception("Null Token"); + } + account = await getAccountFromAuthCode(_authenticator); + saveAccountToCache(account); + currentAccount = account; + return account; + } + + + @override + OauthPasswordAuthenticator getAuthenticator() => OauthPasswordAuthenticator(identifier, clientId, clientSecret,loginUrl , tokenUrl, baseUrl, redirectUrl, scopes); + + + @override + Future getAccountFromAuthCode( + WebAuthenticator authenticator) async { + var auth = authenticator as OauthPasswordAuthenticator; + return OAuthAccount(identifier, + created: DateTime.now().toUtc(), + expiresIn: auth.token.expiresIn, + refreshToken: auth.token.refreshToken, + scope: authenticator.scope ?? List(), + tokenType: auth.token.tokenType, + token: auth.token.accessToken); + } +} diff --git a/simple_auth/lib/src/oauth/oauthPasswordAuthenticator.dart b/simple_auth/lib/src/oauth/oauthPasswordAuthenticator.dart new file mode 100644 index 0000000..a67d7a3 --- /dev/null +++ b/simple_auth/lib/src/oauth/oauthPasswordAuthenticator.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import "package:simple_auth/simple_auth.dart"; +import "package:http/http.dart" as http; +import "dart:async"; + +class OauthPasswordAuthenticator extends OAuthAuthenticator { + String loginUrl; + AuthTokenClass token; + + OauthPasswordAuthenticator(String identifier, String clientId, String clientSecret, + this.loginUrl, tokenUrl, String baseUrl, String redirectUrl, List scopes) + : super(identifier, clientId, clientSecret, tokenUrl, baseUrl, + redirectUrl) {} + + Future verifyCredentials(String username, String password) async { + try { + if (username?.isEmpty ?? true) throw new Exception("Invalid Username"); + if (password?.isEmpty ?? true) throw new Exception("Invalid Password"); + + Map body = { + 'username': username, + 'password': password, + 'grant_type': "password" + }; + + var headers = {'Content-Type' : 'application/x-www-form-urlencoded'}; + var req = await http.post(loginUrl, body: body , headers: headers,encoding: Encoding.getByName("utf-8")); + + var success = req.statusCode >= 200 && req.statusCode < 300; + if (!success) return false; + + + var token = new AuthTokenClass.fromJson(json.decode(req.body)); + if(token.accessToken?.isNotEmpty ?? false) { + this.token = token; + foundAuthCode(token.accessToken); + return true; + } + return false; + + } catch (e) { + return false; + } + } + + + ///Gets the data that will be posted to swap the auth code for an auth token + Future> getTokenPostData(String clientSecret) async { + var data = { + "grant_type": "password", + "client_id": clientId, + "client_secret": clientSecret + }; + return data; + } +} + +class AuthTokenClass { + String accessToken; + String refreshToken; + String tokenType; + int expiresIn; + + AuthTokenClass({this.accessToken,this.refreshToken,this.tokenType,this.expiresIn}); + + factory AuthTokenClass.fromJson(Map json) { + return AuthTokenClass( + accessToken : json['access_token'], + refreshToken : json['refresh_token'], + tokenType: json['token_type'], + expiresIn: json['expires_in'], + ); + } +} diff --git a/simple_auth_flutter/lib/oauth_password_login_page.dart b/simple_auth_flutter/lib/oauth_password_login_page.dart new file mode 100644 index 0000000..50f7e60 --- /dev/null +++ b/simple_auth_flutter/lib/oauth_password_login_page.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:simple_auth/simple_auth.dart'; + +class OauthPasswordLoginPage extends StatefulWidget { + static String defaultLogo; + final OauthPasswordAuthenticator authenticator; + OauthPasswordLoginPage(this.authenticator); + static String tag = 'grant-password-login-page'; + @override + _LoginPageState createState() => new _LoginPageState(); +} + +class _LoginPageState extends State { + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + @override + Widget build(BuildContext context) { + final logo = Hero( + tag: 'hero', + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 48.0, + child: (OauthPasswordLoginPage.defaultLogo?.isEmpty ?? true) + ? Icon(Icons.supervised_user_circle) + : Image.asset(OauthPasswordLoginPage.defaultLogo), + ), + ); + + final email = TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + autofocus: true, + decoration: InputDecoration( + hintText: 'Email', + contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), + ), + ); + + final password = TextFormField( + controller: passwordController, + autofocus: false, + obscureText: true, + decoration: InputDecoration( + hintText: 'Password', + contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), + ), + ); + + final loginButton = Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Material( + borderRadius: BorderRadius.circular(30.0), + shadowColor: Colors.lightBlueAccent.shade100, + elevation: 5.0, + child: MaterialButton( + minWidth: 200.0, + height: 42.0, + onPressed: () async { + try { + bool success = await widget.authenticator.verifyCredentials( + emailController.text, passwordController.text); + if (success) Navigator.pop(context); + } catch (ex) { + var alert = + new AlertDialog(content: new Text(ex), actions: [ + new FlatButton( + child: const Text("Ok"), + onPressed: () { + Navigator.pop(context); + }) + ]); + showDialog( + context: context, builder: (BuildContext context) => alert); + } + }, + color: Colors.lightBlueAccent, + child: Text('Log In', style: TextStyle(color: Colors.white)), + ), + ), + ); + + return Scaffold( + appBar: new AppBar( + title: Text(widget.authenticator.title), + actions: widget.authenticator.allowsCancel + ? [ + new IconButton( + icon: new Icon(Icons.cancel), + onPressed: () { + widget.authenticator.cancel(); + Navigator.pop(context); + }, + ) + ] + : null, + ), + backgroundColor: Colors.white, + body: Center( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + logo, + SizedBox(height: 48.0), + email, + SizedBox(height: 8.0), + password, + SizedBox(height: 24.0), + loginButton, + ], + ), + ), + ); + } + + @override + void dispose() { + widget.authenticator.cancel(); + super.dispose(); + } +} diff --git a/simple_auth_flutter_example/lib/main.dart b/simple_auth_flutter_example/lib/main.dart index 7c09673..0331970 100644 --- a/simple_auth_flutter_example/lib/main.dart +++ b/simple_auth_flutter_example/lib/main.dart @@ -94,6 +94,15 @@ class _MyHomePageState extends State { final simpleAuth.BasicAuthApi basicApi = new simpleAuth.BasicAuthApi( "github-basic", "https://api.github.com/user"); + + final simpleAuth.OAuthPasswordApi oauthPasswordApi = new simpleAuth + .OAuthPasswordApi( + "oauth-password", + "loginUrl", + "tokenUrl", + "clientId", + "clientSecret"); + final simpleAuth.InstagramApi instagramApi = new simpleAuth.InstagramApi( "instagram", "clientId", "clientSecret", "redirecturl"); @@ -311,6 +320,32 @@ class _MyHomePageState extends State { showMessage("Logged out"); }, ), + ListTile( + title: Text( + "Oauth Grant Password", + style: Theme.of(context).textTheme.headline, + ), + ), + ListTile( + leading: Icon(Icons.launch), + title: Text('Login'), + onTap: () async { + try { + var success = await oauthPasswordApi.authenticate(); + showMessage("Logged in success: $success"); + } catch (e) { + showError(e); + } + }, + ), + ListTile( + leading: Icon(Icons.delete), + title: Text('Logout'), + onTap: () async { + await oauthPasswordApi.logOut(); + showMessage("Logged out"); + }, + ), ListTile( title: Text( "Instagram OAuth", diff --git a/simple_auth_generator/lib/src/generator.dart b/simple_auth_generator/lib/src/generator.dart index 00d153f..52bc3e3 100644 --- a/simple_auth_generator/lib/src/generator.dart +++ b/simple_auth_generator/lib/src/generator.dart @@ -202,6 +202,8 @@ class SimpleAuthGenerator return "${simple_auth.OAuthApi}"; case BuiltInAnnotations.oAuthApiKeyApiDeclaration: return "${simple_auth.OAuthApiKeyApi}"; + case BuiltInAnnotations.oAuthPasswordApiDeclaration: + return "${simple_auth.OAuthPasswordApi}"; default: return "${simple_auth.Api}"; } @@ -418,6 +420,29 @@ class SimpleAuthGenerator ..body = new Code(body), ); } + case BuiltInAnnotations.oAuthPasswordApiDeclaration: + { + return new Constructor( + (b) => b + ..requiredParameters.addAll( + _createParameters(annotation, [BuiltInParameters.identifier])) + ..optionalParameters.addAll(_createParameters(annotation, [ + BuiltInParameters.clientId, + BuiltInParameters.clientSecret, + BuiltInParameters.loginUrl, + BuiltInParameters.tokenUrl, + BuiltInParameters.client, + BuiltInParameters.converter, + BuiltInParameters.authStorage + ])) + ..initializers.addAll([ + const Code( + 'super(identifier, clientId, clientSecret, loginUrl, tokenUrl, client: client, converter: converter,authStorage:authStorage)'), + ]) + ..body = new Code(body), + ); + } + default: return new Constructor( (b) => b @@ -729,6 +754,7 @@ class BuiltInAnnotations { static const String linkedInApiDeclaration = 'LinkedInApiDeclaration'; static const String microsoftLiveDeclaration = 'MicrosoftLiveDeclaration'; static const String oAuthApiDeclaration = 'OAuthApiDeclaration'; + static const String oAuthPasswordApiDeclaration = 'OAuthPasswordApiDeclaration'; static const String oAuthApiKeyApiDeclaration = 'OAuthApiKeyApiDeclaration'; static const String apiKeyDeclaration = 'ApiKeyDeclaration'; static const String basicAuthDeclaration = "BasicAuthDeclaration";