Authentication

Authentication in Flint is built around the static Auth facade, AuthConfig, FlintJwt, request/session helpers, and the database helpers used by the auth tables. A real application normally wraps those primitives in its own controller and service layer so it can control response shape, cookies, mail delivery, user-profile fields, and business rules.

Before coding auth in an app, inspect these files:

  • lib/routes/auth_routes.dart
  • lib/controllers/auth_controller.dart
  • lib/services/auth.dart
  • lib/middlewares/auth_middleware.dart
  • lib/models/user.dart
  • lib/mail/otp_mail.dart
  • lib/mail/views/otp.flint.html
  • lib/config/table_registry.dart
  • .env

If the app has local docs generated by flint agent, read Mail before changing any email or OTP delivery code.

Framework Source References

Framework code to inspect when behavior is unclear:

  • lib/src/auth/auth.dart
  • lib/src/auth/auth_config.dart
  • lib/src/auth/auth_service.dart
  • lib/src/extensions/auth_verify.dart
  • lib/src/security/jwt.dart
  • lib/src/request.dart
  • lib/src/response.dart
  • lib/src/mail/

Generated apps should keep their own app name, support links, response wording, and policy names.

Framework Pieces

Use these Flint exports for auth code:

CodeBlock dart
import 'package:flint_dart/auth.dart';
import 'package:flint_dart/flint_dart.dart';
import 'package:flint_dart/mail.dart';

The main framework APIs are:

  • Auth.register(...)
  • Auth.login(...)
  • Auth.loginWithTokens(...)
  • Auth.generateToken(...)
  • Auth.verifyToken(...)
  • Auth.generateEmailVerificationToken(...)
  • Auth.verifyEmail(...)
  • Auth.generateNumericVerificationCode(...)
  • Auth.verifyNumericCode(...)
  • Auth.resendVerificationCode(...)
  • Auth.generatePasswordResetCode(...)
  • Auth.verifyPasswordResetCode(...)
  • Auth.resetPasswordWithCode(...)
  • Auth.resendPasswordResetCode(...)
  • Auth.refreshAccessToken(...)
  • Auth.revokeRefreshToken(...)
  • Auth.revokeAllRefreshTokensForUser(...)
  • Auth.ensureFrameworkTablesExist()
  • TotpService.generateSecret()
  • TotpService.buildOtpAuthUrl(...)
  • TotpService.verifyCode(...)

Request.user can read a bearer token or Flint auth cookie and return the decoded JWT payload. Many apps add their own typed request extension, such as req.authUser, that verifies the token and loads the full User model from the database.

Environment

Auth reads configuration from .env through FlintEnv:

CodeBlock text
AUTH_TABLE=users
AUTH_EMAIL_COLUMN=email
AUTH_PASSWORD_COLUMN=password
AUTH_NAME_COLUMN=name
AUTH_PROVIDER_COLUMN=provider
AUTH_PROVIDER_ID_COLUMN=provider_id
JWT_SECRET=replace-with-a-long-secret
JWT_EXPIRY_HOURS=24
AUTH_ACCESS_TOKEN_MINUTES=1440
AUTH_ENABLE_REFRESH_TOKENS=false
AUTH_REFRESH_TOKEN_DAYS=30
AUTH_ENABLE_LOGIN_THROTTLE=false
AUTH_LOGIN_MAX_ATTEMPTS=5
AUTH_LOGIN_LOCK_MINUTES=15
PASSWORD_MIN_LENGTH=6
REQUIRE_EMAIL_VERIFICATION=false
REDIRECT_BASE=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY=

In production, always set a long non-default JWT_SECRET. Flint rejects the default or weak secret when issuing or verifying JWTs.

REQUIREEMAILVERIFICATION=true only works with the framework's built-in snakecase emailverifiedat field check. If your app stores email verification as emailVerified or emailVerifiedAt, keep REQUIREEMAIL_VERIFICATION=false and enforce verification in your own service/controller.

User Model

The framework needs an auth table with at least an email column and password column. Apps usually add profile, role, and security fields.

CodeBlock dart
class User extends Model<User> {
  User() : super(() => User());

  String get email => getAttribute('email') ?? '';
  String? get password => getAttribute('password');
  String get role => getAttribute('role') ?? 'USER';
  bool get emailVerified => getAttribute('emailVerified') ?? false;
  DateTime? get emailVerifiedAt => getAttribute<DateTime>('emailVerifiedAt');
  bool get twoFactorEnabled => getAttribute('twoFactorEnabled') ?? false;
  String? get twoFactorMethod => getAttribute('twoFactorMethod');
  String? get twoFactorSecret => getAttribute('twoFactorSecret');

  @override
  Table get table => Table(
        name: 'users',
        columns: [
          Column(name: 'email', type: ColumnType.string, isUnique: true),
          Column(name: 'password', type: ColumnType.string, isNullable: true),
          Column(name: 'role', type: ColumnType.string, defaultValue: 'USER'),
          Column(
            name: 'emailVerified',
            type: ColumnType.boolean,
            defaultValue: false,
          ),
          Column(
            name: 'emailVerifiedAt',
            type: ColumnType.datetime,
            isNullable: true,
          ),
          Column(
            name: 'twoFactorEnabled',
            type: ColumnType.boolean,
            defaultValue: false,
          ),
          Column(
            name: 'twoFactorMethod',
            type: ColumnType.string,
            isNullable: true,
          ),
          Column(
            name: 'twoFactorSecret',
            type: ColumnType.string,
            isNullable: true,
          ),
        ],
      );
}

Register the user table in lib/config/table_registry.dart before running migrations.

Framework Auth Tables

Call Auth.ensureFrameworkTablesExist() directly or indirectly through the auth helper methods before using verification or reset codes. Flint creates:

  • emailverificationtokens
  • passwordresettokens
  • authrefreshtokens

The code tables store hashed tokens, not the plain OTP. The plain code is returned only once so the app can send it by email or SMS.

Do not add a separate otps table unless your app needs a custom OTP domain. The built-in numeric OTP helpers already use the framework tables.

Register

The framework registration helper checks for an existing user, validates password length, hashes the password, inserts only columns that exist on the configured table, and returns sanitized user data.

CodeBlock dart
final user = await Auth.register(
  email: email,
  password: password,
  additionalData: {
    'firstName': firstName,
    'lastName': lastName,
    'provider': 'email',
  },
);

For email verification, registration normally continues by generating and sending an OTP:

CodeBlock dart
final otp = await Auth.generateNumericVerificationCode(email);

await OTPVerificationMail(
  recipientName: firstName,
  recipientEmail: email,
  otp: otp,
  purpose: OTPPurpose.emailVerification,
).send();

The controller should validate input, check password confirmation, handle existing unverified accounts by resending the code, and set a short-lived pending-email cookie if the browser needs it.

CodeBlock dart
Future<Response> register() async {
  final data = await req.validate({
    'firstName': 'required|string',
    'lastName': 'required|string',
    'email': 'required|email',
    'password': 'required|string|min:6',
    'confirmPassword': 'required|string',
  });

  if (data['password'] != data['confirmPassword']) {
    return res.status(422).json({
      'errors': {
        'confirmPassword': ['Passwords do not match.'],
      },
    });
  }

  final email = data['email'].toString().trim().toLowerCase();
  final existingUser = await User().where('email', email).first();

  if (existingUser != null && !existingUser.emailVerified) {
    await AuthBusiness.resendOtp(
      email: email,
      purpose: OTPPurpose.emailVerification,
    );
    return res.status(409).json({
      'message': 'Email already exists but is not verified.',
      'actionRequired': true,
      'emailVerificationRequired': true,
    });
  }

  if (existingUser != null) {
    return res.status(409).json({
      'message': 'Email already registered. Please log in.',
      'code': 'email_exists',
    });
  }

  await AuthBusiness.registerUser(
    firstName: data['firstName'],
    lastName: data['lastName'],
    email: email,
    password: data['password'],
  );

  res.setCookie(
    'pending_email',
    email,
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    path: '/',
    maxAge: 10 * 60,
  );

  return res.status(201).json({
    'message': 'Account created. Please check your email for OTP.',
  });
}

Login

Auth.login(email, password) is the simple framework path. It returns sanitized user data and a JWT.

CodeBlock dart
final result = await Auth.login(email, password);
return res.json({
  'user': result['user'],
  'token': result['token'],
});

Production apps often need more business rules than the generic helper: unverified email, account created through an OAuth provider, empty password, 2FA, trusted login IPs, login alert email, cart merge, audit logging, and cookie sessions. Put that orchestration in an auth service instead of the route file.

CodeBlock dart
class AuthResult {
  AuthResult({
    required this.user,
    required this.token,
    this.actionRequired = false,
    this.emailVerificationRequired = false,
    this.twoFactorRequired = false,
    this.passwordSetupRequired = false,
    this.newDeviceVerificationRequired = false,
    this.twoFactorMethod,
  });

  final User user;
  final String token;
  final bool actionRequired;
  final bool emailVerificationRequired;
  final bool twoFactorRequired;
  final bool passwordSetupRequired;
  final bool newDeviceVerificationRequired;
  final String? twoFactorMethod;
}

A service login flow usually follows this order:

  1. Load the user by email.
  2. Reject unknown users without leaking extra state.
  3. If the user has no local password, send a password-setup OTP.
  4. Verify the password with Hashing().verify(...).
  5. If the app requires email verification and the user is not verified, send an email-verification OTP.
  6. If 2FA is enabled, send an email code or require an authenticator code.
  7. If a new login IP/device must be verified, send a new-device OTP.
  8. Issue a JWT only after all required checks pass.
  9. Run successful-login side effects such as audit logs, cart merge, trusted IP update, and login-alert mail.

If the app uses cookie sessions or stores JWTs in cookies, read Sessions And Cookies before implementing login or logout. JWT auth, server FLINTSESSID sessions, and Flint UI browser authSession storage are different layers and should be cleared intentionally on logout.

CodeBlock dart
static Future<AuthResult> login({
  required String email,
  required String? password,
  required Map<String, String> cookies,
  String? clientIp,
  String? userAgent,
}) async {
  final user = await User().where('email', email).first();
  if (user == null) throw Exception('Invalid credentials');

  final hasPassword = (user.password ?? '').isNotEmpty;
  if (!hasPassword) {
    final otp = await Auth.generateNumericVerificationCode(email);
    await OTPVerificationMail(
      recipientName: user.name,
      recipientEmail: email,
      otp: otp,
      purpose: OTPPurpose.passwordReset,
    ).send();
    return AuthResult(
      user: user,
      token: '',
      actionRequired: true,
      passwordSetupRequired: true,
    );
  }

  if (password == null || !Hashing().verify(password, user.password!)) {
    throw Exception('Invalid credentials');
  }

  if (!user.emailVerified) {
    final otp = await Auth.generateNumericVerificationCode(email);
    await OTPVerificationMail(
      recipientName: user.name,
      recipientEmail: email,
      otp: otp,
      purpose: OTPPurpose.emailVerification,
    ).send();
    return AuthResult(
      user: user,
      token: '',
      actionRequired: true,
      emailVerificationRequired: true,
    );
  }

  if (user.twoFactorEnabled) {
    if (user.twoFactorMethod == 'email') {
      final otp = await Auth.generateNumericVerificationCode(email);
      await OTPVerificationMail(
        recipientName: user.name,
        recipientEmail: email,
        otp: otp,
        purpose: OTPPurpose.twoFactorLogin,
      ).send();
    }
    return AuthResult(
      user: user,
      token: '',
      actionRequired: true,
      twoFactorRequired: true,
      twoFactorMethod: user.twoFactorMethod ?? 'authenticator',
    );
  }

  final token = FlintJwt(FlintEnv.get('JWT_SECRET')).generateToken({
    'id': user.id,
    'email': user.email,
    'role': user.role,
    'isAdmin': user.role == 'ADMIN',
  }, expiry: Duration(hours: Auth.config.jwtExpiryHours));

  return AuthResult(user: user, token: token);
}

The controller turns AuthResult into HTTP responses. If an action is required, clear any old auth cookie before returning the challenge.

CodeBlock dart
if (result.emailVerificationRequired) {
  res.clearCookie('auth_token', path: '/');
  res.setCookie(
    'pending_email',
    result.user.email,
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    path: '/',
    maxAge: 10 * 60,
  );

  return res.status(403).json({
    'message': 'Email verification required',
    'actionRequired': true,
    'emailVerificationRequired': true,
  });
}

res.setCookie(
  'auth_token',
  result.token,
  httpOnly: true,
  secure: true,
  sameSite: 'Lax',
  path: '/',
  maxAge: 60 * 60 * Auth.config.jwtExpiryHours,
);

return res.json({
  'id': result.user.id,
  'email': result.user.email,
  'role': result.user.role,
});

Send OTP

For app-owned email OTPs, define a purpose enum so subject, message, and security copy stay consistent.

CodeBlock dart
enum OTPPurpose {
  emailVerification,
  passwordReset,
  resend,
  twoFactorLogin,
  twoFactorSetup,
  newDeviceLoginVerification,
}

Use ViewMailable for the actual email:

CodeBlock dart
class OTPVerificationMail extends ViewMailable {
  OTPVerificationMail({
    required this.recipientName,
    required this.recipientEmail,
    required this.otp,
    required this.purpose,
    this.expiresInMinutes = 10,
  });

  final String recipientName;
  final String recipientEmail;
  final String otp;
  final OTPPurpose purpose;
  final int expiresInMinutes;

  @override
  String get subject {
    switch (purpose) {
      case OTPPurpose.emailVerification:
        return 'Verify your email address';
      case OTPPurpose.passwordReset:
        return 'Reset your password';
      case OTPPurpose.resend:
        return 'Your new verification code';
      case OTPPurpose.twoFactorLogin:
        return 'Your two-factor login code';
      case OTPPurpose.twoFactorSetup:
        return 'Confirm two-factor setup';
      case OTPPurpose.newDeviceLoginVerification:
        return 'Verify new device login';
    }
  }

  @override
  String get view => 'mail/views/otp.flint.html';

  @override
  Map<String, dynamic> get data => {
        'recipientName': recipientName,
        'otp': otp,
        'expiresInMinutes': expiresInMinutes,
        'title': subject,
        'message': _message,
        'securityNotice': _securityNotice,
        'currentYear': DateTime.now().year,
      };

  @override
  List<String> get to => [recipientEmail];

  String get _message => switch (purpose) {
        OTPPurpose.emailVerification =>
          'Use the code below to verify your email address.',
        OTPPurpose.passwordReset =>
          'Use the code below to reset your password.',
        OTPPurpose.resend => 'Here is your new verification code.',
        OTPPurpose.twoFactorLogin =>
          'Use the code below to complete your sign in.',
        OTPPurpose.twoFactorSetup =>
          'Use this code to enable email-based two-factor authentication.',
        OTPPurpose.newDeviceLoginVerification =>
          'Use this code to confirm the login was yours.',
      };

  String get _securityNotice => switch (purpose) {
        OTPPurpose.passwordReset =>
          'If you did not request a password reset, secure your account.',
        OTPPurpose.twoFactorLogin ||
        OTPPurpose.newDeviceLoginVerification =>
          'If this login was not you, change your password.',
        OTPPurpose.twoFactorSetup =>
          'If you did not request 2FA setup, contact support.',
        OTPPurpose.emailVerification ||
        OTPPurpose.resend =>
          'If you did not request this code, you can ignore this email.',
      };
}

Template:

CodeBlock html
<h2>Hello {{ recipientName }}</h2>
<p>{{ message }}</p>
<div style="font-size: 28px; letter-spacing: 4px; font-weight: bold;">
  {{ otp }}
</div>
<p>This code expires in {{ expiresInMinutes }} minutes.</p>
<p>{{ securityNotice }}</p>

The {{ ... }} markers are Flint template variables. Read Mail for variables, nested values, filters, conditionals, loops, includes, and template safety rules.

Do not store the plain OTP in your own user table. Let Auth.generateNumericVerificationCode(...) store the hash and expiry. Do not log the returned OTP.

Verify OTP

For email verification, call Auth.verifyNumericCode(email, otp). It checks emailverificationtokens, validates the hashed code, ensures the record is not expired, deletes the used code, and updates emailverifiedat if that column exists.

If the app uses custom verification fields, update them after the framework check succeeds:

CodeBlock dart
Future<Response> verify() async {
  final body = await req.validate({
    'email': 'required|email',
    'otp': 'required|min:6|max:6',
  });

  final email = body['email'].toString().trim().toLowerCase();
  final verified = await Auth.verifyNumericCode(email, body['otp'].toString());

  if (!verified) {
    return res.status(400).json({
      'success': false,
      'message': 'Invalid or expired OTP',
    });
  }

  final user = await User().where('email', email).first();
  if (user == null) {
    return res.status(404).json({
      'success': false,
      'message': 'User not found',
    });
  }

  final verifiedAt = DateTime.now().toIso8601String();
  await user.update(data: {
    'emailVerified': true,
    'emailVerifiedAt': verifiedAt,
  });

  final token = FlintJwt(FlintEnv.get('JWT_SECRET')).generateToken({
    'id': user.id,
    'email': user.email,
    'role': user.role,
    'isAdmin': user.role == 'ADMIN',
  }, expiry: Duration(hours: Auth.config.jwtExpiryHours));

  res.setCookie(
    'auth_token',
    token,
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    path: '/',
    maxAge: 60 * 60 * Auth.config.jwtExpiryHours,
  );
  res.clearCookie('pending_email', path: '/');

  return res.json({
    'success': true,
    'message': 'Email verified successfully',
    'token': token,
    'user': {
      'id': user.id,
      'email': user.email,
      'emailVerified': true,
      'emailVerifiedAt': verifiedAt,
    },
  });
}

Resend OTP

Use resend for unverified signup, ordinary email verification, password setup, new-device verification, or password reset. Choose the purpose from request data, then generate a fresh code and send the matching email.

CodeBlock dart
Future<Response> resendOtp() async {
  final data = await req.validate({
    'email': 'required|email',
    'reset': 'bool',
    'reason': 'string',
  });

  final email = data['email'].toString().trim().toLowerCase();
  final reset = data['reset'] == true;
  final reason = (data['reason'] ?? '').toString().trim().toLowerCase();

  var purpose = reset ? OTPPurpose.passwordReset : OTPPurpose.resend;
  if (reason == 'new-device') {
    purpose = OTPPurpose.newDeviceLoginVerification;
  } else if (reason == 'signup' || reason == 'email-verification') {
    purpose = OTPPurpose.emailVerification;
  }

  await AuthBusiness.resendOtp(email: email, purpose: purpose);

  return res.json({
    'message': 'OTP resent successfully',
    'email': email,
    'reason': reason.isEmpty ? null : reason,
  });
}

For email verification resends, use Auth.resendVerificationCode(email). For password reset resends, use Auth.resendPasswordResetCode(email) so the code lands in the right framework table.

Forgot Password And Reset

There are two supported styles:

  • Link/token flow: Auth.generatePasswordResetToken(email) and Auth.resetPassword(token: ..., newPassword: ...).
  • Numeric OTP flow: Auth.generatePasswordResetCode(email), Auth.verifyPasswordResetCode(...), and Auth.resetPasswordWithCode(...).

Use the numeric OTP flow when the UI asks the user to enter a six-digit code.

CodeBlock dart
Future<Response> forgotPassword() async {
  final data = await req.validate({'email': 'required|email'});
  final email = data['email'].toString().trim().toLowerCase();

  final code = await Auth.generatePasswordResetCode(email);
  final user = await User().where('email', email).first();

  await OTPVerificationMail(
    recipientName: user?.name ?? 'there',
    recipientEmail: email,
    otp: code,
    purpose: OTPPurpose.passwordReset,
  ).send();

  return res.json({'message': 'OTP sent to your email'});
}

Verify before allowing the user to set a new password:

CodeBlock dart
Future<Response> verifyReset() async {
  final data = await req.validate({
    'email': 'required|email',
    'otp': 'required|min:6|max:6',
  });

  final ok = await Auth.verifyPasswordResetCode(
    email: data['email'].toString().trim().toLowerCase(),
    code: data['otp'].toString(),
  );

  if (!ok) {
    return res.status(400).json({'message': 'Invalid or expired OTP'});
  }

  final resetToken = FlintJwt(FlintEnv.get('JWT_SECRET')).generateToken({
    'email': data['email'],
    'type': 'password_reset',
  }, expiry: const Duration(minutes: 15));

  res.setCookie(
    'reset_token',
    resetToken,
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    path: '/',
    maxAge: 15 * 60,
  );

  return res.json({'success': true, 'message': 'Email verified successfully'});
}

Then update the password and clear the reset cookie:

CodeBlock dart
Future<Response> resetPassword() async {
  final data = await req.validate({'newPassword': 'required|string|min:6'});
  final resetToken = req.cookies['reset_token'];
  if (resetToken == null) {
    return res.status(401).json({'message': 'Reset token missing'});
  }

  final payload = FlintJwt(FlintEnv.get('JWT_SECRET')).verifyToken(resetToken);
  if (payload == null || payload['email'] == null) {
    return res.status(401).json({'message': 'Invalid or expired reset token'});
  }

  final user = await User().where('email', payload['email']).first();
  if (user == null) {
    return res.status(404).json({'message': 'User not found'});
  }

  user.setAttribute('password', Hashing().hash(data['newPassword']));
  await user.save();
  res.clearCookie('reset_token', path: '/');

  return res.json({'success': true, 'message': 'Password reset successfully'});
}

If you do not need a separate verify-reset step, Auth.resetPasswordWithCode(...) can verify the code and update the password in one call. It deletes the used reset code after success.

Auth Middleware

Use middleware to protect routes. Framework Request.user returns a JWT/session payload. Apps that need the full model should add a request extension that verifies the token and fetches the user.

CodeBlock dart
extension AuthHelper on Request {
  Future<User?> get authUser async {
    final token = cookies['auth_token'] ?? bearerToken;
    if (token == null || token.isEmpty) return null;

    final payload = Auth.verifyToken(token);
    if (payload == null || payload['id'] == null) return null;

    set('user', payload);
    return User().find(payload['id']);
  }
}

Middleware:

CodeBlock dart
class AuthMiddleware extends Middleware {
  @override
  Handler handle(Handler next) {
    return (Context ctx) async {
      final res = ctx.res;
      if (res == null) return next(ctx);

      final user = await ctx.req.authUser;
      if (user == null) {
        return res.status(401).json({'message': 'Unauthorized'});
      }

      return next(ctx);
    };
  }
}

Attach it at the route level:

CodeBlock dart
final auth = app.controller(AuthController.new);

auth.get('/me', (controller) => controller.me())
    .useMiddleware(AuthMiddleware());

Two-Factor Authentication

Flint includes a TotpService for authenticator-app 2FA:

CodeBlock dart
final secret = TotpService.generateSecret();
final url = TotpService.buildOtpAuthUrl(
  secret: secret,
  email: user.email,
  issuer: 'My App',
);

Store the secret on the user while setup is pending, then confirm:

CodeBlock dart
final ok = TotpService.verifyCode(secret: user.twoFactorSecret!, code: code);
if (ok) {
  await user.update(data: {
    'twoFactorEnabled': true,
    'twoFactorMethod': 'authenticator',
  });
}

For email-based 2FA, use Auth.generateNumericVerificationCode(user.email) and Auth.verifyNumericCode(user.email, code). During login, issue a short-lived twofatoken cookie or bearer challenge token. Only issue the real authtoken after the second factor succeeds.

Refresh Tokens

Use refresh tokens for long-lived clients:

CodeBlock dart
final result = await Auth.loginWithTokens(
  email,
  password,
  ipAddress: req.headers['x-forwarded-for'],
  userAgent: req.headers['user-agent'],
  deviceName: 'browser',
);

When AUTHENABLEREFRESHTOKENS=true, Flint stores only a hash in authrefresh_tokens. Auth.refreshAccessToken(...) validates the refresh token, issues a new access token, and rotates the refresh token by default.

CodeBlock dart
final refreshed = await Auth.refreshAccessToken(
  refreshToken,
  ipAddress: req.headers['x-forwarded-for'],
  userAgent: req.headers['user-agent'],
);

Revoke on logout or account compromise:

CodeBlock dart
await Auth.revokeRefreshToken(refreshToken);
await Auth.revokeAllRefreshTokensForUser(user.id);

OAuth

Flint supports provider login helpers for Google, GitHub, Facebook, and Apple. Build redirects through Auth.providerRedirectUrl(...) or AuthService helpers, then exchange the callback code with the matching provider method.

CodeBlock dart
final url = Auth.providerRedirectUrl(
  provider: 'google',
  redirectPath: '/api/auth/google/callback',
  state: '/dashboard',
);

return res.redirect(url);

OAuth users are usually marked verified because the provider already owns the email-verification step. Keep that update in app code so it matches the app's user columns.

Routes

A complete auth route group usually looks like this:

CodeBlock dart
class AuthRoutes extends RouteGroup {
  @override
  String get prefix => '/auth';

  @override
  String get tag => 'Auth';

  @override
  void register(Flint app) {
    final auth = app.controller(AuthController.new);
    final authBurst = AuthRateLimitMiddleware.burst();
    final authSensitive = AuthRateLimitMiddleware.sensitive();

    auth.post('/register', (controller) => controller.register())
        .useMiddleware(authSensitive);
    auth.post('/login', (controller) => controller.login())
        .useMiddleware(authBurst);
    auth.post('/forgot-password', (controller) => controller.forgotPassword())
        .useMiddleware(authSensitive);
    auth.post('/resend', (controller) => controller.resendOtp())
        .useMiddleware(authSensitive);
    auth.post('/verify', (controller) => controller.verify())
        .useMiddleware(authSensitive);
    auth.post('/verify-reset', (controller) => controller.verifyReset())
        .useMiddleware(authSensitive);
    auth.post('/reset-password', (controller) => controller.resetPassword())
        .useMiddleware(authSensitive);
    auth.post('/logout', (controller) => controller.logout())
        .useMiddleware(AuthMiddleware());
    auth.get('/me', (controller) => controller.me())
        .useMiddleware(AuthMiddleware());
    auth.post('/2fa/setup', (controller) => controller.twoFactorSetup())
        .useMiddleware(AuthMiddleware());
    auth.post('/2fa/confirm', (controller) => controller.twoFactorConfirm())
        .useMiddleware(AuthMiddleware());
    auth.post('/2fa/verify', (controller) => controller.twoFactorVerify())
        .useMiddleware(authSensitive);
  }
}

AuthRateLimitMiddleware is an app-specific middleware in this example, not a built-in Flint class. Flint does not currently expose a built-in RateLimitMiddleware. Keep rate limiting on auth endpoints that create, verify, or resend codes; use Security And Utilities for rate-limit guidance.

Implementation Checklist

When working on auth:

  1. Read Authentication, then Mail, Middleware, Security And Utilities, and Validation.
  2. Inspect the app's auth routes, controller, service, middleware, user model, and OTP mail class.
  3. Confirm whether the app uses framework emailverifiedat or custom fields like emailVerified.
  4. Use Auth.generateNumericVerificationCode(...) for email verification, email 2FA, and new-device login codes.
  5. Use Auth.generatePasswordResetCode(...) for password reset OTPs.
  6. Send the returned plain code immediately through mail or SMS; never store or log it.
  7. Verify codes with the matching framework method and update app-specific user fields after success.
  8. Delete or clear pending cookies after successful verification.
  9. Add or update Swagger comments on auth routes before running flint --docs-generate.
  10. Run focused tests or dart analyze.

Common Mistakes

  • Do not store plain OTP values.
  • Do not use Auth.resendVerificationCode(...) for password reset OTPs; use Auth.resendPasswordResetCode(...).
  • Do not issue the final auth JWT before email verification or 2FA is complete.
  • Do not leave a previous auth_token cookie active while returning an OTP challenge.
  • Do not assume req.isAuthenticated calls req.user; it only checks cached request storage.
  • Do not rely on REQUIREEMAILVERIFICATION if the app does not use emailverifiedat.
  • Do not fail login only because a non-critical login-alert email failed.
  • Do not send customer emails during impersonation, test mode, or disabled-mail windows unless the app explicitly allows it.
Flint Dart logo
Flint EcosystemThe Unified Dart Technology Stack

One language powering Full-Stack Web, Cross-Platform Clients, Native AI, and Connected Robotics.

Fullstack
Client SDK
AI Engine
Hardware
Copyright 2026 Flint Dart. Maintained by Eulogia Technologies.
v 1.4.0
MIT License
Built with Dart