2016-06-22 17:49:16 +02:00
|
|
|
// ***************************************************************************
|
|
|
|
//
|
|
|
|
// Delphi MVC Framework
|
|
|
|
//
|
|
|
|
// Copyright (c) 2010-2016 Daniele Teti and the DMVCFramework Team
|
|
|
|
//
|
|
|
|
// https://github.com/danieleteti/delphimvcframework
|
|
|
|
//
|
|
|
|
// ***************************************************************************
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
// *************************************************************************** }
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
unit MVCFramework.Middleware.JWT;
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
uses
|
|
|
|
MVCFramework,
|
|
|
|
MVCFramework.Commons,
|
|
|
|
MVCFramework.Logger,
|
|
|
|
MVCFramework.JWT,
|
|
|
|
System.Generics.Collections,
|
2016-06-23 11:42:16 +02:00
|
|
|
System.DateUtils, System.SysUtils;
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
type
|
2016-06-23 11:42:16 +02:00
|
|
|
TJWTClaimsSetup = reference to procedure(const JWT: TJWT);
|
|
|
|
|
2016-05-23 17:21:33 +02:00
|
|
|
TMVCJwtAuthenticationMiddleware = class(TInterfacedObject, IMVCMiddleware)
|
|
|
|
strict private
|
|
|
|
FMVCAuthenticationHandler: IMVCAuthenticationHandler;
|
|
|
|
|
2016-06-23 11:42:16 +02:00
|
|
|
procedure Render(const aErrorCode: UInt16; const aErrorMessage: string; aContext: TWebContext;
|
|
|
|
const aErrorClassName: string = ''); overload;
|
2016-05-23 17:21:33 +02:00
|
|
|
private
|
|
|
|
FClaimsToChecks: TJWTCheckableClaims;
|
2016-06-23 11:42:16 +02:00
|
|
|
FSetupJWTClaims: TJWTClaimsSetup;
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
protected
|
|
|
|
FSecret: string;
|
|
|
|
procedure OnBeforeRouting(Context: TWebContext; var Handled: Boolean);
|
|
|
|
procedure OnAfterControllerAction(Context: TWebContext;
|
|
|
|
const AActionName: string; const Handled: Boolean);
|
|
|
|
procedure OnBeforeControllerAction(Context: TWebContext;
|
|
|
|
const AControllerQualifiedClassName: string; const AActionName: string;
|
|
|
|
var Handled: Boolean);
|
|
|
|
public
|
|
|
|
constructor Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
|
2016-06-23 11:42:16 +02:00
|
|
|
aConfigClaims: TJWTClaimsSetup;
|
|
|
|
aSecret: string = 'D3lph1MVCFram3w0rk';
|
|
|
|
aClaimsToCheck: TJWTCheckableClaims = [
|
2016-05-23 17:21:33 +02:00
|
|
|
TJWTCheckableClaim.ExpirationTime,
|
|
|
|
TJWTCheckableClaim.NotBefore,
|
|
|
|
TJWTCheckableClaim.IssuedAt
|
|
|
|
]); virtual;
|
|
|
|
end;
|
|
|
|
|
|
|
|
implementation
|
|
|
|
|
|
|
|
uses
|
2016-06-23 11:42:16 +02:00
|
|
|
MVCFramework.Session
|
2016-05-23 17:21:33 +02:00
|
|
|
{$IF CompilerVersion < 27}
|
|
|
|
, Data.DBXJSON
|
|
|
|
{$ELSE}
|
|
|
|
, System.JSON, Web.ApacheHTTP
|
|
|
|
{$ENDIF}
|
|
|
|
{$IF CompilerVersion >= 21}
|
|
|
|
, System.NetEncoding
|
|
|
|
{$ELSE}
|
|
|
|
, Soap.EncdDecd
|
|
|
|
{$ENDIF};
|
|
|
|
|
|
|
|
{ TMVCSalutationMiddleware }
|
|
|
|
|
|
|
|
constructor TMVCJwtAuthenticationMiddleware.Create(AMVCAuthenticationHandler
|
|
|
|
: IMVCAuthenticationHandler;
|
2016-06-23 11:42:16 +02:00
|
|
|
aConfigClaims: TJWTClaimsSetup;
|
|
|
|
aSecret: string;
|
|
|
|
aClaimsToCheck: TJWTCheckableClaims);
|
2016-05-23 17:21:33 +02:00
|
|
|
begin
|
|
|
|
inherited Create;
|
|
|
|
FMVCAuthenticationHandler := AMVCAuthenticationHandler;
|
2016-06-23 11:42:16 +02:00
|
|
|
FSecret := aSecret;
|
|
|
|
FClaimsToChecks := aClaimsToCheck;
|
|
|
|
FSetupJWTClaims := aConfigClaims;
|
2016-05-23 17:21:33 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TMVCJwtAuthenticationMiddleware.OnAfterControllerAction
|
|
|
|
(Context: TWebContext; const AActionName: string; const Handled: Boolean);
|
|
|
|
begin
|
|
|
|
// do nothing
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TMVCJwtAuthenticationMiddleware.OnBeforeControllerAction
|
|
|
|
(Context: TWebContext; const AControllerQualifiedClassName,
|
|
|
|
AActionName: string; var Handled: Boolean);
|
|
|
|
var
|
|
|
|
lAuthRequired: Boolean;
|
|
|
|
lIsAuthorized: Boolean;
|
|
|
|
lJWT: TJWT;
|
|
|
|
lAuthHeader: string;
|
|
|
|
lToken: string;
|
|
|
|
lError: String;
|
|
|
|
begin
|
|
|
|
// check if the resource is protected
|
|
|
|
FMVCAuthenticationHandler.OnRequest(AControllerQualifiedClassName,
|
|
|
|
AActionName, lAuthRequired);
|
|
|
|
if not lAuthRequired then
|
|
|
|
begin
|
|
|
|
Handled := False;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
|
|
|
|
// Checking token in subsequent requests
|
|
|
|
// ***************************************************
|
|
|
|
lJWT := TJWT.Create(FSecret);
|
|
|
|
try
|
|
|
|
lJWT.RegClaimsToChecks := Self.FClaimsToChecks;
|
|
|
|
lAuthHeader := Context.Request.Headers['Authentication'];
|
|
|
|
if lAuthHeader.IsEmpty then
|
|
|
|
begin
|
|
|
|
Render(http_status.Unauthorized, 'Authentication Required', Context);
|
|
|
|
Handled := True;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
|
2016-06-23 11:42:16 +02:00
|
|
|
// retrieve the token from the "authentication bearer" header
|
|
|
|
lToken := '';
|
2016-05-23 17:21:33 +02:00
|
|
|
if lAuthHeader.StartsWith('bearer', True) then
|
|
|
|
begin
|
|
|
|
lToken := lAuthHeader.Remove(0, 'bearer'.Length).Trim;
|
|
|
|
end;
|
|
|
|
|
2016-06-23 11:42:16 +02:00
|
|
|
// check the jwt
|
2016-05-23 17:21:33 +02:00
|
|
|
if not lJWT.IsValidToken(lToken, lError) then
|
|
|
|
begin
|
2016-06-23 11:42:16 +02:00
|
|
|
Render(http_status.Unauthorized, lError, Context);
|
2016-05-23 17:21:33 +02:00
|
|
|
Handled := True;
|
|
|
|
end
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
lJWT.LoadToken(lToken);
|
|
|
|
if lJWT.CustomClaims['username'].IsEmpty then
|
|
|
|
begin
|
|
|
|
Render(http_status.Unauthorized, 'Invalid Token, Authentication Required', Context);
|
|
|
|
Handled := True;
|
|
|
|
end
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
lIsAuthorized := False;
|
2016-06-23 11:42:16 +02:00
|
|
|
Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
|
|
|
|
Context.LoggedUser.Roles.AddRange(lJWT.CustomClaims['roles'].Split([',']));
|
|
|
|
Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
|
2016-05-23 17:21:33 +02:00
|
|
|
FMVCAuthenticationHandler.OnAuthorization(Context.LoggedUser.Roles,
|
|
|
|
AControllerQualifiedClassName, AActionName, lIsAuthorized);
|
|
|
|
if lIsAuthorized then
|
|
|
|
begin
|
|
|
|
Handled := False;
|
|
|
|
end
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
Render(http_status.Forbidden, 'Authorization Forbidden', Context);
|
|
|
|
Handled := True;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
lJWT.Free;
|
|
|
|
end;
|
|
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
procedure TMVCJwtAuthenticationMiddleware.OnBeforeRouting
|
|
|
|
(Context: TWebContext; var Handled: Boolean);
|
|
|
|
var
|
|
|
|
lUserName: string;
|
|
|
|
lPassword: string;
|
|
|
|
lRoles: TList<String>;
|
|
|
|
lSessionData: TSessionData;
|
|
|
|
lIsValid: Boolean;
|
|
|
|
lJWT: TJWT;
|
|
|
|
begin
|
|
|
|
if SameText(Context.Request.PathInfo, '/login') and (Context.Request.HTTPMethod = httpPOST) then
|
|
|
|
begin
|
|
|
|
lUserName := Context.Request.Headers['jwtusername'];
|
|
|
|
lPassword := Context.Request.Headers['jwtpassword'];
|
|
|
|
if (lUserName.IsEmpty) or
|
|
|
|
(lPassword.IsEmpty) then
|
|
|
|
begin
|
|
|
|
Render(http_status.Unauthorized, 'Username and password Required', Context);
|
|
|
|
Handled := True;
|
|
|
|
Exit;
|
|
|
|
end;
|
|
|
|
|
|
|
|
// check the authorization for the requested resource
|
|
|
|
lRoles := TList<string>.Create;
|
|
|
|
try
|
|
|
|
lSessionData := TSessionData.Create;
|
|
|
|
try
|
|
|
|
FMVCAuthenticationHandler.OnAuthentication(lUserName, lPassword,
|
|
|
|
lRoles, lIsValid, lSessionData);
|
|
|
|
if lIsValid then
|
|
|
|
begin
|
|
|
|
lJWT := TJWT.Create(FSecret);
|
|
|
|
try
|
2016-06-23 11:42:16 +02:00
|
|
|
// let's user config claims and custom claims
|
|
|
|
FSetupJWTClaims(lJWT);
|
|
|
|
|
|
|
|
// these claims are mandatory and managed by the middleware
|
|
|
|
if not lJWT.CustomClaims['username'].IsEmpty then
|
|
|
|
raise EMVCJWTException.Create
|
|
|
|
('Custom claim "username" is reserved and cannot be modified in the JWT setup');
|
|
|
|
if not lJWT.CustomClaims['roles'].IsEmpty then
|
|
|
|
raise EMVCJWTException.Create
|
|
|
|
('Custom claim "roles" is reserved and cannot be modified in the JWT setup');
|
|
|
|
|
2016-05-23 17:21:33 +02:00
|
|
|
lJWT.CustomClaims['username'] := lUserName;
|
|
|
|
lJWT.CustomClaims['roles'] := String.Join(',', lRoles.ToArray);
|
|
|
|
|
2016-06-23 11:42:16 +02:00
|
|
|
/// / setup the current logged user from the JWT
|
2016-05-23 17:21:33 +02:00
|
|
|
Context.LoggedUser.Roles.AddRange(lRoles);
|
2016-06-23 11:42:16 +02:00
|
|
|
Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
|
|
|
|
Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
|
|
|
|
Context.LoggedUser.Realm := lJWT.Claims.Subject;
|
|
|
|
/// ////////////////////////////////////////////////
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
InternalRender(TJSONObject.Create(TJSONPair.Create('token', lJWT.GetToken)),
|
|
|
|
TMVCMediaType.APPLICATION_JSON,
|
|
|
|
TMVCConstants.DEFAULT_CONTENT_CHARSET, Context);
|
|
|
|
Handled := True;
|
|
|
|
finally
|
|
|
|
lJWT.Free;
|
|
|
|
end;
|
|
|
|
end
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
Render(http_status.Forbidden, 'Forbidden', Context);
|
|
|
|
Handled := True;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
lSessionData.Free;
|
|
|
|
end;
|
|
|
|
finally
|
|
|
|
lRoles.Free;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2016-06-23 11:42:16 +02:00
|
|
|
procedure TMVCJwtAuthenticationMiddleware.Render(const aErrorCode: UInt16;
|
|
|
|
const aErrorMessage: string; aContext: TWebContext;
|
|
|
|
const aErrorClassName: string = '');
|
2016-05-23 17:21:33 +02:00
|
|
|
var
|
|
|
|
j: TJSONObject;
|
|
|
|
status: string;
|
|
|
|
begin
|
2016-06-23 11:42:16 +02:00
|
|
|
aContext.Response.StatusCode := aErrorCode;
|
|
|
|
aContext.Response.ReasonString := aErrorMessage;
|
2016-05-23 17:21:33 +02:00
|
|
|
status := 'error';
|
2016-06-23 11:42:16 +02:00
|
|
|
if (aErrorCode div 100) = 2 then
|
2016-05-23 17:21:33 +02:00
|
|
|
status := 'ok';
|
|
|
|
j := TJSONObject.Create;
|
|
|
|
j.AddPair('status', status);
|
2016-06-23 11:42:16 +02:00
|
|
|
if aErrorClassName = '' then
|
2016-05-23 17:21:33 +02:00
|
|
|
j.AddPair('classname', TJSONNull.Create)
|
|
|
|
else
|
2016-06-23 11:42:16 +02:00
|
|
|
j.AddPair('classname', aErrorClassName);
|
|
|
|
j.AddPair('message', aErrorMessage);
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
InternalRender(j, TMVCConstants.DEFAULT_CONTENT_TYPE,
|
2016-06-23 11:42:16 +02:00
|
|
|
TMVCConstants.DEFAULT_CONTENT_CHARSET, aContext);
|
2016-05-23 17:21:33 +02:00
|
|
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|