Server-Side Events
TXDataServerModule published several events that you can use to implement additional server-side logic, customize XData behavior, among other tasks. You can use events, for example, to:
Implement custom authorization, refusing or accepting an operation based on the user credentials or any other context information;
Restrict/change the data returned to the client by TMS Aurelius CRUD Endpoints, by adding more filters to a query made by the user, for example;
Implement additional server-side logic, for example, performing extra operations after a resource is saved.
Events in XData are available in the Events property of the
TXDataServerModule object. Such
property refers to a TXDataModuleEvents (declared in unit
XData.Module.Events
) object with several subproperties, each to them
related to an event.
Read Using Events for more detailed info. You can also see real-world usage of events by checking the Authentication Example using JSON Web Token (JWT).
Events in TXDataModuleEvents
General-purpose events
Name | Description |
---|---|
OnModuleException | Occurs when an exception is raised during request processing. You can use it to provide custom error-handling. |
Events of TMS Aurelius CRUD Endpoints
Name | Description |
---|---|
OnEntityGet | Occurs after an entity is retrieved, right before being sent to the client. |
OnEntityList | Occurs when the client queries an entity collection. |
OnEntityInserting | Occurs right before an entity creation. |
OnEntityInserted | Occurs right after an entity creation. |
OnEntityModifying | Occurs right before an entity update. |
OnEntityModified | Occurs right after an entity update. |
OnEntityDeleting | Occurs right before an entity delete. |
OnEntityDeleted | Occurs right after an entity delete. |
Using Events
Events in XData are available in the Events property of the
TXDataServerModule object. Such
property refers to a TXDataModuleEvents (declared in unit
XData.Module.Events
) object with several subproperties, each to them
related to an event.
For example, to access the OnEntityInserting event:
uses {...}, XData.Server.Module, XData.Module.Events;
// Module is an instance of TXDataServerModule object
Module.Events.OnEntityInserting.Subscribe(
procedure(Args: TEntityInsertingArgs)
begin
// Use Args.Entity to retrieve the entity being inserted
end
);
In a less direct way, using method reference instead of anonymous method:
uses {...}, XData.Server.Module, XData.Module.Events;
procedure TSomeClass.MyEntityInsertingProc(Args: TEntityInsertingArgs);
begin
// Use Args.Entity to retrieve the entity being inserted
end;
procedure TSomeClass.RegisterMyEventListeners(Module: TXDataServerModule);
var
Events: TXDataModuleEvents;
begin
Events := Module.Events;
Events.OnEntityInserting.Subscribe(MyEntityInsertingProc);
end;
Listeners are method references that receive a single object as a parameter. Such object has several properties containing relevant information about the event, and differ for each event type. Names of event properties, method reference type and arguments follow a standard. The event property is named "On<event>", method reference type is "T<event>Proc" and parameter object is "T<event>Args". For example, for the "EntityInserting" event, the respective names will be "OnEntityInserting", "TEntityInsertingProc" and "TEntityInsertingArgs".
All events in XData are multicast events, which means you can add several events handlers (listeners) to the same event. When an event occurs, all listeners will be notified. This allows you to add a listener in a safe way, without worrying if it will replace an existing listener that might have been set by other part of the application.
It's always safe to set the events before adding the module and running the server.
OnEntityGet Event
Occurs after an entity is retrieved, right before being sent to the client. This event is also fired when the client requests part of that entity, for example, individual properties of the entity, blob data, or associated entities.
Example:
Module.Events.OnEntityGet.Subscribe(
procedure(Args: TEntityGetArgs)
begin
// code here
end
);
TEntityGetArgs Properties:
Name | Description |
---|---|
Entity: TObject | The retrieved entity. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityList Event
Occurs when the client queries an entity collection. This event is fired after the TCriteria object is built based on the client request, and right before the criteria is actually executed to retrieve the entities and sent to the client.
Example:
Module.Events.OnEntityList.Subscribe(
procedure(Args: TEntityListArgs)
begin
// code here
end
);
TEntityListArgs Properties:
Name | Description |
---|---|
Criteria: TCriteria | The Aurelius criteria built based on client request that will be used to retrieve the collections. You can modify the request here, adding extra filters, orders, etc., before it's executed and results are sent to the client. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityInserting Event
Occurs right before an entity creation. This event happens in the middle of a database transaction, right before the entity is about to be created in the database.
Example:
Module.Events.OnEntityInserting.Subscribe(
procedure(Args: TEntityInsertingArgs)
begin
// code here
end
);
TEntityInsertingArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity being inserted. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityInserted Event
Occurs after an entity is created. Note that, unlike OnEntityInserting Event, this event happens after the transaction is committed. There is no way to rollback the insertion of the entity, and any database operation here will be performed with no active transaction (unless you begin one manually).
Example:
Module.Events.OnEntityInserted.Subscribe(
procedure(Args: TEntityInsertedArgs)
begin
// code here
end
);
TEntityInsertedArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity which was created (inserted). |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityModifying Event
Occurs right before an entity update. This event happens in the middle of a database transaction, right before the entity is about to be updated in the database.
Example:
Module.Events.OnEntityModifying.Subscribe(
procedure(Args: TEntityModifyingArgs)
begin
// code here
end
);
TEntityModifyingArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity being modified. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityModified Event
Occurs right after an entity update. Note that, unlike OnEntityModifying Event, this event happens after the transaction is committed. There is no way to rollback the update of the entity, and any database operation here will be performed with no active transaction (unless you begin one manually).
Example:
Module.Events.OnEntityModified.Subscribe(
procedure(Args: TEntityModifiedArgs)
begin
// code here
end
);
TEntityModifiedArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity which was modified. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityDeleting Event
Occurs right before an entity delete. This event happens in the middle of a database transaction, right before the entity is about to be deleted in the database.
Example:
Module.Events.OnEntityDeleting.Subscribe(
procedure(Args: TEntityDeletingArgs)
begin
// code here
end
);
TEntityDeletingArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity being deleted. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnEntityDeleted Event
Occurs after an entity is deleted. Note that, unlike OnEntityDeleting event, this event happens after the transaction is committed. There is no way to rollback the deletion of the entity, and any database operation here will be performed with no active transaction (unless you begin one manually).
Example:
Module.Events.OnEntityDeleted.Subscribe(
procedure(Args: TEntityDeletedArgs)
begin
// code here
end
);
TEntityDeletedArgs Properties:
Name | Description |
---|---|
Entity: TObject | The entity which has been deleted. |
Handler: TXDataBaseRequestHandler | The XData request processor object. |
OnModuleException Event
Occurs when an unexpected exception is raised during server request processing. By default, when that happens XData will send a response to the client with the property HTTP status code (usually 500 but other codes might be provided as well) and a JSON response with details of the error (a JSON object with properties ErrorCode and ErrorMessage). You can use this event to change that behavior when a specific exception occurs.
Example:
Module.Events.OnModuleException.Subscribe(
procedure(Args: TModuleExceptionArgs)
begin
// code here, for example:
if Args.Exception is EInvalidJsonProperty then
Args.StatusCode := 400;
end
);
TModuleExceptionArgs Properties:
Name | Description |
---|---|
Exception: Exception | The Exception object raised while processing the requested. |
StatusCode: Integer | The HTTP status code to be returned to the client. You can change this property to tell XData to send a different status code. |
ErrorCode: string | The value of the ErrorCode property in the JSON response to be sent to the client. You can modify this value. |
ErrorMessage: string | The value of the ErrorMessage property in the JSON response to be sent to the client. You can modify this value. |
Action: TModuleExceptionAction | The action to be performed:TModuleExceptionAction = (SendError, RaiseException, Ignore) Default value is SendError which means XData will send the HTTP response to the client with the specified StatusCode and with a JSON response that includes ErrorCode and ErrorMessage. You can optionally use RaiseException, which means re-raising the original exception and let it propagate through the code. This gives an opportunity for some Sparkle middleware to catch the raise exception. If that doesn't happen, the exception will be handled by the Sparkle dispatcher. The third option is simply choose Ignore. Use this option if you want to send a custom HTTP response yourself. In this case XData will simply do nothing and finish processing request silently. |
Authentication Example using JSON Web Token (JWT)
Authentication and Authorization mechanisms in XData are available through the built-in auth mechanisms provided in TMS Sparkle, the underlying HTTP framework which XData is based on. The authentication/authorization in Sparkle is generic and can be used for any types of HTTP server, not only XData. But this topic illustrates how to use it integrated with XData server-side events to build an authentication system using JSON Web Tokens.
JSON Web Token (JWT)
From Wikipedia:
JSON Web Token (JWT) is a JSON-based open standard (RFC 7519) for passing claims between parties in web application environment.
That doesn't say if we are just learning about it. There is plenty of information out there, so here I'm going directly to the point in a very summarized practical way.
A JWT is a string with this format:
aaaaaaaaaaa.bbbbbbbbbb.cccccccccc
It's just three sections in string separated by dots. Each section is a text encoded using base64-url:
<base64url-encoded header>.<base64url-encoded claims>.<base64url-encoded signature>
So a real JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidG1zdXNlciIsImlzcyI6IlRNUyBYRGF0YSBTZXJ2ZXIiLCJhZG1pbiI6dHJ1ZX0.pb-4JAajpYxTsDTqWtgyIgpoqCQH8wlHl4RoTki8kpQ
If we decode each part of the JWT separately (remember, we have three parts separated by dots), this is what we will have from part one (spaces and returns added to make it more readable). It's the header:
{
"alg":"HS256",
"typ":"JWT"
}
And this is part two decoded, which is the payload or claims set:
{
"name":"tmsuser",
"iss":"TMS XData Server",
"admin":true
}
Finally the third part is the signature. It makes no sense to decode it here since it's just a bunch of bytes that represent the hash of the header, the payload, and a secret that only the generator of the JWT knows.
The payload is the JSON object that "matters", it's the actualy content that end-user applications will read to perform actions. The header contains meta information the token, mostly the hashing algorithm using to generate the signature, also present in the token. So, we could say that a JWT is just an alternative way to represent a JSON object, but with a signature attached to it.
What does it has to do with authentication and authorization? Well, you can think of the JWT as a "session" or "context" for an user accessing your server. The JSON object in the payload will contain arbitrary information that you are going to put in there, like permissions, user name, etc.. This token will be generated by your server upon some event (for example, an user "login"), and then the client will resend the token to the server whenever he wants to perform any operation. This would be the basic workflow:
Client performs "login" in the server by passing regular user credentials (user name and password for example).
The server validates the credentials, generate a JWT with relevant info, using the secret, and sends the JWT back to the client.
The client sends the JWT in next requests, passing the JWT again to the server.
When processing each request, the server checks if the JWT signature is valid. If it is, then it can trust that the JSON Object in payload is valid and process actions accordingly.
Since only the server has the secret, there is no way the client can change the payload, adding "false" information to it for example. When the server receives the modified JWT, the signature will not match and the token will be rejected by the server.
For more detailed information on JSON Web Tokens (JWT) you can refer to https://jwt.io, the Wikipedia article or just the official specification. It's also worth mentioning that for handling JWT internally, either to create or validate the tokens, TMS XData uses under the hood the open source Delphi JOSE and JWT library.
JWT Authentication with TMS XData
Enough of theory, the next steps will show you how to implement authentication/authorization using JWT in TMS XData. This is just a suggestion, and it's up to you to define with more details how your system will work. In this example we will create a login service, add the middleware and use server-side events to implement authorization.
User Login and JWT Generation
We're going to create a service operation to allow users to perform login. Our service contract will look like this:
[ServiceContract]
ILoginService = interface(IInvokable)
['{BAD477A2-86EC-45B9-A1B1-C896C58DD5E0}']
function Login(const UserName, Password: string): string;
end;
Users will send user name and password, and receive the token. Delphi applications can invoke this method using the TXDataClient, or invoke it using regular HTTP, performing a POST request passing user name and password parameters in the body request in JSON format. Nevertheless, it's worth noting that you should always use a secure connection (HTTPS) in your server to protect such requests.
The implementation of such service operation would be something like this:
uses {...}, Bcl.Jose.Core.JWT, Bcl.Jose.Core.Builder;
function TLoginService.Login(const UserName, Password: string): string;
var
JWT: TJWT;
Role: string;
IsAdmin: Boolean;
begin
{ check if UserName and Password are valid, retrieve User data from database,
add relevant claims to JWT and return it. In this example, we will only
add two claims: Role and IsAdmin. }
// Now that application specific logic is finished, generate the token
JWT := TJWT.Create(TJWTClaims);
try
JWT.Claims.SetClaimOfType<string>('role', Role);
JWT.Claims.SetClaimOfType<Boolean>('isadmin', IsAdmin);
JWT.Claims.Issuer := 'XData Server';
Result := TJOSE.SHA256CompactToken('secret', JWT);
finally
JWT.Free;
end;
end;
Now, users can simply login to the server by performing a request like this (some headers removed):
POST /loginservice/login HTTP/1.1
content-type: application/json
{
"UserName": "tmsuser",
"Password": "tmsuser"
}
and the response will be a JSON object containing the JSON Web Token (some headers removed and JWT modified):
HTTP/1.1 200 OK
Content-Type: application/json
{
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6InRtc3VzZXIiLCJpc3MiOiJYRGF0YSBTZXJ2ZXIifQ.CAxxa3aizZheG3VXmBoXtfdg3N5jN9tNAZHEV7R-W4Q"
}
For further requests, clients just need to add that token in the request using the authorization header by indicating it's a bearer token. For example:
GET /customers?$orderby=Name HTTP/1.1
content-type: application/json
authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6InRtc3VzZXIiLCJpc3MiOiJYRGF0YSBTZXJ2ZXIifQ.CAxxa3aizZheG3VXmBoXtfdg3N5jN9tNAZHEV7R-W4Q
Adding TJwtMiddleware to process tokens in requests
The second step is to add the TJwtMiddleware to our XData server module. It's done just once, before starting up the server with the module. All we need to inform is the secret our server will use to validate the signature of the tokens it will receive:
uses {...}, Sparkle.Middleware.Jwt;
{...}
Module.AddMiddleware(TJwtMiddleware.Create('secret'));
That's it. What this will do? It will automatically check for the token in the authorization header. If it does exist and signature is valid, it will create the IUserIdentity interface, set its Claims based on the claims in the JWT, and set such interface to the User property of THttpRequest object. Regardless if the token exists or not and the User property is set or not, the middleware will forward the processing of the request to your server anyway. It's up to you to check if user is present in the request or not. The only situation where the middleware will return immediately is if the token is present and is invalid. In this case it will return an to the client immediately and your server code will not be executed.
Authorizing the requests
Finally, in your application specific code, all you have to do is check for such User property and take actions based on it. For example, suppose you have a service operation DoSomething that does an arbitrary action. You don't want to allow anonymous requests (not authenticated) to perform such action. And you will only execute such action is authenticated user is an administrator. This is how you would implement it:
uses {...}, Sparkle.Security, XData.Sys.Exceptions;
procedure TMyService.DoSomething;
var
User: IUserIdentity;
begin
User := TXDataOperationContext.Current.Request.User;
if User = nil then
raise EXDataHttpUnauthorized.Create('User not authenticated');
if not (User.Claims.Exists('isadmin') and User.Claims['isadmin'].AsBoolean) then
raise EXDataHttpForbidden.Create('Not enough privileges');
// if code reachs here, user is authenticated and is administrator
// execute the action
end;
Server-side events
The above code is valid for service operations. But you can also use server-side events to protect the entity sets published by XData. For example, you can use the OnEntityDeleting event to not allow non-admin users to delete resources. The event handler implementation would be pretty much the same as the code above (Module refers to a TXDataServerModule object):
Module.Events.OnEntityDeleting.Subscribe(procedure(Args: TEntityDeletingArgs)
var User: IUserIdentity;
begin
User := TXDataOperationContext.Current.Request.User;
if User = nil then
raise EXDataHttpUnauthorized.Create('User not authenticated');
if not (User.Claims.Exists('isadmin') and User.Claims['isadmin'].AsBoolean) then
raise EXDataHttpForbidden.Create('Not enough privileges');
end
);
That applies to all entities. Of course, if you want to restrict the code to some entities, you can check the Args.Entity property to verify the class of object being deleted and perform actions accordingly.
Finally, another nice example for authorization and server-side events. Suppose that every entity in your application has a property named "Protected" which means only admin users can see those entities. You can use a code similar to the one above to refuse requests that try to modify, create or retrieve a protected entity if the requesting user is not admin.
But what about complex queries? In this case you can use the OnEntityList event, which will provide you with the Aurelius criteria (TCriteria) that will be used to retrieve the entities:
Module.Events.OnEntityList.Subscribe(procedure(Args: TEntityListArgs)
var
User: IUserIdentity;
IsAdmin: Boolean;
begin
User := Args.Handler.Request.User;
IsAdmin := (User <> nil) and User.Claims.Exists('isadmin')
and User.Claims['isadmin'].AsBoolean;
if not IsAdmin then
Args.Criteria.Add(TLinq.EqualsTo('Protected', false));
end
);
The code above simply checks if the requesting user has elevated privileges. If it does not, then it adds an extra condition to the criteria (whatever the criteria is) which filters only the entities that are not protected. So non-admin users will not see the protected entities in the server response.
Authentication using TXDataClient
If you are using TXDataClient from a Delphi application to access the XData server, you can simply use the OnSendingRequest event to add authentication credentials (the token you retrieved from the server):
var
Login: ILoginService;
vToken: string;
begin
vToken := Client.Service<ILoginService>.Login(edtUser.Text, edtPassword.Text);
Client.HttpClient.OnSendingRequest := procedure(Req: THttpRequest)
begin
Req.Headers.SetValue('Authorization','Bearer '+vToken);
end;
end;