Dart Frog Part 4: Secure Authentication Tutorial (JWT + Password Hashing) đ
full authentication with JWT tokens, bcrypt hashing, and protected routes to our Dart Frog server, enabling a secure TODO project.

Hello, I'm Samuel, also known as Tech With Sam.
I am passionate about learning and teaching programming, particularly Flutter and Dart at the moment. Please support me by subscribing to my newsletter. Thanks!
Subscribe for weekly tutorials and tips, or DM me to bring your app idea to life.
Questions? Join me on Discord: https://discord.gg/8X7dPYujqm For Business: techwithsam10@gmail.com
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
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,
};
}
}
In-memory: lib/src/user_repository.dart (similar to todos, Map<String, User> 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;
}
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});
}
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());
}
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â);
}
};
}
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





