LogoSkills

Serverpod 3.x Authentication Rules

The authentication API has changed in Serverpod 3.x. This document describes the correct usage.

The authentication API has changed in Serverpod 3.x. This document describes the correct usage.

Official docs: https://docs.serverpod.dev/concepts/authentication/working-with-users

Critical Change: AuthenticationInfo API#

Before (Serverpod 2.x) - No longer supported#

// ❌ WRONG - userId field removed in Serverpod 3.x
final userId = session.authenticated?.userId;

return UserInfo.db.findFirstRow(
  session,
  where: (t) => t.id.equals(auth.userId),  // Compile error!
);
// ✅ CORRECT - Use userIdentifier
final userIdentifier = session.authenticated?.userIdentifier;

return UserInfo.db.findFirstRow(
  session,
  where: (table) => table.userIdentifier.equals(auth.userIdentifier),
);

AuthenticationInfo Structure (Serverpod 3.x)#

FieldTypeDescription
userIdentifier String (non-nullable) Unique user identifier (can be email, UUID, numeric ID, or other formats)
scopesSet<Scope>Access permission scopes
authIdStringAuthentication session ID

Warning:

  • The userId field does not exist.
  • userIdentifier may not be an email (could be UUID, numeric ID, etc.)
  • The userId extension in serverpod_auth_server attempts to parse userIdentifier as int → causes FormatException for UUID formats

Correct Patterns#

getCurrentUserInfo() Utility#

// backend/kobic_server/lib/src/utils/auth.dart
Future<UserInfo?> getCurrentUserInfo(Session session) async {
  final auth = session.authenticated;
  if (auth == null) return null;

  // Serverpod 3.x: Query directly with userIdentifier (do not use auth.userId)
  final userIdentifier = auth.userIdentifier;

  // If userIdentifier is numeric, attempt lookup by id
  // (some auth methods may store a numeric ID in userIdentifier)
  final numericId = int.tryParse(userIdentifier);
  if (numericId != null) {
    final userById = await UserInfo.db.findFirstRow(
      session,
      where: (table) => table.id.equals(numericId),
    );
    if (userById != null) return userById;
  }

  // Query by userIdentifier (email, UUID, etc.)
  return UserInfo.db.findFirstRow(
    session,
    where: (table) => table.userIdentifier.equals(userIdentifier),
  );
}

Dual-Strategy Lookup Explanation#

userIdentifier can be in various formats depending on the auth method:

Auth MethoduserIdentifier FormatExample
Email AuthEmailuser@example.com
Google Sign-InUUID550e8400-e29b-41d4-a716-446655440000
Apple Sign-InUUID550e8400-e29b-41d4-a716-446655440000
Legacy AuthNumeric ID12345

Therefore getCurrentUserInfo() queries in the following order:

  1. Numeric format: If userIdentifier is numeric, attempt lookup by id column
  2. Other formats: Query directly by userIdentifier column

Usage in Endpoints#

class MyEndpoint extends Endpoint {
  @override
  bool get requireLogin => true;

  Future<User?> getMe(Session session) async {
    final userInfo = await getCurrentUserInfo(session);
    if (userInfo == null) {
      session.log(
        'âš ī¸ UserInfo not found - '
        'authId: ${session.authenticated?.authId}, '
        'userIdentifier: ${session.authenticated?.userIdentifier}',
        level: LogLevel.warning,
      );
      return null;
    }

    return User.db.findFirstRow(
      session,
      where: (table) => table.userInfoId.equals(userInfo.id),
    );
  }
}

auth.userId Extension Caution#

The userId extension in the serverpod_auth_server package internally works as follows:

// serverpod_auth_server internal implementation
extension AuthenticationInfoUserIdExtension on AuthenticationInfo {
  int get userId => int.parse(userIdentifier); // FormatException risk!
}

Warning: If userIdentifier is in UUID format, FormatException is thrown!

// ❌ DANGEROUS - Throws exception for UUID formats
final userId = session.authenticated!.userId; // FormatException!

// ✅ SAFE - Use getCurrentUserInfo
final userInfo = await getCurrentUserInfo(session);

Logging Rules#

SituationLog LevelEmoji
User lookup failureLogLevel.warningâš ī¸
Successful lookupLogLevel.info (default)✅
Informational messageLogLevel.info (default)â„šī¸
Start/entry logLogLevel.info (default)🚀
// âš ī¸ Warning log example
session.log(
  'âš ī¸ [EndpointName] UserInfo not found',
  level: LogLevel.warning,
);

Test Patterns#

Writing Integration Tests#

import 'test_tools/serverpod_test_tools.dart';

void main() {
  withServerpod('Given auth utils', (sessionBuilder, endpoints) {
    test('should return UserInfo for authenticated session', () async {
      final session = await sessionBuilder.build();

      // Create test UserInfo
      final userInfo = await UserInfo.db.insertRow(
        session,
        UserInfo(
          userIdentifier: 'test@example.com',
          created: DateTime.now(),
          scopeNames: [],
          blocked: false,
        ),
      );

      // Set authentication info
      await session.updateAuthenticated(
        AuthenticationInfo(
          userInfo.userIdentifier,
          <Scope>{},
          authId: 'test-auth-id',
        ),
      );

      // When: Call getCurrentUserInfo
      final result = await getCurrentUserInfo(session);

      // Then: Returns UserInfo
      expect(result, isNotNull);
      expect(result!.userIdentifier, equals('test@example.com'));

      // Cleanup required!
      await UserInfo.db.deleteRow(session, userInfo);
      await session.close();
    });
  });
}
  • backend/kobic_server/lib/src/utils/auth.dart - Authentication utilities
  • backend/kobic_server/test/integration/auth_utils_test.dart - Integration tests

Bug Case Study#

Sample Book Request History Loading Failure (PR #2007)#

Symptom: Only skeleton displayed, data not loaded

Cause: getCurrentUserInfo() used auth.userId extension → FormatException thrown for UUID format

Fix: Changed to dual-strategy lookup based on userIdentifier

Impact: 18 files, 51 call sites

Quick Reference#

ItemServerpod 2.xServerpod 3.x
User IDauth.userId❌ Removed
User Identifier-auth.userIdentifier (various formats)
Session ID-auth.authId
Permission Scopesauth.scopesauth.scopes
UserInfo Lookup Direct lookup by id Use getCurrentUserInfo() recommended

Checklist#

When writing new endpoints:

  • Use getCurrentUserInfo() instead of auth.userId
  • Do not assume userIdentifier is an email
  • Add appropriate logging for UserInfo lookup failures
  • Check isClosed before emit (BLoC pattern)