Authentication
Extracted from Wikipedia:
Authentication (from Greek: "real" or "genuine", from "author") is the act of confirming the truth of an attribute of a datum or entity. This might involve confirming the identity of a person or software program, tracing the origins of an artifact, or ensuring that a product is what its packaging and labeling claims to be. Authentication often involves verifying the validity of at least one form of identification.
Principles
How to handle authentication in a RESTful Client-Server architecture is a matter of debate.
Commonly, it can be achieved, in the SOA over HTTP world via:
- HTTP basic auth over HTTPS;
- Cookies and session management;
- Query Authentication with additional signature parameters.
We'll have to adapt, or even better mix those techniques, to match our framework architecture at best.
Each authentication scheme has its own PROs and CONs, depending on the purpose of your security policy and software architecture:
| Criteria | HTTPS basic auth | Cookies+Session | Query Auth. |
| Browser integration | Native | Native | Via JavaScript |
| User Interaction | Rude | Custom | Custom |
| Web Service use (rough estimation) |
95% | 4% | 1% |
| Session handling | Yes | Yes | No |
| Session managed by | Client | Server | N/A |
| Password on Server | Yes | Yes/No | N/A |
| Truly Stateless | Yes | No | Yes |
| Truly RESTful | No | No | Yes |
| HTTP-free | No | No | Yes |
HTTP basic auth over HTTPS
This first solution, based on the standard HTTPS protocol, is used by most web services. It's easy to implement, available by default on all browsers, but has some known draw-backs, like the awful authentication window displayed on the Browser, which will persist (there is no LogOut-like feature here), some server-side additional CPU consumption, and the fact that the user-name and password are transmitted (over HTTPS) into the Server (it should be more secure to let the password stay only on the client side, during keyboard entry, and be stored as secure hash on the Server).
The supplied TSQLHttpClientWinHTTP and
TSQLHttpClientWinINet clients classes are able to connect using
HTTPS, and the THttpApiServer server class can send compatible
content.
Session via Cookies
To be honest, a session managed on the Server is not truly Stateless. One possibility could be to maintain all data within the cookie content. And, by design, the cookie is handled on the Server side (Client in fact don’t even try to interpret this cookie data: it just hands it back to the server on each successive request). But this cookie data is application state data, so the client should manage it, not the server, in a pure Stateless world.
The cookie technique itself is HTTP-linked, so it's not truly RESTful, which should be protocol-independent. Since our framework does not provide only HTTP protocol, but offers other ways of transmission, Cookies were left at the baker's home.
Query Authentication
Query Authentication consists in signing each RESTful request via some additional parameters on the URI. See http://broadcast.oreilly.com/2009/12/principles-for-standardized-rest-authentication.html about this technique. It was defined as such in this article:
All REST queries must be authenticated by signing the query parameters sorted in lower-case, alphabetical order using the private credential as the signing token. Signing should occur before URI encoding the query string.
For instance, here is a generic URI sample from the link above:
GET /object?apiKey=Qwerty2010
should be transmitted as such:
GET /object?timestamp=1261496500&apiKey=Qwerty2010&signature=abcdef0123456789
The string being signed is
"/object?apikey=Qwerty2010×tamp=1261496500" and the signature is
the SHA256 hash of that string using the private component of the API
key.
This technique is perhaps the more compatible with a Stateless architecture, and can also been implemented with a light session management.
Server-side data caching is always available. In our framework, we cache the
responses at the SQL level, not at the URI level (thanks to our optimized
implementation of GetJSONObjectAsSQL, the URI to SQL conversion is
very fast). So adding this extra parameter doesn't break the cache
mechanism.
Framework authentication
Even if, theoretically speaking, Query Authentication sounds to be the better for implementing a truly RESTful architecture, our framework tries to implement a Client-Server design.
In practice, we may consider two way of using it:
- With no authentication nor user right management (e.g. for local access of
data, or framework use over a secured network);
- With per-user authentication and right management via defined security
groups, and a per-query authentication.
According to RESTful principle, handling per-session data is not to be implemented in such architecture. A minimal "session-like" feature was introduced only to handle user authentication with very low overhead on both Client and Server side. The main technique used for our security is therefore Query Authentication, i.e. a per-URI signature.
If both AuthGroup and AuthUser are not available
on the Server TSQLModel (i.e. if the
aHandleUserAuthentication parameter was set to false
for the TSQLRestServer. Create constructor), no authentication is
performed. All tables will be accessible by any client, as stated in 19. As
stated above, for security reasons, the ability to execute INSERT /
UPDATE / DELETE SQL statement via a RESTful POST command is
never allowed by default with remote connections: only SELECT can
be executed via this POST verb.
On the Server side, a dedicated service, accessible via the
ModelRoot/Auth URI is to be called to register an User, and create
a session.
If authentication is enabled for the Client-Server process (i.e. if both
AuthGroup and AuthUser are available in the Server
TSQLModel, and the aHandleUserAuthentication
parameter was set to true at the TSQLRestServer
instance construction), the following security features will be added:
- Client should open a session to access to the Server, and provide a
valid UserName / Password pair (see next paragraph);
- Each CRUD statement is checked against the authenticated User security group,
via the AccessRights column and its GET / POST / PUT /
DELETE per-table bit sets;
- Thanks to Per-User authentication, any SQL statement commands may be
available via the RESTful POST verb for an user with its
AccessRights group field containing
AllowRemoteExecute=true;
- Each REST request will expect an additional parameter, named
session_signature, to every URL. Using the URI instead of
cookies allows the signature process to work with all communication
protocols, not only HTTP.
Per-User authentication
On the Server side, two tables, defined by the TSQLAuthGroup
and TSQLAuthUser classes will handle respectively per-group access
rights, and user authentication.
The corresponding AccessRights column is a textual CSV
serialization of the TSQLAccessRights record content, as expected
by the TSQLRestServer.URI method. Using a CSV serialization,
instead of a binary serialization, will allow the change of the
MAX_SQLTABLES constant value.
The AuthUser table, as defined by the TSQLAuthUser
class type.
Each user has therefore its own associated AuthGroup table, a
name to be entered at login, a name to be displayed on screen or reports, and a
SHA-256 hash of its registered password. A custom Data BLOB field
is specified for your own application use, but not accessed by the
framework.
By default, the following security groups are created on a void database:
| AuthGroup | POST SQL | Auth Read | Auth Write | Tables R | Tables W |
| Admin | Yes | Yes | Yes | Yes | Yes |
| Supervisor | No | Yes | No | Yes | Yes |
| User | No | No | No | Yes | Yes |
| Guest | No | No | No | Yes | No |
Then the corresponding 'Admin', 'Supervisor' and
'User' AuthUser accounts are created, with the default
'synopse' password.
You MUST override those default 'synopse' passwords for
each AuthUser record to a custom genuine value.
'Admin' will be the only group able to execute remote not SELECT
SQL statements for POST commands (i.e. to have TSQLAccessRights.
AllowRemoteExecute = true) and modify the Auth* tables
(i.e. AuthUser and AuthGroup) content.
A typical JSON representation of the default security user/group definitions are the following:
[{"AuthUser":[
{"RowID":1,"LogonName":"Admin","DisplayName":"Admin","PasswordHashHexa":"67aeea294e1cb515236fd7829c55ec820ef888e8e221814d24d83b3dc4d825dd","GroupRights":1,"Data":null},
{"RowID":2,"LogonName":"Supervisor","DisplayName":"Supervisor","PasswordHashHexa":"67aeea294e1cb515236fd7829c55ec820ef888e8e221814d24d83b3dc4d825dd","GroupRights":2,"Data":null},
{"RowID":3,"LogonName":"User","DisplayName":"User","PasswordHashHexa":"67aeea294e1cb515236fd7829c55ec820ef888e8e221814d24d83b3dc4d825dd","GroupRights":3,"Data":null}]},
{"AuthGroup":[
{"RowID":1,"Ident":"Admin","SessionTimeout":10,"AccessRights":"11,1-256,0,1-256,0,1-256,0,1-256,0"},
{"RowID":2,"Ident":"Supervisor","SessionTimeout":60,"AccessRights":"10,1-256,0,3-256,0,3-256,0,3-256,0"},
{"RowID":3,"Ident":"User","SessionTimeout":60,"AccessRights":"10,3-256,0,3-256,0,3-256,0,3-256,0"},
{"RowID":4,"Ident":"Guest","SessionTimeout":60,"AccessRights":"0,3-256,0,0,0,0"}]}]
Of course, you can change AuthUser and AuthGroup
table content, to match your security requirements, and application
specifications. You can specify a per-table CRUD access, via the
AccessRights column, as we stated above, speaking about the
TSQLAccessRights record layout.
This will implement both Query Authentication together with a group-defined per-user right management.
Session handling
A dedicated RESTful service, available from the ModelRoot/Auth
URI, is to be used for user authentication, handling so called sessions.
In mORMot, a very light in-memory set of sessions is
implemented:
- The unique ModelRoot/Auth URI end-point will create a session
after proper authorization;
- In-memory session allows very fast process and reactivity, on Server
side;
- An integer session identifier is used for all
authorization process, independently from the underlying authentication scheme
(i.e. mORMot is not tied to cookies, and its session process is much
more generic).
Those sessions are in-memory TAuthSession class instances. Note
that this class does not inherit from a TSQLRecord table so won't
be remotely accessible, for performance and security reasons.
The server methods should not have to access those instances directly, but
rely on the SessionID identifier. The only access available is via
the function TSQLRestServer.SessionGetUser(aSessionID: Cardinal):
TSQLAuthUser method.
When the Client is about to close (typically in
TSQLRestClientURI.Destroy), a GET
ModelRoot/auth?UserName=...&Session=... request is sent to the
remote server, in order to explicitly close the corresponding session in the
server memory (avoiding most re-play attacks).
Note that each opened session has an internal TimeOut parameter
(retrieved from the associated TSQLAuthGroup table content): after
some time of inactivity, sessions are closed on the Server Side.
In addition, sessions are used to manage safe cross-client
transactions:
- When a transaction is initiated by a client, it will store the corresponding
client Session ID, and use it to allow client-safe writing;
- Any further write to the DB (Add/Update/Delete) will be accessible only from
this Session ID, until the transaction is released (via commit or
rollback);
- If a transaction began and another client session try to write on the DB, it
will wait until the current transaction is released - a timeout may occur if
the server is not able to acquire the write status within some time;
- This global write locking is implemented in the TSQLRest.AcquireWrite /
ReleaseWrite protected methods, and used on the Server-Side by
TSQLRestServer.URI;
- If the server do not handle Session/Authentication, transactions can be
unsafe, in a multi-client concurrent architecture.
Therefore, for performance reasons in a multi-client environment, it's mandatory to release a transaction (via commit or rollback) as soon as possible.
Authentication schemes
Class-driven authentication
Authentication is implemented in mORMot via the following classes:
| class | Scheme |
TSQLRestServerAuthenticationDefault |
mORMot secure authentication scheme, based on a proprietary dual-pass challenge and SHA-256 hashing |
TSQLRestServerAuthenticationSSPI |
Windows authentication, via the logged user |
TSQLRestServerAuthenticationNone |
Weak but simple authentication, based on user name |
All those classes will identify a TSQLAuthUser record from a
user name. The associated TSQLAuthGroup is then used later for
authorization.
You can add you own custom authentication scheme by defining your own class,
inheriting from TSQLRestServerAuthentication.
By default, no authentication is performed.
If you set the aHandleUserAuthentication parameter to
true when calling the constructor
TSQLRestServer.Create(), both default secure mORMot
authentication and Windows authentication are available. In fact, the
constructor executes the following:
constructor TSQLRestServer.Create(aModel: TSQLModel; aHandleUserAuthentication: boolean);
(...)
if aHandleUserAuthentication then
// default mORMot authentication schemes
AuthenticationRegister([
TSQLRestServerAuthenticationDefault,TSQLRestServerAuthenticationSSPI]);
(...)
In order to define one or several authentication scheme, you can call the
AuthenticationRegister() and
AuthenticationUnregister() methods of
TSQLRestServer.
mORMot secure RESTful authentication
The TSQLRestServerAuthenticationDefault class implements a
proprietary but secure RESTful 18.
Here are the typical steps to be followed in order to create a new user
session via mORMot authentication scheme:
- Client sends a GET ModelRoot/auth?UserName=... request to the
remote server;
- Server answers with an hexadecimal nonce contents (valid for about 5
minutes), encoded as JSON result object;
- Client sends a GET
ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... request
to the remote server, in which ClientNonce is a random value used
as Client nonce, and PassWord is computed from the log-on
and password entered by the User, using both Server and Client nonce
as salt;
- Server checks that the transmitted password is valid, i.e. that its matches
the hashed password stored in its database and a time-valid Server
nonce - if the value is not correct, authentication fails;
- On success, Server will create a new in-memory session and return the session
number and a private key to be used during the session (encoded as JSON result
object);
- On any further access to the Server, a &session_signature=
parameter is added to the URL, and will be checked against the valid sessions
in order to validate the request.
Query Authentication is handled at the Client side in
TSQLRestClientURI.SessionSign method, by computing the
session_signature parameter for a given URL, according to the
TSQLRestServerAuthentication class used.
In order to enhance security, the session_signature parameter
will contain, encoded as 3 hexadecimal 32 bit cardinals:
- The Session ID (to retrieve the private key used for the signature);
- A Client Time Stamp (in 256 ms resolution) which must be greater or equal
than the previous time stamp received;
- The URI signature, using the session private key, the user hashed password,
and the supplied Client Time Stamp as source for its crc32 hashing
algorithm.
Such a classical 3 points signature will avoid most man-in-the-middle (MITM) or re-play attacks.
Here is typical signature to access the root URL
root?session_signature=0000004C000F6BE365D8D454
In this case, 0000004C is the Session ID, 000F6BE3
is the client time stamp (aka nonce), and 65D8D454 is the
signature, checked by the following Delphi expression:
(crc32(crc32(fPrivateSaltHash,PTimeStamp,8),pointer(aURL),aURLlength)=aSignature);
For instance, a RESTful GET of the TSQLRecordPeople table with
RowID=6 will have the following URI:
root/People/6?session_signature=0000004C000F6DD02E24541C
For better Server-side performance, the URI signature will use fast crc32 hashing method, and not the more secure (but much slower) SHA-256. Since our security model is not officially validated as a standard method (there is no standard for per URI authentication of RESTful applications), the better security will be handled by encrypting the whole transmission channel, using standard HTTPS with certificates signed by a trusted CA, validated for both client and server side. The security involved by using crc32 will be enough for most common use. Note that the password hashing and the session opening will use SHA-256, to enhance security with no performance penalty.
In our implementation, for better Server-side reaction, the
session_signature parameter is appended at the end of the URI, and
the URI parameters are not sorted alphabetically, as suggested by the reference
article quoted above. This should not be a problem, either from a Delphi Client
or from a AJAX / JavaScript client.
On practice, this scheme is secure and very fast, perfect for a Delphi client.
Authentication using Windows credentials
By default, the hash of the user password is stored safely on the server side. This may be an issue for corporate applications, since a new user name / password pair is to be defined by each client, which may be annoying.
Since revision 1.18 of the framework, mORMot is able to use Windows Authentication to identify any user. That is, the user does not need to enter any name nor password, but her/his Windows credentials, as entered at Windows session startup, will be used.
If the SSPIAUTH conditional is defined (which is the default),
any call to TSQLRestClientURI.SetUser() method with a void
aUserName parameter will try to use current logged name and
password to perform a secure Client-Server authentication. It will in fact call
the class function
TSQLRestServerAuthenticationSSPI.ClientSetUser() method.
In this case, the aPassword parameter will just be ignored.
This will be transparent to the framework, and a regular session will be
created on success.
Only prerequisite is that the TSQLAuthUser table shall contain
a corresponding entry, with its LogonName column equals to
'DomainNameUserName' value. This data row won't be created
automatically, since it is up to the application to allow or disallow access
from an authenticated user: you can be member of the domain, but not eligible
to the application.
Weak authentication
The TSQLRestServerAuthenticationNone class can be used if you
trust your client (e.g. via a https connection). It will implement
a weak but simple authentication scheme.
Here are the typical steps to be followed in order to create a new user
session via this authentication scheme:
- Client sends a GET ModelRoot/auth?UserName=... request to the
remote server;
- Server checks that the transmitted user name is valid, i.e. that it is
available in the TSQLAuthGroup table - if the value is not
correct, authentication fails
- On success, Server will create a new in-memory session and returns the
associated session number (encoded as hexadecimal in the JSON result
object);
- On any further access to the Server, a &session_signature=
parameter is to be added to the URL with the correct session ID, and will be
checked against the valid sessions in order to validate the request.
For instance, a RESTful GET of the TSQLRecordPeople table with
RowID=6 will have the following URI:
root/People/6?session_signature=0000004C
Here is some sample code about how to define this authentication scheme:
// on the Server side: Server.AuthenticationRegister(TSQLRestServerAuthenticationNone); ... // on the Client side: TSQLRestServerAuthenticationNone.ClientSetUser(Client,'User');
The performance benefit is very small in comparison to
TSQLRestServerAuthenticationDefault, so should not be used for
Delphi clients.
Clients authentication
Client interactivity
Note that with this design, it's up to the Client to react to an authentication error during any request, and ask again for the User pseudo and password at any time to create a new session. For multiple reasons (server restart, session timeout...) the session can be closed by the Server without previous notice.
In fact, the Client should just use create one instance of the
TSQLRestClientURI classes as presented in 6, then call the
SetUser method as such:
Check(Client.SetUser('User','synopse')); // use default user
Then an event handled can be associated to the TSQLRestClientURI.
OnAuthentificationFailed property, in order to ask the user to enter its
login name and password:
TOnAuthentificationFailed = function(Retry: integer;
var aUserName, aPassword: string): boolean;
Of course, if Windows Authentication is defined (see above), this
event handler shall be adapted as expected. For instance, you may add a custom
notification to register the corresponding user to the
TSQLAuthUser table.
Authentication using AJAX
Some working JavaScript code has been published in our forum by a framework
user (thanks, "RangerX"), which implements the authentication schema as
detailed above. It uses jQuery, and HTML 5
LocalStorage, not cookies, for storing session information on the
Client side.
See http://synopse.info/forum/viewtopic.php?pid=2995#p2995
The current revision of the framework contains the code as expected by this JavaScript code - especially the results encoded as 2 objects.
In the future, some "official" code will be available for such AJAX clients.
It will probably rely on pure-pascal implementation using such an
Object-Pascal-to-JavaScript compiler - it does definitively make sense
to have Delphi-like code on the client side, not to break the ORM design. For
instance, the Open Source DWS (DelphiWebScript) compiler
matches our needs - see http://delphitools.info/tag/javascript
Authorization
Per-table access rights
Even if authentication is disabled, a pointer to a
TSQLAccessRights record, and its GET / POST / PUT /
DELETE fields, is sent as a member of the parameter to the unique access
point of the server class:
procedure TSQLRestServer.URI(var Call: TSQLRestServerURIParams);
This will allow checking of access right for all CRUD operations, according
to the table invoked. For instance, if the table TSQLRecordPeople
has 2 as index in TSQLModel.Tables[], any incoming
POST command for TSQLRecordPeople will be allowed
only if the 2nd bit in RestAccessRights^.POST field is set, as
such:
case URI.Method of
mPOST: begin // POST=ADD=INSERT
if URI.Table=nil then begin
(...)
end else
// here, Table<>nil and TableIndex in [0..MAX_SQLTABLES-1]
if not (URI.TableIndex in Call.RestAccessRights^.POST) then // check User
Call.OutStatus := HTML_FORBIDDEN else
(...)
Making access rights a parameter allows this method to be handled as pure stateless, thread-safe and session-free, from the bottom-most level of the framework.
On the other hand, the security policy defined by this global parameter does
not allow tuned per-user authorization. In the current implementation, the
SUPERVISOR_ACCESS_RIGHTS constant is transmitted for all handled
communication protocols (direct access, GDI messages, named pipe or HTTP). Only
direct access via TSQLRestClientDB will use
FULL_ACCESS_RIGHTS, i.e. will have AllowRemoteExecute
parameter set to true.
The light session process, as implemented by 18, is used to override the
access rights with the one defined in the
TSQLAuthGroup.AccessRights field.
Be aware than this per-table access rights depend on the table order as
defined in the associated TSQLModel. So if you add some tables to
your database model, please take care to add the new tables after the
existing. If you insert the new tables within current tables, you will need to
update the access rights values.
Additional safety
A AllowRemoteExecute: TSQLAllowRemoteExecute field has been
made available in the TSQLAccessRights record to tune remote
execution, depending on the authenticated user.
It adds some options to tune the security policy.
SQL remote execution
In our RESTful implementation, the POST command with no table associated in the URI allows to execute any SQL statement directly.
This special command should be carefully tested before execution, since SQL misuses could lead into major security issues. Such execution on any remote connection, if the SQL statement is not a SELECT, is unsafe. In fact, if it may affect the data content.
By default, for security reasons, this AllowRemoteExecute field
value in SUPERVISOR_ACCESS_RIGHTS constant does not include
reSQL. It means that no remote call will be allowed but for safe
read-only SELECT statements.
Another possibility of SQL remote execution is to add a
sql=.... inline parameter to a GET request (with optional paging).
The reUrlEncodedSQL option is used to enable or disable this
feature.
Last but not least, a WhereClause=... inline parameter can be
added to a DELETE request. The reUrlEncodedDelete option is used
to enable or disable this feature.
You can change the default safe policy by including reSQL,
reUrlEncodedSQL or reUrlEncodedDelete in the
TSQLAuthGroup.AccessRights field if an authentication user
session. But since remote execution of any SQL statements can be unsafe, we
recommend to write a dedicated server-side service (method-based or
interface-based) to execute such statements.
Service execution
The reService option can be used to enable or unable the
interface-based services feature of mORMot.
In addition to this global parameter, you can set per-service and per-method security via dedicated methods.
For method-based services, if authentication is enabled, any method execution will be processed only for signed URI.
You can use TSQLRestServer.ServiceMethodByPassAuthentication()
to disable the need of a signature for a given service method - e.g. it is the
case for Auth and TimeStamp standard method
services.
Feedback is welcome on our forum, as usual.
