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)