├── LICENSE ├── README.md ├── android └── app │ └── build.gradle ├── lib └── main.dart └── pubspec.yaml /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Carmine Zaccagnino 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter app for a forthcoming [carmine.dev](https://carmine.dev) blog post about authentication and JWT authorization with Flutter and Node 2 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.jwt_example_app" 42 | minSdkVersion 18 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:http/http.dart' as http; 3 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 4 | import 'dart:convert' show json, base64, ascii; 5 | 6 | const SERVER_IP = 'http://192.168.1.167:5000'; 7 | final storage = FlutterSecureStorage(); 8 | 9 | void main() { 10 | runApp(MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | Future get jwtOrEmpty async { 15 | var jwt = await storage.read(key: "jwt"); 16 | if(jwt == null) return ""; 17 | return jwt; 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MaterialApp( 23 | title: 'Authentication Demo', 24 | theme: ThemeData( 25 | primarySwatch: Colors.blue, 26 | ), 27 | home: FutureBuilder( 28 | future: jwtOrEmpty, 29 | builder: (context, snapshot) { 30 | if(!snapshot.hasData) return CircularProgressIndicator(); 31 | if(snapshot.data != "") { 32 | var str = snapshot.data; 33 | var jwt = str.split("."); 34 | 35 | if(jwt.length !=3) { 36 | return LoginPage(); 37 | } else { 38 | var payload = json.decode(ascii.decode(base64.decode(base64.normalize(jwt[1])))); 39 | if(DateTime.fromMillisecondsSinceEpoch(payload["exp"]*1000).isAfter(DateTime.now())) { 40 | return HomePage(str, payload); 41 | } else { 42 | return LoginPage(); 43 | } 44 | } 45 | } else { 46 | return LoginPage(); 47 | } 48 | } 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class LoginPage extends StatelessWidget { 55 | final TextEditingController _usernameController = TextEditingController(); 56 | final TextEditingController _passwordController = TextEditingController(); 57 | 58 | void displayDialog(context, title, text) => showDialog( 59 | context: context, 60 | builder: (context) => 61 | AlertDialog( 62 | title: Text(title), 63 | content: Text(text) 64 | ), 65 | ); 66 | 67 | Future attemptLogIn(String username, String password) async { 68 | var res = await http.post( 69 | "$SERVER_IP/login", 70 | body: { 71 | "username": username, 72 | "password": password 73 | } 74 | ); 75 | if(res.statusCode == 200) return res.body; 76 | return null; 77 | } 78 | 79 | Future attemptSignUp(String username, String password) async { 80 | var res = await http.post( 81 | '$SERVER_IP/signup', 82 | body: { 83 | "username": username, 84 | "password": password 85 | } 86 | ); 87 | return res.statusCode; 88 | 89 | } 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | return Scaffold( 94 | appBar: AppBar(title: Text("Log In"),), 95 | body: Padding( 96 | padding: const EdgeInsets.all(8.0), 97 | child: Column( 98 | children: [ 99 | TextField( 100 | controller: _usernameController, 101 | decoration: InputDecoration( 102 | labelText: 'Username' 103 | ), 104 | ), 105 | TextField( 106 | controller: _passwordController, 107 | obscureText: true, 108 | decoration: InputDecoration( 109 | labelText: 'Password' 110 | ), 111 | ), 112 | FlatButton( 113 | onPressed: () async { 114 | var username = _usernameController.text; 115 | var password = _passwordController.text; 116 | var jwt = await attemptLogIn(username, password); 117 | if(jwt != null) { 118 | storage.write(key: "jwt", value: jwt); 119 | Navigator.push( 120 | context, 121 | MaterialPageRoute( 122 | builder: (context) => HomePage.fromBase64(jwt) 123 | ) 124 | ); 125 | } else { 126 | displayDialog(context, "An Error Occurred", "No account was found matching that username and password"); 127 | } 128 | }, 129 | child: Text("Log In") 130 | ), 131 | FlatButton( 132 | onPressed: () async { 133 | var username = _usernameController.text; 134 | var password = _passwordController.text; 135 | 136 | if(username.length < 4) 137 | displayDialog(context, "Invalid Username", "The username should be at least 4 characters long"); 138 | else if(password.length < 4) 139 | displayDialog(context, "Invalid Password", "The password should be at least 4 characters long"); 140 | else{ 141 | var res = await attemptSignUp(username, password); 142 | if(res == 201) 143 | displayDialog(context, "Success", "The user was created. Log in now."); 144 | else if(res == 409) 145 | displayDialog(context, "That username is already registered", "Please try to sign up using another username or log in if you already have an account."); 146 | else { 147 | displayDialog(context, "Error", "An unknown error occurred."); 148 | } 149 | } 150 | }, 151 | child: Text("Sign Up") 152 | ) 153 | ], 154 | ), 155 | ) 156 | ); 157 | } 158 | } 159 | 160 | class HomePage extends StatelessWidget { 161 | HomePage(this.jwt, this.payload); 162 | 163 | factory HomePage.fromBase64(String jwt) => 164 | HomePage( 165 | jwt, 166 | json.decode( 167 | ascii.decode( 168 | base64.decode(base64.normalize(jwt.split(".")[1])) 169 | ) 170 | ) 171 | ); 172 | 173 | final String jwt; 174 | final Map payload; 175 | 176 | @override 177 | Widget build(BuildContext context) => 178 | Scaffold( 179 | appBar: AppBar(title: Text("Secret Data Screen")), 180 | body: Center( 181 | child: FutureBuilder( 182 | future: http.read('$SERVER_IP/data', headers: {"Authorization": jwt}), 183 | builder: (context, snapshot) => 184 | snapshot.hasData ? 185 | Column(children: [ 186 | Text("${payload['username']}, here's the data:"), 187 | Text(snapshot.data, style: Theme.of(context).textTheme.display1) 188 | ],) 189 | : 190 | snapshot.hasError ? Text("An error occurred") : CircularProgressIndicator() 191 | ), 192 | ), 193 | ); 194 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: jwt_example_app 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=2.1.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | http: ^0.12.0+4 12 | flutter_secure_storage: ^3.3.1+1 13 | cupertino_icons: ^0.1.2 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | uses-material-design: true --------------------------------------------------------------------------------