Authentication, Authorization, and Process Safety
mORMot implements a comprehensive security architecture through three complementary layers: process safety, authentication, and authorization. This chapter covers the security mechanisms built into the framework.
┌─────────────────────────────────────────────────────────────────┐
│ Security Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Process │ │ Authentication│ │ Authorization │ │
│ │ Safety │ │ (Who?) │ │ (What?) │ │
│ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
│ │ • Encryption │ │ • Sessions │ │ • Per-table │ │
│ │ • ACID DB │ │ • Signatures │ │ • Per-service │ │
│ │ • Stateless │ │ • SSPI/Kerb │ │ • Per-method │ │
│ │ • Type safety │ │ • JWT │ │ • Groups │ │
│ │ • Testing │ │ • HTTP Basic │ │ • Access bits │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Principle | Implementation |
|---|---|
| Defense in depth | Multiple security layers |
| Least privilege | Group-based access rights |
| Fail secure | Reject by default |
| No security by obscurity | Published algorithms |
| Session management | Server-side session tracking |
mORMot provides process safety at every architectural level:
Encryption:
Authentication confirms user identity. mORMot supports multiple authentication schemes:
| Scheme | Class | Security | Use Case |
|---|---|---|---|
| mORMot Default | TRestServerAuthenticationDefault |
★★★★ | Delphi clients |
| SSPI/Kerberos | TRestServerAuthenticationSspi |
★★★★ | Windows domain |
| HTTP Basic | TRestServerAuthenticationHttpBasic |
★★ | Browser/legacy |
| None | TRestServerAuthenticationNone |
★ | Testing only |
| JWT | Via JwtForUnauthenticatedRequest |
★★★ | Public APIs |
uses
mormot.rest.sqlite3,
mormot.rest.server;
var
Server: TRestServerDB;
begin
// Create server WITH authentication enabled
Server := TRestServerDB.Create(Model, 'data.db3', True); // aHandleUserAuthentication = True
try
Server.CreateMissingTables; // Creates AuthUser/AuthGroup tables
// Server now requires authentication
finally
Server.Free;
end;
end;
The True parameter enables:
TAuthUser and TAuthGroup tablesTRestServerAuthentication (abstract)
├── TRestServerAuthenticationSignedUri
│ ├── TRestServerAuthenticationDefault → mORMot secure challenge
│ └── TRestServerAuthenticationSspi → Windows SSPI/Kerberos │
├── TRestServerAuthenticationNone → Weak (username only)
└── TRestServerAuthenticationHttpAbstract
└── TRestServerAuthenticationHttpBasic → HTTP Basic (Base64)
The default authentication uses a secure two-pass challenge:
Client Server
│ │
│ GET /auth?UserName=John │
├────────────────────────────────────────►│
│ │
│ {"result":"<hex_nonce>"} │
│◄────────────────────────────────────────┤
│ │
│ GET /auth?UserName=John& │
│ Password=<computed>& │
│ ClientNonce=<random> │
├────────────────────────────────────────►│
│ │
│ {"result":"SessionID+PrivateKey", │
│ "logonname":"John"} │
│◄────────────────────────────────────────┤
│ │
│ All requests now include: │
│ ?session_signature=XXXX │
├────────────────────────────────────────►│
The password sent is computed as:
Password = SHA256(ModelRoot + ServerNonce + ClientNonce + UserName +
SHA256('salt' + PlainPassword))
This ensures:
Each authenticated request includes a signature:
session_signature = Hexa8(SessionID) +
Hexa8(Timestamp) +
Hexa8(CRC32(SessionID + PrivateKey +
SHA256('salt' + Password) +
Timestamp + URL))
Example URL:
root/Customer/123?session_signature=0000004C000F6DD02E24541C
^^^^^^^^ ^^^^^^^^ ^^^^^^^^
SessionID Timestamp Signature
uses
mormot.rest.http.client;
var
Client: TRestHttpClientWinHTTP;
begin
Client := TRestHttpClientWinHTTP.Create('localhost', '8080', Model);
try
// Authenticate with username/password
if not Client.SetUser('Admin', 'synopse') then
raise Exception.Create('Authentication failed');
// All subsequent requests are signed automatically
Client.Orm.Retrieve(ID, Customer);
finally
Client.Free;
end;
end;
// Configure signature algorithm (server-side)
(Server.AuthenticationRegister(TRestServerAuthenticationDefault) as
TRestServerAuthenticationSignedUri).Algorithm := suaSHA256;
Available algorithms:
| Algorithm | Speed | Security | Notes |
|---|---|---|---|
suaCRC32 |
★★★★ | ★★ | Default, fast |
suaMD5 |
★★★ | ★★ | Legacy |
suaSHA256 |
★★ | ★★★★ | Recommended for high security |
suaSHA512 |
★ | ★★★★ | Highest security |
suaSHA3 |
★★ | ★★★★ | Modern |
// For AJAX clients with network latency
(Server.AuthenticationRegister(TRestServerAuthenticationDefault) as
TRestServerAuthenticationSignedUri).NoTimestampCoherencyCheck := true;
// Or adjust tolerance (default: 5 seconds)
(Server.AuthenticationRegister(TRestServerAuthenticationDefault) as
TRestServerAuthenticationSignedUri).TimestampCoherencySeconds := 10;
SSPI allows using Windows credentials without entering username/password:
uses
mormot.rest.http.client;
var
Client: TRestHttpClientWinHTTP;
begin
Client := TRestHttpClientWinHTTP.Create('server', '8080', Model);
try
// Empty username = use Windows credentials
Client.SetUser('', ''); // NTLM authentication
// Or with Kerberos SPN
Client.SetUser('', 'mymormotservice/myserver.mydomain.tld');
finally
Client.Free;
end;
end;
For Kerberos authentication, register an SPN:
rem Register SPN for service running under SYSTEM account
setspn -a mymormotservice/myserver.mydomain.tld myserver
rem Register SPN for service running under domain account
setspn -a mymormotservice/myserver.mydomain.tld myserviceaccount
SSPI-authenticated users must exist in TAuthUser:
// User.LogonName must match 'DOMAIN\Username'
User := TAuthUser.Create;
try
User.LogonName := 'MYDOMAIN\JohnDoe';
User.DisplayName := 'John Doe';
User.GroupRights := TAuthGroup(AdminGroupID);
Server.Orm.Add(User, true);
finally
User.Free;
end;
For browser clients or legacy systems:
// Server: Register HTTP Basic
Server.AuthenticationRegister(TRestServerAuthenticationHttpBasic);
// Client: Use HTTP Basic
TRestServerAuthenticationHttpBasic.ClientSetUser(Client, 'User', 'password');
Warning: HTTP Basic sends credentials as Base64 (not encrypted). Always use HTTPS!
For authentication without creating a mORMot session:
// Client-side only, for proxy authentication
TRestServerAuthenticationHttpBasic.ClientSetUserHttpOnly(
Client, 'proxyUser', 'proxyPass');
JWT provides stateless authentication for public APIs:
uses
mormot.crypt.jwt,
mormot.rest.server;
var
Server: TRestServerDB;
JwtEngine: TJwtHS256;
begin
Server := TRestServerDB.Create(Model, 'data.db3', False); // No session auth
// Configure JWT validation
JwtEngine := TJwtHS256.Create(
'my-secret-key-at-least-32-bytes!', // Secret
60000, // Clock tolerance ms
[jrcIssuer, jrcExpirationTime], // Required claims
[], // Audience (optional)
60 // Expiration minutes
);
Server.JwtForUnauthenticatedRequest := JwtEngine; // Server owns it
// Optionally restrict to specific IPs
Server.JwtForUnauthenticatedRequestWhiteIP := '192.168.1.0/24';
end;
// Obtain JWT from your authentication service
var Token: RawUtf8 := GetJwtFromAuthService('user', 'pass');
// Set as HTTP header
Client.SessionHttpHeader := AuthorizationBearer(Token);
// All requests now include: Authorization: Bearer <token>
| Class | Algorithm | Key Type |
|---|---|---|
TJwtHS256 |
HMAC-SHA256 | Symmetric |
TJwtHS384 |
HMAC-SHA384 | Symmetric |
TJwtHS512 |
HMAC-SHA512 | Symmetric |
TJwtES256 |
ECDSA P-256 | Asymmetric |
TJwtRS256 |
RSA-SHA256 | Asymmetric |
TJwtPS256 |
RSA-PSS-SHA256 | Asymmetric |
TAuthGroup = class(TOrm)
published
property Ident: RawUtf8; // Group name ('Admin', 'User', etc.)
property SessionTimeout: integer; // Session timeout in minutes
property AccessRights: RawUtf8; // CSV-encoded TOrmAccessRights
end;
TAuthUser = class(TOrm)
published
property LogonName: RawUtf8; // Login identifier
property DisplayName: RawUtf8; // Display name
property PasswordHashHexa: RawUtf8; // SHA-256 hash of password
property GroupRights: TAuthGroup; // Associated group
property Data: RawBlob; // Custom application data
end;
When authentication is enabled, these groups are created automatically:
| Group | POST SQL | SELECT SQL | Auth R | Auth W | Tables R | Tables W | Services |
|---|---|---|---|---|---|---|---|
| Admin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Supervisor | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| User | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Guest | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
synopse. Change immediately in production!
var
User: TAuthUser;
Group: TAuthGroup;
begin
// Find or create group
Group := TAuthGroup.Create;
try
if not Server.Orm.Retrieve('Ident=?', [], ['CustomGroup'], Group) then
begin
Group.Ident := 'CustomGroup';
Group.SessionTimeout := 30;
Group.OrmAccessRights := SUPERVISOR_ACCESS_RIGHTS;
Server.Orm.Add(Group, true);
end;
// Create user
User := TAuthUser.Create;
try
User.LogonName := 'newuser';
User.DisplayName := 'New User';
User.SetPasswordPlain('secure_password');
User.GroupRights := TAuthGroup(Group.ID);
Server.Orm.Add(User, true);
finally
User.Free;
end;
finally
Group.Free;
end;
end;
// Simple SHA-256 (mORMot 1 compatible)
User.SetPasswordPlain('password'); // Uses SHA256('salt' + password)
// PBKDF2-HMAC-SHA256 (more secure)
User.SetPassword('password', 'unique-salt', 10000); // 10000 rounds
Authorization is controlled by TOrmAccessRights:
TOrmAccessRights = record
AllowRemoteExecute: TOrmAllowRemoteExecute; // SQL/service flags
GET: TOrmTableBits; // Read access per table
POST: TOrmTableBits; // Create access per table
PUT: TOrmTableBits; // Update access per table
DELETE: TOrmTableBits; // Delete access per table
end;
TOrmAllowRemoteExecute = set of (
reSQL, // Allow POST with SQL statements
reSQLSelectWithoutTable, // Allow complex SELECT (JOINs)
reService, // Allow interface-based services
reUrlEncodedSQL, // Allow SQL in URL parameters
reUrlEncodedDelete, // Allow DELETE with WHERE clause
reOneSessionPerUser // Enforce single session per user
);
const
// Full access (use only for local/in-process)
FULL_ACCESS_RIGHTS: TOrmAccessRights = (
AllowRemoteExecute: [reSQL, reSQLSelectWithoutTable,
reService, reUrlEncodedSQL,
reUrlEncodedDelete];
GET: ALL_ACCESS_RIGHTS;
POST: ALL_ACCESS_RIGHTS;
PUT: ALL_ACCESS_RIGHTS;
DELETE: ALL_ACCESS_RIGHTS;
);
// Admin access (remote, with SQL)
ADMIN_ACCESS_RIGHTS: TOrmAccessRights = (
AllowRemoteExecute: [reSQL, reSQLSelectWithoutTable, reService];
// ...
);
// Supervisor access (remote, SELECT only)
SUPERVISOR_ACCESS_RIGHTS: TOrmAccessRights = (
AllowRemoteExecute: [reSQLSelectWithoutTable, reService];
// ...
);
var
Rights: TOrmAccessRights;
begin
// Start with no access
FillChar(Rights, SizeOf(Rights), 0);
Rights.AllowRemoteExecute := [reService];
// Grant full CRUD on Customer table
Rights.Edit(Model, TCustomer, True, True, True, True); // C, R, U, D
// Grant read-only on Order table
Rights.Edit(Model, TOrder, False, True, False, False); // R only
// Apply to group
Group.OrmAccessRights := Rights;
end;
For interface-based services:
// Disable authentication for specific service
Server.ServiceDefine(TMyPublicService, [IMyPublicService], sicShared)
.ByPassAuthentication := True;
// Or restrict to specific groups
Server.ServiceDefine(TMyAdminService, [IMyAdminService], sicShared)
.AllowedGroups := [1]; // Group ID 1 only (Admin)
For method-based services:
// Bypass authentication for specific method
Server.ServiceMethodByPassAuthentication('Timestamp');
Server.ServiceMethodByPassAuthentication('Auth');
Sessions are stored in-memory as TAuthSession instances:
TAuthSession = class(TSynPersistent)
property ID: cardinal; // Session identifier
property User: TAuthUser; // Associated user (loaded)
property TimeOutTix: cardinal; // Expiration tick
property RemoteIP: RawUtf8; // Client IP address
property ConnectionID: TRestConnectionID;
end;
┌──────────────────────────────────────────────────────────────┐
│ Session Lifecycle │
├──────────────────────────────────────────────────────────────┤
│ │
│ Client.SetUser() ──► Auth Request ──► Session Created │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ │ │ In-Memory │ │
│ │ │ TAuthSession│ │
│ │ └─────────────┘ │
│ │ │ │
│ │ ┌─────────┴─────────┐ │
│ │ ▼ ▼ │
│ │ Session Timeout Explicit Close│
│ │ │ │ │
│ │ └─────────┬─────────┘ │
│ │ ▼ │
│ │ Session Destroyed │
│ │ │
└──────────────────────────────────────────────────────────────┘
Configure via TAuthGroup.SessionTimeout:
// Set 30-minute timeout for a group
Group.SessionTimeout := 30;
Server.Orm.Update(Group);
Sessions can be persisted for server restarts:
// Save sessions before shutdown
Server.Shutdown('sessions.bin');
// Restore sessions after restart
Server.SessionsLoadFromFile('sessions.bin');
Note: This works for ORM sessions only, not SOA with stateful services.
// Server-side: Get current session user
var
User: TAuthUser;
begin
User := Server.SessionGetUser(Ctxt.SessionID);
if User <> nil then
Log('Request from: %s', [User.DisplayName]);
end;
// In service implementation
procedure TMyService.DoSomething;
var
Ctxt: TRestServerUriContext;
begin
Ctxt := ServiceRunningContext;
if Ctxt <> nil then
Log('Session ID: %d, User: %s',
[Ctxt.SessionID, Ctxt.SessionUser.LogonName]);
end;
// Always use HTTPS in production
HttpServer := TRestHttpServer.Create(
'443',
[Server],
'+',
useHttpAsync,
secTLS // Enable TLS
);
// Configure certificate
HttpServer.TLS.CertificateFile := 'server.crt';
HttpServer.TLS.PrivateKeyFile := 'server.key';
// ❌ Never store plain passwords
User.PasswordHashHexa := 'plaintext';
// ✓ Always hash passwords
User.SetPasswordPlain('password'); // SHA-256
// ✓ Or use PBKDF2 for better security
User.SetPassword('password', RandomString(16), 10000);
// ❌ Dangerous: SQL injection possible
Server.Orm.ExecuteFmt('SELECT * FROM Customer WHERE Name = ''%s''', [UserInput]);
// ✓ Safe: Use parameterized queries
Server.Orm.Retrieve('Name = ?', [], [UserInput], Customer);
// ❌ Don't give all users admin rights
User.GroupRights := TAuthGroup(AdminGroupID);
// ✓ Create specific groups with minimal rights
User.GroupRights := TAuthGroup(ReadOnlyGroupID);
// Enable detailed logging
Server.OnAfterUri := procedure(Ctxt: TRestServerUriContext)
begin
if Ctxt.SessionUser <> nil then
Log('%s: %s %s from %s', [
DateTimeToIso8601(Now, True),
Ctxt.SessionUser.LogonName,
Ctxt.Uri,
Ctxt.RemoteIP
]);
end;
type
TRestServerAuthenticationCustom = class(TRestServerAuthentication)
public
function Auth(Ctxt: TRestServerUriContext;
const aUserName: RawUtf8): boolean; override;
function RetrieveSession(
Ctxt: TRestServerUriContext): TAuthSession; override;
end;
function TRestServerAuthenticationCustom.Auth(
Ctxt: TRestServerUriContext;
const aUserName: RawUtf8): boolean;
var
User: TAuthUser;
Token: RawUtf8;
begin
Result := False;
Token := Ctxt.InputUtf8['token'];
// Validate token with your external service
if not ValidateExternalToken(Token, aUserName) then
Exit;
// Create session
User := GetUser(Ctxt, aUserName);
if User <> nil then
begin
SessionCreate(Ctxt, User);
Result := True;
end;
end;
Server.AuthenticationRegister(TRestServerAuthenticationCustom);
// Override user retrieval from external source
Server.OnAuthenticationUserRetrieve :=
function(Sender: TRestServerAuthentication;
Ctxt: TRestServerUriContext;
const aUserName: RawUtf8): TAuthUser;
begin
Result := TAuthUser.Create;
Result.LogonName := aUserName;
Result.ID := GetUserIdFromLDAP(aUserName);
Result.GroupRights := TAuthGroup(GetGroupFromLDAP(aUserName));
end;
| Need | Solution |
|---|---|
| Delphi clients | TRestServerAuthenticationDefault |
| Windows domain | TRestServerAuthenticationSspi |
| Browser/REST API | TRestServerAuthenticationHttpBasic + HTTPS |
| Public stateless API | JWT via JwtForUnauthenticatedRequest |
| Development/testing | TRestServerAuthenticationNone |
| Need | Property |
|---|---|
| Per-table CRUD | TOrmAccessRights.GET/POST/PUT/DELETE |
| SQL execution | AllowRemoteExecute flags |
| Service access | ByPassAuthentication, AllowedGroups |
| Group management | TAuthGroup.AccessRights |
| Unit | Purpose |
|---|---|
mormot.rest.server |
Authentication classes, sessions |
mormot.rest.core |
TAuthUser, TAuthGroup |
mormot.orm.core |
TOrmAccessRights |
mormot.crypt.jwt |
JWT classes |
mormot.crypt.secure |
Cryptographic primitives |
Next: Chapter 22 covers the Scripting Engine (if applicable to your mORMot2 version).
| Previous | Index | Next |
|---|---|---|
| Chapter 20: Hosting and Deployment | Index | Chapter 22: Scripting Engine |