|
<< Click to Display Table of Contents >> Navigation: Demos > 15 > Delphi > OAuth > UserSessionUnit.pas |
unit UserSessionUnit;
{
This is a DataModule where you can add components or declare fields that are specific to
ONE user. Instead of creating global variables, it is better to use this datamodule. You can then
access the it using UserSession.
}
interface
uses
IWUserSessionBase, SysUtils, Classes, Data.DB, Datasnap.DBClient;
type
TIWUserSession = class(TIWUserSessionBase)
cdsUserAccess: TClientDataSet;
cdsUserAccessTokenHash: TStringField;
cdsUserAccessUserName: TStringField;
cdsUserAccessUserEmail: TStringField;
cdsUserAccessUserId: TStringField;
cdsUserAccessToken: TStringField;
cdsUserAccessRefreshToken: TStringField;
cdsUserAccessApi: TStringField;
cdsUserAccessExpiresAt: TDateTimeField;
cdsUserAccessDateTime: TDateTimeField;
procedure IWUserSessionBaseCreate(Sender: TObject);
private
{ Private declarations }
FIsLoggedIn: Boolean;
FTokenHash: string;
FCDSFileName: string;
function GetTokenInfo(const aTokenHash: string; out aApi, aToken, aRefreshToken: string): Boolean;
procedure SaveAccessCookie(const aTokenHash: string);
procedure RemoveAccessCokie;
procedure SetIsLoggedIn(Value: Boolean);
function DoSaveTokenInfo(const aUserName, aUserEmail, aUserId, aApi, aToken, aRefreshToken: string; aExpiresAt: TDateTime): string; overload;
function UpdateTokenInfo(const aTokenHash, aToken, aRefreshToken: string): Boolean;
function DeleteTokenInfo: Boolean;
function HasTokenHash(out aTokenHash: string): Boolean;
public
// Verifies if user has already logged in before using one of the OAuth APIs
// and, if so, tries to re-validate their access token.
function CheckUserIsLoggedIn: Boolean;
// save user info into user access table and returns the token hash used as the PK of the table
// update token infor stored in our db. This is called by ServerController when
// login is successful
function SaveTokenInfo: string; overload;
// User logoff method. Removes the access cookie from the browser and also deletes
// the record related to OAuth authentication
procedure Logoff;
// Returns True if user is currently logged in using one of the OAuth APIs
// Setting it to False is the same as calling Logoff
property IsLoggedIn: Boolean read FIsLoggedIn write SetIsLoggedIn;
end;
implementation
{$R *.dfm}
uses
IW.Common.AppInfo, IW.Common.Crypt, IW.OAuth.Base;
const
Key = 'UvW*ZH$V(EEWyIqMgX2f';
AccessCookieName = 'access_cookie';
procedure TIWUserSession.IWUserSessionBaseCreate(Sender: TObject);
begin
// initializes our "database". For this example, a simple ClientDataSet
// which persists into a XML file is used.
// In real world applications, a real Database should be used here
FCDSFileName := TIWAppInfo.GetAppPath + 'useraccess.xml';
if FileExists(FCDSFileName) then begin
cdsUserAccess.LoadFromFile(FCDSFileName);
end else begin
cdsUserAccess.CreateDataSet;
end;
cdsUserAccess.LogChanges := False;
end;
function TIWUserSession.CheckUserIsLoggedIn: Boolean;
var
loggedIn: Boolean;
TokenHash,
Api, Token, RefreshToken: string;
begin
// If WebApplication.OAuth.UserInfo is nil here, there is no user information
// so the user hasn't authenticated yet.
// The UserInfo can also exist but has been cleared after a logoff
loggedIn := Assigned(WebApplication.OAuth.UserInfo) and (WebApplication.OAuth.UserInfo.Email <> '');
if not loggedIn then begin
// We check here if there is a token hash contained in an application cookie.
// If we have a token hash, we assume that the user has been granted access before,
// using one of the APIs (a returning user).
// We will then re-authenticate (i.e. Validate) the acess token
// In many cases, the adcess token will need to be refreshed
if HasTokenHash(TokenHash) then begin
// Using the Token hash coming obtained from the cookie, we will lookup into our
// "database" and retrieve the information associated with that access token
// This information will be used to validate the token ahead
if GetTokenInfo(TokenHash, Api, Token, RefreshToken) then begin
// Validate the access token
loggedIn := WebApplication.OAuth.ValidateAccessToken(Api, Token, RefreshToken);
if loggedIn then begin
// If token is still valid, check if it needs to be re-saved to our DB
UpdateTokenInfo(TokenHash, Token, RefreshToken);
end;
end;
end;
end;
IsLoggedIn := loggedIn;
Result := IsLoggedIn;
end;
function TIWUserSession.GetTokenInfo(const aTokenHash: string; out aApi, aToken, aRefreshToken: string): Boolean;
var
encToken, encRefreshToken: string;
begin
// Load the token information from our "database". In a real world application a real database should
// be used here. Using a local file is not safe (both in terms of security and
// also in terms of multi-threading/concurrency)
Result := cdsUserAccess.Locate('TokenHash', aTokenHash, []);
if not Result then
Exit;
aApi := cdsUserAccess.FieldByName('Api').AsString;
encToken := cdsUserAccess.FieldByName('Token').AsString;
encRefreshToken := cdsUserAccess.FieldByName('RefreshToken').AsString;
// The token information has been saved encrypted. We need to decrypt it here
aToken := TIWCrypt.DecryptStringBase64(encToken, Key);
aRefreshToken := TIWCrypt.DecryptStringBase64(encRefreshToken, Key);
end;
function TIWUserSession.SaveTokenInfo: string;
var
UserInfo: TOAuthUserInfo;
Token: TOAuthToken;
begin
UserInfo := WebApplication.OAuth.UserInfo;
Token := WebApplication.OAuth.Token;
Assert(Assigned(UserInfo), 'User info should be assigned');
Assert(Assigned(Token), 'Token should be assigned');
Result := DoSaveTokenInfo(UserInfo.Name,
UserInfo.Email,
UserInfo.Id,
Token.ApiId,
Token.AccessToken,
Token.RefreshToken,
Token.ExpiresAt);
end;
function TIWUserSession.DoSaveTokenInfo(const aUserName, aUserEmail, aUserId, aApi, aToken, aRefreshToken: string; aExpiresAt: TDateTime): string;
var
encToken, encRefreshToken: string;
begin
// calculate the token hash (internally TIWCrypt will use SHA-512 by default)
Result := TIWCrypt.HashString(aToken);
FTokenHash := Result;
// Encrypt the token and the refresh token, using a strong algorithm.
// It is also converted to Base64 string for convenience
encToken := TIWCrypt.EncryptStringBase64(aToken, Key);
encRefreshToken := TIWCrypt.EncryptStringBase64(aRefreshToken, Key);
// Save into our "database". In a real world application a real database should
// be used here. Saving into a local file is not safe (both in terms of security and
// also in terms of multi-threading/concurrency)
with cdsUserAccess do begin
Insert;
FieldByName('TokenHash').AsString := Result;
FieldByName('UserName').AsString := aUserName;
FieldByName('UserEmail').AsString := aUserEmail;
FieldByName('UserId').AsString := aUserId;
FieldByName('Api').AsString := aApi;
FieldByName('Token').AsString := encToken;
FieldByName('RefreshToken').AsString := encRefreshToken;
FieldByName('ExpiresAt').AsDateTime := aExpiresAt;
FieldByName('DateTime').AsDateTime := Now;
Post;
SaveToFile(FCDSFileName, dfXMLUTF8);
end;
// set our cookie which contains a hash of the access token
SaveAccessCookie(Result);
end;
function TIWUserSession.UpdateTokenInfo(const aTokenHash, aToken, aRefreshToken: string): Boolean;
var
encToken, encRefreshToken: string;
begin
Result := cdsUserAccess.Locate('TokenHash', aTokenHash, []);
if not Result then
Exit;
FTokenHash := aTokenHash;
// Encrypt the token and the refresh token, using a strong algorithm
encToken := TIWCrypt.EncryptStringBase64(aToken, Key);
encRefreshToken := TIWCrypt.EncryptStringBase64(aRefreshToken, Key);
// Update our "database" with the token information. It may or may not have changed
with cdsUserAccess do begin
if (FieldByName('Token').AsString <> encToken) or (FieldByName('RefreshToken').AsString <> encRefreshToken) then begin
with cdsUserAccess do begin
Edit;
FieldByName('Token').AsString := encToken;
FieldByName('RefreshToken').AsString := encRefreshToken;
FieldByName('DateTime').AsDateTime := Now;
Post;
SaveToFile(FCDSFileName, dfXMLUTF8);
end;
// also update the cookie value
SaveAccessCookie(aTokenHash);
end;
end;
end;
function TIWUserSession.DeleteTokenInfo: Boolean;
begin
Result := (FTokenHash <> '') and cdsUserAccess.Locate('TokenHash', FTokenHash, []);
if not Result then
Exit;
// Delete the hash info from our DB. Ideally, instead of deleting it, it should just be flagged
// as "inactive" or "invalid"
with cdsUserAccess do begin
Delete;
SaveToFile(FCDSFileName, dfXMLUTF8);
end;
end;
function TIWUserSession.HasTokenHash(out aTokenHash: string): Boolean;
begin
// Checks if there is a cookie in the request which contains our access token hash value
aTokenHash := WebApplication.Request.GetCookieValue(AccessCookieName);
Result := aTokenHash <> '';
end;
procedure TIWUserSession.SaveAccessCookie(const aTokenHash: string);
var
cookieExpireTime: TDateTime;
begin
// this cookie will be valid for 60 days
cookieExpireTime := Now + 60;
// add the cookie to our response.
// Note that is is marked as HttpOnly (can't be read/manipulated by JavaScript code) and Secure (will be sent only over an HTTPS connection)
WebApplication.Response.Cookies.AddCookie(AccessCookieName, aTokenHash, WebApplication.CookiePath, cookieExpireTime, {aHttpOnly=}True, {aSecure=}False);
end;
procedure TIWUserSession.RemoveAccessCokie;
begin
// delete our access cookie from the user browser. This will effectively force the user
// to login again using one of the OAuth APIs
WebApplication.Response.Cookies.RemoveCookie(AccessCookieName, WebApplication.CookiePath);
end;
procedure TIWUserSession.Logoff;
begin
WebApplication.OAuth.UserInfo.ResetToDefaults;
IsLoggedIn := False;
end;
procedure TIWUserSession.SetIsLoggedIn(Value: Boolean);
begin
if Value <> FIsLoggedIn then begin
FIsLoggedIn := Value;
end;
if not FIsLoggedIn then begin
DeleteTokenInfo;
RemoveAccessCokie;
end;
end;
end.