Authentication and Authorization
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 mechanism in Sparkle is generic and can be used for any types of HTTP server, not only XData. But this topic illustrates how to better use specific XData features like server-side events and attributed-based authorization to make it even easier to secure your REST API.
In this guide we will present the concept of JSON Web Token, then how to authenticate requests (make sure there is a "user" doing requests) and finally how to authorize requests (make sure such user has permissions to perform specific operations).
Note
Even though we are using JWT as an example, the authentication/authorization mechanism is generic. You can use other type of token/authentication mechanism, and the authorization mechanism you use will be exactly the same regardless what token type you use. Holger Flick's book TMS Hands-on With Delphi shows a good example of authentication/authorization using a different approach than JWT.
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 much 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.
Authentication
Enough of theory, now we will show you how to do authentication 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 and attributes 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;
Clients 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.
Warning
It's worth noting that in production code 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 User, Password: string): string;
var
JWT: TJWT;
Scopes: string;
begin
{ check if UserName and Password are valid, retrieve User data from database,
add relevant claims to JWT and return it. In this simplified example,
we are doing a simple check for password }
if User <> Password then
raise EXDataHttpUnauthorized.Create('Invalid password');
JWT := TJWT.Create;
try
JWT.Claims.SetClaimOfType<string>('user', User);
if User = 'admin' then
JWT.Claims.SetClaimOfType<Boolean>('admin', True);
Scopes := 'reader';
if (User = 'admin') or (User = 'writer') then
Scopes := Scopes + ' writer';
JWT.Claims.SetClaimOfType<string>('scope', Scopes);
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": "writer",
"Password": "writer"
}
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.eyJ1c2VyIjoid3JpdGVyIiwic2NvcGUiOiJyZWFkZXIgd3JpdGVyIiwiaXNzIjoiWERhdGEgU2VydmVyIn0.QdRTt6gOl3tb1Zg0aAJ74bepQwqm0Rd735FKToPEyFY"
}
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 /artist?$orderby=Name HTTP/1.1
content-type: application/json
authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoid3JpdGVyIiwic2NvcGUiOiJyZWFkZXIgd3JpdGVyIiwiaXNzIjoiWERhdGEgU2VydmVyIn0.QdRTt6gOl3tb1Zg0aAJ74bepQwqm0Rd735FKToPEyFY
Note
This authentication mechanism is a suggestion and is totally independent from the rest of this guide. The JWT token could be provided in any other way: a different service, different server, different sign-in mechanism (not necessarily username/password), or even a 3rd party token provider.
Implementing JWT Authentication with TJwtMiddleware
The second step is to add a JWT middleware to your server.
At design-time, right-click the TXDataServer component, choose the option to manage the middleware list, and add a JWT Middleware to it. The middleware has an OnGetSecret
event that you need to handle to pass to it the JWT secret our server will use to validate the signature of the tokens it will receive.
If you are using the XData server module, all you need to do is to add a TJwtMiddleware
and inform the secret in the constructor:
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.
Warning
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. It's up to you to check if user is
present in the request or not. If you want the token to prevent non-authenticated requests to be processed, set its ForbidAnonymousAccess
to true.
If the token is present and is invalid, it will return an error to the client immediately and your server code will not be executed.
Authorization
Now that we know how to check for authenticated requests, it's time to authorize the requests - in other words, check if the authenticated client/user has permission to perform specific operations.
Attribute-based Authorization
The easiest way to authorize your API is to simply add authorization attributes to parts of the code you want to specify permissions.
Remember that a XData server has two mechanism for publishing endpoints: service operations and automatic CRUD endpoints. Each of them has different ways to be authorized.
You must use unit XData.Security.Attributes
to use authorization attributes.
Authorize Attribute
This attribute is can be used in service operations. Just add an Authorize
attribute to any method in your service contract interface, and such method will only be invoked if the request is authenticated.
[ServiceContract]
IMyService = interface(IInvokable)
['{80A69E6E-CA89-41B5-A854-DFC412503FEA}']
function NonRestricted: string;
[Authorize]
function Restricted: string;
end;
In the example above, the endpoint represented by the method NonRestricted
will be publicly accessible, while the method Restricted
will only be invoked if the request is authenticated. Otherwise, a 403 Forbidden
response will be returned.
You can also apply the Authorize
attribute at the interface level:
[ServiceContract]
[Authorize]
IMyService = interface(IInvokable)
['{80A69E6E-CA89-41B5-A854-DFC412503FEA}']
function Restricted: string;
function AlsoRestricted: string;
end;
In this case, the attribute rule will be applied to all methods of the interface. In the example above, both Restricted
and AlsoRestricted
methods will only be invoked if request is authenticated.
AuthorizeScopes Attribute
You can use AuthorizeScope
attribute in service operations if you want to allow them to be invoked only if the specified scopes are present in user claims.
It will check for a user claim with name scope
, and check its values. The scope values in scope
claim must be separated by spaces. For example, the scope claim might contain editor
or reader writer
. In the last example, it has two scopes: reader
and writer
.
[AuthorizeScopes('admin')]
procedure ResetAll;
In the example above, the ResetAll
method can only be invoked if the admin
scope is present in the scope
claim.
You can specify optional scopes that will allow a method to be invoked, by separating scopes with comma:
[AuthorizeScopes('admin,writer')]
procedure ModifyEverything;
In the previous example, ModifyEverything
can be invoked if the scope
claim contain either admin
scope, or writer
scope.
You can add multiple AuthorizeScopes
attributes, which will end up as two requirements that must be met to allow method to be invoked:
[AuthorizeScopes('publisher')]
[AuthorizeScopes('editor')]
procedure PublishAndModify;
For method PublishAndModify
to be invoked, both scopes publisher
and editor
must be present in the scope
claim.
Note
Just like Authorize
attribute and any other authorization attribute, you can apply AuthorizeScopes
attribute at both method and interface level. If you apply to both, then all requirements set by the authorization attributes added to interface and method will have to be met for the method to be invoked.
AuthorizeClaims Attribute
If you want to check for an arbitrary user claim, the use AuthorizeClaims
attribute:
[AuthorizeClaims('admin')]
procedure OnlyForAdmins;
The OnlyForAdmins
method will only be executed if the claim 'admin' is present in user claims.
You can also check for a specific claim value, for example:
[AuthorizeClaims('user', 'john')]
procedure MethodForJohn;
In this case, MethodForJohn
will only be executed if claim user
is present and its value is john
.
As already described above, AuthorizeClaims
attribute can be used multiple times and can be applied at both method and interface level.
EntityAuthorize Attribute
You can also protect automatically created CRUD endpoints using authorization attributes. Since those endpoints are based on existing Aurelius entities, you should apply those attributes to the entity class itself.
Note
Attributes for automatic CRUD endpoints are analogous to the ones you use in service operations. The different is they have a prefix Entity
in the name, and receive an extra parameter of type TEntitySetPermissions
indicating to which CRUD operations the attribute applies to.
The simplest one is the EntityAuthorize
attribute:
[Entity, Automapping]
[EntityAuthorize(EntitySetPermissionsWrite)]
TCustomer = class
In the previous example, to invoke endpoints that modify the TCustomer
entity (like POST
, PUT
, DELETE
), the request must be authenticated.
Warning
The rules are applied by entity set permission. In the previous example, the read permissions (GET
a list of customer or a specific customer) are not specified, and thus they are not protected. User don't need to be authenticated to execute them.
EntityAuthorizeScopes Attribute
Similarly to AuthorizeScopes, you can restrict access to CRUD endpoints depending on the existing scopes using EntityAuthorizeScopes
:
[Entity, Automapping]
[EntityAuthorizeScopes('reader', EntitySetPermissionsRead)]
[EntityAuthorizeScopes('writer', EntitySetPermissionsWrite)]
TArtist = class
In the previous example, read operations will be allowed if and only if the scope reader
is present. At the same time, the writer
scope must be present to perform write operations.
That means that to perform both read and write operations, the scope claim must have both reader
and writer
values: reader writer
.
One alternative approach is the following:
[Entity, Automapping]
[EntityAuthorizeScopes('reader,writer', EntitySetPermissionsRead)]
[EntityAuthorizeScopes('writer', EntitySetPermissionsWrite)]
TArtist = class
In the case above, if the scope just have writer
, then it will be able to perform both read and write operations, since read permissions are allowed if either reader
or writer
are present in scope
claim. Either approach is fine, it's up to you to decide what's best for your application.
EntityAuthorizeClaims Attribute
Finally, similarly to AuthorizeClaims
attribute, you can use EntityAuthorizeClaims
to allow certain operations only if a claim exists and/or has a specific value:
[Entity, Automapping]
[EntityAuthorizeClaims('user', 'john', [TEntitySetPermissions.Delete]])]
TArtist = class
In the example above, only users with claim user
equals john
will be able to delete artists.
Manual Authorization in Service Operations
Finally, in addition to authorization attributes, you can always add specific code that authorizes (or forbids) specific operations based on user identity and claims. All you have to do is check for the request 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('admin') and User.Claims['admin'].AsBoolean) then
raise EXDataHttpForbidden.Create('Not enough privileges');
// if code reachs here, user is authenticated and is administrator
// execute the action
end;
Using Server-Side Events
You can also use
server-side events to protect the entity sets published by XData, and add custom code to it. For
example, you can use the OnEntityDeleting
event to forbid non-admin
users from deleting 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('admin') 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
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('admin');
if not IsAdmin then
Args.Criteria.Add(not Linq['Protected']));
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.
Using Authentication Credentials with 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;
JwtToken: string;
begin
JwtToken := Client.Service<ILoginService>.Login(edtUser.Text, edtPassword.Text);
Client.HttpClient.OnSendingRequest := procedure(Req: THttpRequest)
begin
Req.Headers.SetValue('Authorization','Bearer ' + JwtToken);
end;
end;