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!
);
After (Serverpod 3.x) - Recommended approach#
// â
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)#
| Field | Type | Description |
|---|---|---|
userIdentifier |
String (non-nullable) |
Unique user identifier (can be email, UUID, numeric ID, or other formats) |
scopes | Set<Scope> | Access permission scopes |
authId | String | Authentication session ID |
Warning:
- The
userIdfield does not exist. userIdentifiermay not be an email (could be UUID, numeric ID, etc.)-
The
userIdextension inserverpod_auth_serverattempts to parseuserIdentifieras 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 Method | userIdentifier Format | Example |
|---|---|---|
| Email Auth | user@example.com | |
| Google Sign-In | UUID | 550e8400-e29b-41d4-a716-446655440000 |
| Apple Sign-In | UUID | 550e8400-e29b-41d4-a716-446655440000 |
| Legacy Auth | Numeric ID | 12345 |
Therefore getCurrentUserInfo() queries in the following order:
-
Numeric format: If
userIdentifieris numeric, attempt lookup byidcolumn - Other formats: Query directly by
userIdentifiercolumn
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#
| Situation | Log Level | Emoji |
|---|---|---|
| User lookup failure | LogLevel.warning | â ī¸ |
| Successful lookup | LogLevel.info (default) | â |
| Informational message | LogLevel.info (default) | âšī¸ |
| Start/entry log | LogLevel.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();
});
});
}
Related Files#
backend/kobic_server/lib/src/utils/auth.dart- Authentication utilitiesbackend/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#
| Item | Serverpod 2.x | Serverpod 3.x |
|---|---|---|
| User ID | auth.userId | â Removed |
| User Identifier | - | auth.userIdentifier (various formats) |
| Session ID | - | auth.authId |
| Permission Scopes | auth.scopes | auth.scopes |
| UserInfo Lookup | Direct lookup by id |
Use getCurrentUserInfo() recommended |
Checklist#
When writing new endpoints:
- Use
getCurrentUserInfo()instead ofauth.userId - Do not assume
userIdentifieris an email - Add appropriate logging for UserInfo lookup failures
- Check
isClosedbefore emit (BLoC pattern)