├── .env.example ├── CHANGELOG.md ├── preview.gif ├── .gitignore ├── lib ├── hash.dart ├── config.dart └── auth-provider.dart ├── .vscode ├── settings.json └── launch.json ├── pubspec.yaml ├── analysis_options.yaml ├── README.md ├── LICENSE └── bin └── server.dart /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | JWT_AUTH_SECRET=ASDF_QWERTY_REPLACE_ME_PLZ_THX -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version, created by Stagehand 4 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenreilly/dart-jwt-example/HEAD/preview.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /lib/hash.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:crypto/crypto.dart'; 3 | 4 | abstract class Hash { 5 | 6 | static Utf8Encoder _encoder = const Utf8Encoder(); 7 | 8 | static List _convert(String x) => _encoder.convert(x); 9 | 10 | static String create (String str) => sha256.convert(_convert(str)).toString(); 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dart]": { 3 | "editor.tabSize": 4, 4 | "editor.insertSpaces": false, 5 | "editor.detectIndentation": false, 6 | }, 7 | "dart.closingLabels": false, 8 | "files.exclude": { 9 | "**/.classpath": true, 10 | "**/.project": true, 11 | "**/.settings": true, 12 | "**/.factorypath": true 13 | } 14 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart", 9 | "program": "bin/server.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_jwt_example 2 | description: An example project showing how to authenticate with JWT in Dart 3 | author: Kenneth Reilly <@8_bit_hacker> 4 | homepage: https://github.com/kenreilly/dart-jwt-example 5 | version: 1.0.0 6 | 7 | environment: 8 | sdk: '>=2.1.0 <3.0.0' 9 | 10 | dependencies: 11 | args: ^1.4.2 12 | shelf: ^0.7.2 13 | dotenv: ^1.0.0 14 | jaguar_jwt: ^2.1.6 15 | 16 | dev_dependencies: 17 | pedantic: ^1.0.0 18 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dotenv/dotenv.dart' as dotenv; 3 | 4 | abstract class Config { 5 | 6 | static Map get _env => dotenv.env ?? {}; 7 | 8 | static int get port => int.tryParse(_env['PORT']) ?? 3000; 9 | 10 | static String get secret => _env['JWT_AUTH_SECRET'] ?? ''; 11 | 12 | static init() async { 13 | 14 | String filename = (await File.fromUri(Uri.parse('.env')).exists()) ? '.env' : '.env.example'; 15 | return dotenv.load(filename); 16 | } 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## dart-jwt-example 2 | An example project demonstrating how to implement basic JWT authentication in Dart 3 | 4 | ![](preview.gif) 5 | 6 | ___ 7 | ### Getting Started 8 | * Install Dart, available [here](https://dart.dev) 9 | * Clone this repository 10 | * `cd` into the project directory 11 | * run `pub get` to install packages 12 | 13 | ___ 14 | ### Testing 15 | * start the server with `$ dart bin/server.dart` 16 | * try an API call with `$ curl localhost:3000/hello` (should be rejected) 17 | * authenticate with `$ curl localhost:3000/auth -d '{"username":"test", "password":"insecure"}'` (should return a JWT, copy this) 18 | * retry API call with `$ curl localhost:3000/hello -H "Authorization: Bearer YOUR_TOKEN"` (should return OK) 19 | 20 | ___ 21 | ### Docs 22 | For more information, see [this tutorial](https://itnext.io/authentication-with-jwt-in-dart-6fbc70130806). 23 | 24 | ___ 25 | ### Contributions 26 | Suggestions, ideas, comments, and pull requests are welcome. Thanks! 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kenneth Reilly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:args/args.dart'; 3 | import 'package:shelf/shelf.dart'; 4 | import 'package:shelf/shelf_io.dart' as io; 5 | import 'package:dart_jwt_example/auth-provider.dart'; 6 | import 'package:dart_jwt_example/config.dart'; 7 | 8 | abstract class Server { 9 | 10 | static bool debug; 11 | 12 | static Response _echo(Request request) => 13 | Response.ok('Authorization OK for "${request.url}"'); 14 | 15 | static Future start(List args) async { 16 | 17 | await Config.init(); 18 | 19 | ArgParser parser = ArgParser()..addOption('debug', abbr: 'd', defaultsTo: 'false'); 20 | ArgResults result = parser.parse(args); 21 | 22 | debug = (result['debug'] == 'true'); 23 | if (debug) { stdout.writeln('Debug output is enabled'); } 24 | 25 | Middleware auth = createMiddleware(requestHandler: AuthProvider.handle); 26 | Handler handler = const Pipeline() 27 | .addMiddleware(logRequests()) 28 | .addMiddleware(auth) 29 | .addHandler(_echo); 30 | 31 | HttpServer server = await io.serve(handler, 'localhost', Config.port); 32 | print('Serving at http://${server.address.host}:${server.port}'); 33 | } 34 | } 35 | 36 | main(List args) async => Server.start(args); -------------------------------------------------------------------------------- /lib/auth-provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'package:shelf/shelf.dart'; 4 | import 'package:jaguar_jwt/jaguar_jwt.dart'; 5 | import 'package:dart_jwt_example/hash.dart'; 6 | import 'package:dart_jwt_example/config.dart'; 7 | 8 | abstract class AuthProvider { 9 | 10 | static JsonDecoder _decoder = const JsonDecoder(); 11 | 12 | static List> users = [ 13 | { "username":"test", "password": Hash.create("insecure") }, 14 | { "username":"beep", "password": Hash.create("beepboop") } 15 | ]; 16 | 17 | static bool _check(Map user, Map creds) => 18 | (user['username'] == creds['username'] && user['password'] == creds['password']); 19 | 20 | static FutureOr handle(Request request) async => 21 | (request.url.toString() == 'login') 22 | ? AuthProvider.auth(request) 23 | : AuthProvider.verify(request); 24 | 25 | static FutureOr auth(Request request) async { 26 | 27 | try { 28 | dynamic data = _decoder.convert(await request.readAsString()); 29 | String user = data['username']; 30 | String hash = Hash.create(data['password']); 31 | 32 | Map creds = { "username": user, "password": hash }; 33 | int index = users.indexWhere((user) => _check(user, creds)); 34 | if (index == -1) { throw Exception(); } 35 | 36 | JwtClaim claim = JwtClaim( 37 | subject: user, 38 | issuer: 'ACME Widgets Corp', 39 | audience: ['example.com'], 40 | ); 41 | 42 | String token = issueJwtHS256(claim, Config.secret); 43 | return Response.ok(token); 44 | } 45 | catch (e) { 46 | return Response(401, body: 'Incorrect username/password'); 47 | } 48 | } 49 | 50 | static FutureOr verify(Request request) async { 51 | 52 | try { 53 | String token = request.headers['Authorization'].replaceAll('Bearer ', ''); 54 | JwtClaim claim = verifyJwtHS256Signature(token, Config.secret); 55 | claim.validate(issuer: 'ACME Widgets Corp', audience: 'example.com'); 56 | return null; 57 | } 58 | catch(e) { 59 | return Response.forbidden('Authorization rejected'); 60 | } 61 | } 62 | } --------------------------------------------------------------------------------