DEV Community

Samuel Adekunle
Samuel Adekunle

Posted on • Originally published at techwithsam.dev

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.

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:

dependencies:
  dart_jsonwebtoken: ^latest
  bcrypt: ^latest
Enter fullscreen mode Exit fullscreen mode

User model: lib/src/user.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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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.
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});
}
Enter fullscreen mode Exit fullscreen mode

auth/register.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());
}
Enter fullscreen mode Exit fullscreen mode

Update todos/_middleware.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');
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)