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 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
Top comments (0)