Authentication and Authorization
TMS Sparkle provides easy-to-use, built-in authentication and authorization mechanisms. You can use several authentication mechanisms, like Basic or JWT, and implement an agnostic authorization server mechanism, independent of the authentication used.
Here are the basic steps to implement it:
Add one of more authentication middleware to your sparkle module. For example, Basic Authenticaction Middleware or JWT Authentication Middleware.
Depending on the middleware used, upon authentication create the user identity and its claims.
When implementing your server, authorize your requests, by checking for the User property in the request, verify if it exists (authenticated) and if it has the claims needed to perform the server request (authorization).
The following topics explain the above steps in details and provide additional info.
Adding Authentication Middleware
To authenticate your incoming request, you need to add an authentication middleware to your TMS Sparkle module. The purpose of the authentication middleware is check for user credentials sent by the client, and then creating the user identity and claims based on those credentials. The user identity/claims will be added to the request, which will then be forwarded to the next request processor in the middleware pipeline. From that point, you will be able to use that info to authorize your requests.
These are the available authentication middleware and a simple example about how to use them. For more info, go to the specific authentication middleware topic.
JWT (JSON Web Token) Middleware (Delphi XE6 and up only)
Authenticates your requests using JWT (JSON Web Token). This will look for a Bearer token in the request authentication header which contains the JWT. The user identity and claims will be automatically retrieved from the JWT itself. Basic usage is just create the middleware with the secret used to sign the token:
uses {...}, Sparkle.Middleware.Jwt;
Module.AddMiddleware(TJwtMiddleware.Create('my jwt secret'));
The middleware just validates and extracts the token information. To proper create a full JWT authentication mechanism, your server has to somehow generate a token for the client (for example, upon a Login method). This is explained in more details in the Authentication Example using JWT (JSON Web Token).
Basic Authentication Middleware
Authenticates requests using Basic Authentication. It looks for the Basic keyword in the request authentication header and retrieves user name and password. It successful it pass user name and password to an event handler that should in turn create and return the proper user identity and claims. Here is an example:
uses {...}, Sparkle.Middleware.BasicAuth, Sparkle.Security;
Module.AddMiddleware(TBasicAuthMiddleware.Create(
procedure(const UserName, Password: string; var User: IUserIdentity)
begin
// Implement custom logic to authenticate user based on UserName and Password
// For example, go to the database, check credentials and then create an user
// identity with all permissions (claims) for this user
User := TUserIdentity.Create;
User.Claims.AddOrSet('roles').AsString := SomeUserPermission;
User.Claims.AddOrSet('sub').AsString := UserName;
end,
'My Server Realm'
));
User Identity and Claims
The authentication/authorization mechanism is based on user identity and
claims. That is represented in Sparkle by the IUserIdentity interface,
declared in unit User.Security
:
IUserIdentity = interface
function Claims: TUserClaims;
end;
Such interface has a single function which return the user claims. Claims is a name/value mapping that holds information about the user, like its name, e-mail address, permissions, and any extra info you can add it. Each claim has a name and a value, and the value can be of several types, like integer, string, etc.
The user identity is created and filled by the authentication middleware, and attached to the THttpRequest object in the User property.
If you are implementing the authentication middleware processing, you might want to create a new user identity (class TUserIdentity is a ready-to-use class that implements IUserIdentity interface) and add claims to it using AddOrSet method, for example:
uses {...}, Sparkle.Middleware.BasicAuth, Sparkle.Security;
Module.AddMiddleware(TBasicAuthMiddleware.Create(
procedure(const UserName, Password: string; var User: IUserIdentity)
begin
// Implement custom logic to authenticate user based on UserName and Password
// For example, go to the database, check credentials and then create an user
// identity with all permissions (claims) for this user
User := TUserIdentity.Create;
User.Claims.AddOrSet('roles').AsString := SomeUserPermission;
User.Claims.AddOrSet('sub').AsString := UserName;
end,
'My Server Realm'
));
If you are implementing your server logic and wants to check user identity and claims, you can use methods Exists or Find to get the user claim and then its value:
var RolesClaim: TUserClaim;
begin
RolesClaim := nil;
if Request.User <> nil then
RolesClaim := Request.User.Find('roles');
if RolesClaim <> nil then
Permissions := RolesClaim.AsString;
TUserClaims holds a list of TUserClaim objects which are destroyed when TUserClaims is destroyed. TUserClaims and TUserClaim classes provide several properties and methods for you to create and read claims.
TUserClaims Methods
Name | Description |
---|---|
function AddOrSet(Claim: TUserClaim): TUserClaim | Adds a new claim to the user. If there is already a claim with the same name as the one being passed, it will be replaced by the new one. You don't need to destroy the TUserClaim object, it will be destroyed when TUserClaims is destroyed. |
function AddOrSet(const Name: string): TUserClaim | Creates and adds a new claim with the specified name. If there is already a claim with same name, it will be destroyed and replaced by the new one. You don't need to destroy the new claim. Example:User.Claims.AddOrSet('isadmin').AsBoolean := True; |
function AddOrSet(const Name, Value: string): TUserClaim | Creates and adds a new claim with the specified name and string value. If there is already a claim with same name, it will be destroyed and replaced by the new one. You don't need to destroy the new claim. Example:User.Claims.AddOrSet('email', 'user@myserver.com'); |
function Exists(const Name: string): Boolean | Returns true if a claim with specified name exists. |
function Find(const Name: string): TUserClaim | Returns the TUserClaim object with the specified name. If it doesn't exist, returns nil. |
procedure Remove(const Name: string) | Removes and destroys the claim with the specified name. |
property Items[const Name: string]: TUserClaim | Returns a claim with the specified name. If the claim doesn't exist, an error is raised. |
for Claim in User.Claims | You can use the for..in operator to enumerate all the claims in the user identity. |
TUserClaim Properties
Name | Description |
---|---|
Name: string | Contains the name of the claim. |
AsString: string | Reads or writes the claim value as a string. If the value was not previously saved as a string, an error may be raised. |
AsInteger: Int64 | Reads or writes the claim value as an Int64 value. If the value was not previously saved as an Int64 value, an error may be raised. |
AsDouble: Double | Reads or writes the claim value as a double value. If the value is not a double value, an error may be raised. |
AsBoolean: Boolean | Reads or writes the claim value as a boolean value. If the value was not previously saved as a boolean value, an error may be raised. |
AsEpoch: TDateTime | Reads or writes the claim value as a date time value. When writing, saves it as a Unix time. When reading, it considers the existing value is a valid Unix time, and converts it to TDateTime. If the value is not a valid Unix time, an exception is raised. |
Authorizing Requests
If you have property added an authentication middleware, the HTTP request you receive will already contain authentication information. Implementing your code is just as simple of examining the User property of the request, check if it exists and check the claims it contains.
Suppose a very simple "hello world" module that refuses anonymous connections and only respond to requests from users which has a claim 'isadmin' set to true. It also considers that the authentication mechanism contains a claim named 'sub' which has the name of the user authenticated.
uses {...}, Sparkle.Security;
procedure TProtectedHelloModule.ProcessRequest(const C: THttpServerContext);
var
User: IUserIdentity;
AdminClaim, NameClaim: TUserClaim;
ResponseText: string;
begin
User := C.Request.User;
if User = nil then
// not authenticated
C.Response.StatusCode := 401
else
begin
AdminClaim := User.Claims.Find('isadmin');
if not (Assigned(AdminClaim) and AdminClaim.AsBoolean) then
// forbidden
C.Response.StatusCode := 403;
else
begin
NameClaim := User.Claims.Find('sub');
ResponseText := 'Hello';
if NameClaim <> nil then
ResponseText := ResponseText + ', ' + NameClaim.AsString;
C.Response.StatusCode := 200;
C.Response.ContentType := 'text/plain';
C.Response.Close(TEncoding.UTF8.GetBytes(ResponseText));
end;
end;
end;
A more detailed example in Authentication Example using JWT (JSON Web Token).
Creating JSON Web Tokens
For JWT handling, Sparkle users a slightly modified version of the nice open source Delphi JOSE and JWT Library: http://github.com/paolo-rossi/delphi-jose-jwt.
You can refer to that library page and documentation to learn how to
create the tokens, for example to implement a login mechanism. The
library provided in the Sparkle is mostly the same, with the main
different that all units names are prefixed with "Bcl." to avoid unit
name conflict. So for example, the unit JSON.Core.Builder
becomes
Bcl.Json.Core.Builder
.
Just for a reference, here is an example about how to generate a JWT:
function TTestService.Login(const UserName, Password: string): string;
var
JWT: TJWT;
PermissionsFromDatabase: string;
begin
// check if UserName and Password are valid, and retrieve User data from database
// for example, PermissionsFromDatabase, and set desired claims accordingly
JWT := TJWT.Create(TJWTClaims);
try
JWT.Claims.SetClaimOfType<string>('roles', PermissionsFromDatabase);
JWT.Claims.SetClaimOfType<string>('roles', UserName);
JWT.Claims.Issuer := 'XData Server';
JWT.Claims.Expiration := IncHour(Now, 1); // Expire token one hour from now
Result := TJOSE.SHA256CompactToken('my JWT secret', JWT);
finally
JWT.Free;
end;
end;