Dart Frog Part 4: Secure Authentication Tutorial (JWT + Password Hashing) πŸ”’

Dart Frog Part 4: Secure Authentication Tutorial (JWT + Password Hashing) πŸ”’
Dart Frog Part 4: Secure Authentication Tutorial (JWT + Password Hashing) πŸ”’

Hey guys! Welcome to Part 4 of our Dart Frog series. If you missed Parts 1, 2, and 3, we set up Dart Frog, built a CRUD API for our Task App, and integrated it on frontend app. Watch it now if you’re new!

Today, we will integrate full authentication with JWT tokens, bcrypt hashing, and protected routes to our Dart Frog server, enabling a secure TODO project.

We’ll add: Register/login endpoints, JWT middleware, protected Todos.

Watch the full tutorial here: https://youtu.be/8VNzuNlourE

Planning & Theory

Flow:

  • Users register β†’ password hashed with bcrypt
  • Login β†’ verify hash β†’ sign JWT
  • Protected endpoints β†’ middleware extracts/verifies bearer token

Packages: dart_jsonwebtoken for JWT, bcrypt for hashing. Store users in-memory (easy swap to Postgres later).

Update pubspec.yaml:

yaml
dependencies:
  dart_jsonwebtoken: ^latest
  bcrypt: ^latest

User model: lib/src/user.dart

dart
///
class User {
  ///
  const User({
    required this.id,
    required this.username,
    required this.hashedPassword,
  });

  /// fromJson
  final String id;

  /// username
  final String username;

  /// hashedPassword
  final String hashedPassword;

  /// toJson
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'username': username,
    };
  }
}

In-memory: lib/src/user_repository.dart (similar to todos, Map<String, User> by email/ID).

dart
import 'package:collection/collection.dart';
import 'package:my_project/src/user_model.dart';
import 'package:uuid/uuid.dart';

const _uuid = Uuid();
final _users = <String, User>{};

/// Find user by username
User? findUserByUsername(String username) {
  return _users.values.firstWhereOrNull((u) => u.username == username);
}

/// Find user by id
User? findUserById(String id) {
  return _users[id];
}

/// Create user
User createUser({required String username, required String passwordHash}) {
  final id = _uuid.v4();
  final user = User(id: id, username: username, hashedPassword: passwordHash);
  _users[id] = user;
  return user;
}
  • Register/Login routes: routes/auth/register.dart & login.dart 
  • Register: Hash with BCrypt.hashpw(password), store user, return 201.
  • Login: Check email β†’ BCrypt.checkpw β†’ generateJwt β†’ return token.
dart
auth/login.dart:
import 'package:bcrypt/bcrypt.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:my_project/src/constant.dart';
import 'package:my_project/src/user_repository.dart';

Future<Response> onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.post) {
    return Response(statusCode: 405, body: 'Method Not Allowed');
  }

  final body = await context.request.json() as Map<String, dynamic>;
  final username = body['username'] as String?;
  final password = body['password'] as String?;

  if (username == null ||
      username.isEmpty ||
      password == null ||
      password.isEmpty) {
    return Response(
      statusCode: 400,
      body: 'Username and password are required.',
    );
  }

  final user = findUserByUsername(username);
  if (user == null || !BCrypt.checkpw(password, user.hashedPassword)) {
    return Response(statusCode: 401, body: 'Invalid username or password.');
  }

  final jwt = JWT({
    'id': user.id,
    'username': user.username,
  });

  final token = jwt.sign(SecretKey(jwtSecret));

  return Response.json(body: {'token': token});
}

auth/register.dart:

dart
import 'package:bcrypt/bcrypt.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:my_project/src/user_repository.dart';

Future<Response> onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.post) {
    return Response(statusCode: 405, body: 'Method Not Allowed');
  }

  final body = await context.request.json() as Map<String, dynamic>;
  final username = body['username'] as String?;
  final password = body['password'] as String?;

  if (username == null ||
      username.isEmpty ||
      password == null ||
      password.isEmpty) {
    return Response(
      statusCode: 400,
      body: 'Username and password are required.',
    );
  }

  if (findUserByUsername(username) != null) {
    return Response(statusCode: 409, body: 'Username already exists.');
  }

  final hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
  final user = createUser(username: username, passwordHash: hashedPassword);

  return Response.json(body: user.toJson());
}

Update todos/_middleware.dart:

dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:my_project/src/constant.dart';
import 'package:my_project/src/user_model.dart';
import 'package:my_project/src/user_repository.dart';

Handler middleware(Handler handler) {
  return handler.use(requestLogger()).use(_authMiddleware).use(_corsMiddleware);
}

Handler _corsMiddleware(Handler handler) {
  return (context) async {
    final response = await handler(context);
    return response.copyWith(
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type',
      },
    );
  };
}

Handler _authMiddleware(Handler handler) {
  return (context) async {
    if (context.request.method == HttpMethod.options) {
      return handler(context);
    }

    final authHeader = context.request.headers['Authorization'];
    if (authHeader == null || !authHeader.startsWith('Bearer ')) {
      return Response(
        statusCode: 401,
        body: 'Missing or invalid Authorization header',
      );
    }

    final token = authHeader.substring(7);

    try {
      final jwt = JWT.verify(token, SecretKey(jwtSecret));
      final payload = jwt.payload as Map<String, dynamic>;
      final userId = payload['id'] as String;

      final user = findUserById(userId);
      if (user == null) {
        return Response(statusCode: 401, body: 'User not found');
      }

      return handler(context.provide<User>(() => user));
    } catch (e) {
      return Response(statusCode: 401, body: 'Invalid token: $e');
    }
  };
}

Apply to protected routes (e.g., wrap todos handler).

Update todos to be per-user (add userId).

Test with Postman: Register β†’ login β†’ use token for CRUD.

Source Code πŸ‘‡β€Š-β€ŠShow some ❀️ by starring ⭐ the repo and follow me πŸ˜„! https://github.com/techwithsam/dart_frog_full_course_tutorial

Fully done with wrapping our Task App with Authentication secured with JWT and Password Hashing! Next part: Deployment with Dart Globe.

Samuel Adekunle, Tech With Sam YouTube

Open for Collaborations

Need help with your app?

I help startups and teams build world-class Flutter applications through architecture audits and high-impact technical guidance.

Get in Touch
Samuel Adekunle

Enjoyed this article?

Get weekly deep-dives on Flutter, Dart Architecture, and Mobile Engineering β€” straight to your inbox.

No spam, ever. Unsubscribe anytime.