summaryrefslogtreecommitdiff
path: root/packages/fcl-web/src/base/fpoauth2.pp
blob: 6358a4a67c01a9635a85c14202629ed33808f6e9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
{ **********************************************************************
  This file is part of the Free Component Library (FCL)
  Copyright (c) 2015 by the Free Pascal development team
        
  OAuth2 web request handler classes 
            
  See the file COPYING.FPC, included in this distribution,
  for details about the copyright.
                   
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  **********************************************************************}
unit fpoauth2;

{$mode objfpc}{$H+}

interface

uses
  Typinfo,Classes, SysUtils, fpjson, fpjwt, fpwebclient;

Type
  { TOAuth2Config }
  TAccessType = (atOnline,atOffline);
  TAbstracTOAuth2ConfigStore = Class;
  EOAuth2 = Class(Exception);
  { TOAuth2Config }

  { TJWTIDToken }

  TJWTIDToken = Class(TJWT)
  private
    FClaimsClass: TClaimsClass;
    FJOSEClass: TJOSEClass;
  Protected
    Function CreateClaims : TClaims; override;
    Function CreateJOSE : TJOSE; override;
    Property ClaimsClass: TClaimsClass Read FClaimsClass;
    Property JOSEClass: TJOSEClass Read FJOSEClass;
  Public
    // Pass on the actual Claims/JOSE class to be used. When Nil, defaults are used.
    Constructor CreateWithClasses(AClaims: TClaimsClass; AJOSE : TJOSEClass);
    // Extract a unique user ID from the claims. By default, this calls GetUniqueUserName
    Function GetUniqueUserID : String; virtual;
    // Extract a unique user name from the claims. Must be overridden by descendents.
    Function GetUniqueUserName : String; virtual;
    // Extract a user display name from the claims. By default, this calls GetUniqueUserName
    Function GetUserDisplayName : String; virtual;
  end;
  // OAuth2 client and server settings.

  TOAuth2Config = Class(TPersistent)
  private
    FAuthScope: String;
    FAuthURL: String;
    FClientID: String;
    FClientSecret: String;
    FRedirectURI: String;
    FDeveloperKey: String;
    FHostedDomain: String;
    FIncludeGrantedScopes: Boolean;
    FOpenIDRealm: String;
    FTokenURL: String;
    FAccessType: TAccessType;
  Protected
  Public
    Procedure Assign(Source : TPersistent); override;
    Procedure SaveToStrings(L : TStrings);
  Published
    //
    // Local OAuth2 client config part.
    //
    Property ClientID : String Read FClientID Write FClientID;
    Property ClientSecret : String Read FClientSecret Write FClientSecret;
    Property RedirectURI : String Read FRedirectURI Write FRedirectURI;
    Property AccessType : TAccessType Read FAccessType Write FAccessType;
    // Specific for google.
    Property DeveloperKey : String Read FDeveloperKey Write FDeveloperKey;
    Property OpenIDRealm : String Read FOpenIDRealm Write FOpenIDRealm;
    //
    // Auth Provider part
    //
    // Domain part, can be substituted on URL to refresh access token
    Property HostedDomain : String Read FHostedDomain Write FHostedDomain;
    // URL to authenticate a user. used in creating the redirect URL. Can contain %HostedDomain%
    Property AuthURL: String Read FAuthURL Write FAuthURL;
    // URL To exchange authorization code for access token. Can contain %HostedDomain%
    Property TokenURL: String Read FTokenURL Write FTokenURL;
    // Authorized Scopes (Google parlance) or resources (Microsoft parlance)
    Property AuthScope: String Read FAuthScope Write FAuthScope;
    // Google specific: adds AuthScope to existing scopes (incremental increase of authorization).
    Property IncludeGrantedScopes : Boolean Read FIncludeGrantedScopes Write FIncludeGrantedScopes;
  end;
  TOAuth2ConfigClass = Class of TOAuth2Config;

  { TOAuth2Session }
  //
  // User config part
  //

  TOAuth2Session = Class(TPersistent)
  Private
    FRefreshToken: String;
    FLoginHint: String;
    FIDToken: String;
    FState: String;
    FAccessToken: String;
    FAuthTokenType: String;
    FAuthCode: String;
    FAuthExpires: TDateTime;
    FAuthExpiryPeriod: Integer;
    procedure SetAuthExpiryPeriod(AValue: Integer);
  Protected
    Class Function AuthExpiryMargin : Integer; virtual;
    procedure DoLoadFromJSON(AJSON: TJSONObject); virtual;
  Public
    Procedure LoadTokensFromJSONResponse(Const AJSON : String);
    Procedure LoadStartTokensFromVariables(Const Variables : TStrings);
    Procedure SaveToStrings(L : TStrings);
    procedure Assign(Source: TPersistent); override;
  Published
    // Authentication code received at the first step of the OAuth2 sequence
    Property AuthCode: String Read FAuthCode Write FAuthCode;
    // Access token to be used for authorized scopes. Received in step 2 of the OAuth2 sequence;
    Property AccessToken: String Read FAccessToken Write FAccessToken;
    // Refresh token to renew Access token. received in step 2 of the OAuth2 sequence;
    Property RefreshToken : String Read FRefreshToken Write FRefreshToken;
    // When does the authentication end, local time.
    Property AuthExpires : TDateTime Read FAuthExpires Write FAuthExpires;
    // Seconds till access token expires. Setting this will set the AuthExpires property to Now+(AuthExpiryPeriod-AuthExpiryMargin)
    Property AuthExpiryPeriod : Integer Read FAuthExpiryPeriod Write SetAuthExpiryPeriod;
    // Token type (Bearer)
    Property AuthTokenType: String Read FAuthTokenType Write FAuthTokenType;
    // State, saved as part of the user config.
    Property State : String Read FState Write FState;
    // Login hint
    Property LoginHint : String Read FLoginHint Write FLoginHint;
    // IDToken
    Property IDToken : String Read FIDToken Write FIDToken;
  end;
  TOAuth2SessionClass = Class of TOAuth2Session;

  TAbstractOAuth2ConfigStore = CLass(TComponent)
  Public
    Procedure SaveConfig(AConfig : TOAuth2Config); virtual; abstract;
    Procedure LoadConfig(AConfig : TOAuth2Config); virtual; abstract;
    Procedure SaveSession(ASession : TOAuth2Session; Const AUser : String); virtual; abstract;
    Procedure LoadSession(ASession : TOAuth2Session; Const AUser : String); virtual; abstract;
  end;
  TAbstractOAuth2ConfigStoreClass = Class of TAbstractOAuth2ConfigStore;

  TUserConsentHandler = Procedure (Const AURL : String; Out AAuthCode : String) of object;
  TOnAuthConfigChangeHandler = Procedure (Const Sender : TObject; Const AConfig : TOAuth2Config) of object;
  TOnAuthSessionChangeHandler = Procedure (Const Sender : TObject; Const ASession : TOAuth2Session) of object;
  TOnIDTokenChangeHandler = Procedure (Const Sender : TObject; Const AToken : TJWTIDToken) of object;
  TSignRequestHandler = Procedure (Const Sender : TObject; Const ARequest : TWebClientRequest)of object;

  TAuthenticateAction = (aaContinue,aaRedirect,aaFail);

  { TOAuth2Handler }

  TOAuth2Handler = Class(TAbstractRequestSigner)
  private
    FAutoConfig: Boolean;
    FAutoSession: Boolean;
    FConfigLoaded: Boolean;
    FSessionLoaded: Boolean;
    FClaimsClass: TClaimsClass;
    FConfig: TOAuth2Config;
    FSession: TOAuth2Session;
    FIDToken: TJWTIDToken;
    FWebClient: TAbstractWebClient;
    FStore : TAbstracTOAuth2ConfigStore;
    FOnAuthSessionChange: TOnAuthSessionChangeHandler;
    FOnIDTokenChange: TOnIDTokenChangeHandler;
    FOnSignRequest: TOnAuthConfigChangeHandler;
    FOnAuthConfigChange: TOnAuthConfigChangeHandler;
    FOnUserConsent: TUserConsentHandler;
    Function GetAutoStore : Boolean;
    Procedure SetAutoStore(AValue : Boolean); 
    procedure SetConfig(AValue: TOAuth2Config);
    procedure SetSession(AValue: TOAuth2Session);
    procedure SetStore(AValue: TAbstracTOAuth2ConfigStore);
  Protected
    function CheckHostedDomain(URL: String): String; virtual;
    Function RefreshToken: Boolean; virtual;
    Function CreateOauth2Config : TOAuth2Config; virtual;
    Function CreateOauth2Session : TOAuth2Session; virtual;
    Function CreateIDToken : TJWTIDToken; virtual;
    Procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    Procedure DoAuthConfigChange; virtual;
    Procedure DoAuthSessionChange(Const AUser : String = ''); virtual;
    Procedure DoSignRequest(ARequest: TWebClientRequest); override;
    Property ConfigLoaded : Boolean Read FConfigLoaded;
    Property SessionLoaded : Boolean Read FSessionLoaded;
  Public
    Class Var DefaultConfigClass : TOAuth2ConfigClass;
    Class Var DefaultSessionClass : TOAuth2SessionClass;
  Public
    Constructor Create(AOwner : TComponent);override;
    Destructor Destroy; override;
    // Variable name for AuthScope in authentication URL.
    // Default = scope. Descendents can override this to provide correct behaviour.
    Class Function AuthScopeVariableName : String; virtual;
    // Default for hosted domain, if any
    Class function DefaultHostedDomain: String; virtual;
    // Check if config is authenticated.
    Function IsAuthenticated : Boolean; virtual;
    // Generate an authentication URL
    Function AuthenticateURL : String; virtual;
    // Check what needs to be done for authentication.
    // Do whatever is necessary to mark the request as 'authenticated'.
    Function Authenticate: TAuthenticateAction; virtual;
    // Load config from store
    procedure LoadConfig(Force : Boolean = false);
    // Save config to store
    procedure SaveConfig;
    // Load Session from store.If AUser is empty, then ID Token.GetUniqueUser is used. 
    procedure LoadSession(Const AUser : String = ''; AForce : Boolean = False);
    // Save session in store. If AUser is empty, then ID Token.GetUniqueUser is used. Will call OnAuthSessionChange
    procedure SaveSession(Const AUser : String = '');
    // Refresh ID token from Session.IDToken. Called after token is refreshed or session is loaded.
    // This will change the actual IDToken instance.
    procedure RefreshIDToken;
    // This is populated from Config.IDToken if it is not empty. Do not cache this instance. It is recreated after a call to RefreshIDToken
    Property IDToken : TJWTIDToken Read FIDToken;
    // Set this to initialize the claims for the ID token. By default, it is TClaims
    Property ClaimsClass : TClaimsClass Read FClaimsClass Write FClaimsClass;
  Published
    // Must be set prior to calling
    Property Config : TOAuth2Config Read FConfig Write SetConfig;
    // Session info.
    Property Session : TOAuth2Session Read FSession Write SetSession;
    // Webclient used to do requests to authorization service
    Property WebClient : TAbstractWebClient Read FWebClient Write FWebClient;
    // Event handler to get user consent if no access token or refresh token is available
    Property OnUserConsent : TUserConsentHandler Read FOnUserConsent Write FOnUserConsent;
    // Called when the auth config informaion changes
    Property OnAuthConfigChange : TOnAuthConfigChangeHandler Read FOnAuthConfigChange Write FOnAuthConfigChange;
    // Called when the auth sesson information changes
    Property OnAuthSessionChange : TOnAuthSessionChangeHandler Read FOnAuthSessionChange Write FOnAuthSessionChange;
    // Called when the IDToken information changes
    Property OnIDTokenChange : TOnIDTokenChangeHandler Read FOnIDTokenChange Write FOnIDTokenChange;
    // Called when a request is signed
    Property OnSignRequest : TOnAuthConfigChangeHandler Read FOnSignRequest Write FOnSignRequest;
    // User to load/store parts of the config store.
    Property Store : TAbstracTOAuth2ConfigStore Read FStore Write SetStore;
    // Call storing session/config automatically when needed.
    Property AutoStore : Boolean Read GetAutoStore Write SetAutoStore;
    // AutoSession = True makes sure the load/save of the session as needed.
    Property AutoSession : Boolean Read FAutoSession Write FAutoSession default True;
    // AutoConfig = True will enable the load of config as needed.
    Property AutoConfig : Boolean Read FAutoConfig Write FAutoConfig default True;
  end;
  TOAuth2HandlerClass = Class of TOAuth2Handler;



implementation

uses httpdefs;

Resourcestring
  SErrFailedToRefreshToken = 'Failed to refresh access token: Status %d, Error: %s';

{ TOAuth2Handler }

{ Several possibilities:
  1. Acess token is available.
     A) Access token is not yet expired
        -> All is well, continue.
     B) Access token is available, but is expired.
        Refresh token is
          i) Available
             -> get new access token using refresh token.
             (may fail -> fail)
          ii) Not available
              -> error.
  3. No access token is available.
     A) Offline
        -> Need to get user consent using callback.
        i) User consent results in Access token (AConfig.AuthToken)
           ->  Auth token is exchanged for a refresh token & access token
        ii) User consent failed or no callback.
           -> Fail
     B) Online: Need to redirect to get access token and auth token.

}

{ TTWTIDToken }

constructor TJWTIDToken.CreateWithClasses(AClaims: TClaimsClass;
  AJOSE: TJOSEClass);
begin
  FClaimsClass:=AClaims;
  FJOSEClass:=AJOSE;
  Inherited Create;
end;

function TJWTIDToken.GetUniqueUserID: String;
begin
  Result:=GetUniqueUserName;
end;

function TJWTIDToken.GetUniqueUserName: String;
begin
  Result:='';
end;

function TJWTIDToken.GetUserDisplayName: String;
begin
  Result:=GetUniqueUserName;
end;

function TJWTIDToken.CreateClaims: TClaims;
begin
  if FClaimsClass=Nil then
    Result:=Inherited CreateClaims
  else
    Result:=FClaimsClass.Create;
end;

function TJWTIDToken.CreateJOSE: TJOSE;
begin
  if FJOSEClass=Nil then
    Result:=Inherited CreateJOSE
  else
  Result:=FJOSEClass.Create;
end;

function TOAuth2Handler.Authenticate: TAuthenticateAction;

Var
  S : String;

begin
  if IsAuthenticated then
    result:=aaContinue
  else
    Case Config.AccessType of
      atonline :
        Result:=aaRedirect; // we need to let the user authenticate himself.
      atoffline :
        if Not Assigned(FOnUserConsent) then
          result:=aaFail
        else
          begin
          FOnUserConsent(AuthenticateURL,S);
          Session.AuthCode:=S;
          // Exchange authcode for access code.
          if IsAuthenticated then
            result:=aaContinue
          else
            result:=aaFail
          end;
    end;
end;

function TOAuth2Handler.CheckHostedDomain(URL : String): String;

Var
  HD : String;

begin
  HD:=Config.HostedDomain;
  if (HD='') then
    Result:=DefaultHostedDomain;
  Result:=StringReplace(URL,'%HostedDomain%',Config.HostedDomain,[rfIgnoreCase]);
end;

Class function TOAuth2Handler.DefaultHostedDomain : String;

begin
  Result:='';
end;

function TOAuth2Handler.AuthenticateURL: String;

begin
  Result:=Config.AuthURL
        + '?'+ AuthScopeVariableName+'='+HTTPEncode(Config.AuthScope)
        +'&redirect_uri='+HTTPEncode(Config.RedirectUri)
        +'&client_id='+HTTPEncode(Config.ClientID)
        +'&response_type=code'; // Request refresh token.
  Result:=CheckHostedDomain(Result);
  if Assigned(Session) then
    begin
    if (Session.LoginHint<>'') then
      Result:=Result +'&login_hint='+HTTPEncode(Session.LoginHint);
    if (Session.State<>'') then
      Result:=Result +'&state='+HTTPEncode(Session.State);
    end;
end;

procedure TOAuth2Handler.SetConfig(AValue: TOAuth2Config);

begin
  if FConfig=AValue then Exit;
  FConfig.Assign(AValue);
end;

procedure TOAuth2Handler.SetSession(AValue: TOAuth2Session);
begin
  if FSession=AValue then Exit;
  FSession.Assign(AValue);
end;

procedure TOAuth2Handler.LoadConfig(Force : Boolean = False);

begin
  if Assigned(Store) then
    if Force or not ConfigLoaded then
      begin
      Store.LoadConfig(Config);
      FConfigLoaded:=True;
      end;
end;

procedure TOAuth2Handler.SaveConfig;
begin
  if Assigned(Store) then
    begin
    Store.SaveConfig(Config);
    FConfigLoaded:=True;
    end;
end;

procedure TOAuth2Handler.LoadSession(const AUser: String; AForce : Boolean = False);

Var
  U : String;

begin
  if Assigned(Store) then
    if AForce or Not SessionLoaded then
      begin
      U:=AUser;
      If (U='') and Assigned(FIDToken) then
        U:=FIDToken.GetUniqueUserID;
      Store.LoadSession(Session,AUser);
      FSessionLoaded:=True;
      if (Session.IDToken<>'') then
        RefreshIDToken;
      end;
end;

procedure TOAuth2Handler.SaveSession(const AUser: String);

Var
  U : String;

begin
  if Assigned(FOnAuthSessionChange) then
    FOnAuthSessionChange(Self,Session);
  if Assigned(Store) then
    begin
    Store.SaveSession(Session,AUser);
    FSessionLoaded:=True;
    end;
end;

Function TOAuth2Handler.GetAutoStore : Boolean;

begin
  Result:=AutoSession and AutoConfig;
end;

Procedure TOAuth2Handler.SetAutoStore(AValue : Boolean); 

begin
  AutoSession:=True;
  AutoConfig:=True;
end;

procedure TOAuth2Handler.RefreshIDToken;
begin
  FreeAndNil(FIDToken);
  if (Session.IDToken<>'') then
    begin
    FIDtoken:=CreateIDToken;
    FIDToken.AsEncodedString:=Session.IDToken;
    If Assigned(FOnIDTokenChange) then
      FOnIDTokenChange(Self,FIDToken);
    end;
end;

function TOAuth2Handler.RefreshToken: Boolean;

Var
  URL,Body : String;
  D : TJSONData;
  Req: TWebClientRequest;
  Resp: TWebClientResponse;

begin
  if AutoConfig and not ConfigLoaded then
    LoadConfig;
  Req:=Nil;
  Resp:=Nil;
  D:=Nil;
  try
    Req:=WebClient.CreateRequest;
    Req.Headers.Values['Content-Type']:='application/x-www-form-urlencoded';
    url:=CheckHostedDomain(Config.TOKENURL);
    Body:='client_id='+HTTPEncode(Config.ClientID)+
          '&client_secret='+ HTTPEncode(Config.ClientSecret);
    if (Session.RefreshToken<>'') then
      body:=Body+'&refresh_token='+HTTPEncode(Session.RefreshToken)+
                 '&grant_type=refresh_token'
    else
      begin
      body:=Body+
            '&grant_type=authorization_code'+
            '&redirect_uri='+HTTPEncode(Config.RedirectUri)+
            '&code='+HTTPEncode(Session.AuthCode);
      end;
    Req.SetContentFromString(Body);
    Resp:=WebClient.ExecuteRequest('POST',url,Req);
    Result:=(Resp.StatusCode=200);
    if Result then
      begin
      Session.LoadTokensFromJSONResponse(Resp.GetContentAsString);
      If (Session.IDToken<>'') then
        begin
        RefreshIDToken;
        if AutoSession then
          DoAuthSessionChange(IDToken.GetUniqueUserName);
        end;
      end
    else
      Raise EOAuth2.CreateFmt(SErrFailedToRefreshToken,[Resp.StatusCode,Resp.StatusText]);
    Result:=True;
  finally
    D.Free;
    Resp.Free;
    Req.Free;
  end;
end;

function TOAuth2Handler.CreateOauth2Config: TOAuth2Config;
begin
  Result:=DefaultConfigClass.Create;
end;

function TOAuth2Handler.CreateOauth2Session: TOAuth2Session;
begin
  Result:=DefaultSessionClass.Create;
end;

function TOAuth2Handler.CreateIDToken: TJWTIDToken;
begin
  Result:=TJWTIDToken.CreateWithClasses(ClaimsClass,Nil);
end;

procedure TOAuth2Handler.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation=opRemove) then
    if AComponent=FStore then
      FStore:=Nil;
end;

function TOAuth2Handler.IsAuthenticated: Boolean;

begin
  If AutoConfig then
    LoadConfig;
  // See if we need to load the session
  if (Session.RefreshToken='') and AutoSession then
    LoadSession;
  Result:=(Session.AccessToken<>'');
  If Result then
    // have access token. Check if it is still valid.
    begin
    // Not expired ?
    Result:=(Now<Session.AuthExpires);
    // Expired, but have refresh token ?
    if (not Result) and (Session.RefreshToken<>'') then
      Result:=RefreshToken;
    end
  else if (Session.RefreshToken<>'') then
    begin
    // No access token, but have refresh token
    Result:=RefreshToken;
    end
  else  if (Session.AuthCode<>'') then
    // No access or refresh token, but have auth code.
      Result:=RefreshToken;
end;


{ TOAuth2Handler }


procedure TOAuth2Handler.DoAuthConfigChange;
begin
  If Assigned(FOnAuthConfigChange) then
    FOnAuthConfigChange(Self,Config);
  SaveConfig;
end;

procedure TOAuth2Handler.DoAuthSessionChange(Const AUser : String = ''); 
    
begin
  If Assigned(FOnAuthSessionChange) then
    FOnAuthSessionChange(Self,Session);
  SaveSession(AUser);
end;

procedure TOAuth2Handler.DoSignRequest(ARequest: TWebClientRequest);

Var
  TT,AT : String;
begin
  if Authenticate=aaContinue then
    begin
    TT:=Session.AuthTokenType;
    AT:=Session.AccessToken;
    Arequest.Headers.Add('Authorization: '+TT+' '+HTTPEncode(AT));
    end
  else
    Raise EOAuth2.Create('Cannot sign request: not authorized');
end;

constructor TOAuth2Handler.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FConfig:=CreateOauth2Config;
  FSession:=CreateOauth2Session;
  FAutoSession:=True;
  FAutoConfig:=True;
end;

destructor TOAuth2Handler.Destroy;
begin
  FreeAndNil(FIDToken);
  FreeAndNil(FConfig);
  FreeAndNil(FSession);
  inherited Destroy;
end;

class function TOAuth2Handler.AuthScopeVariableName: String;
begin
  Result:='scope';
end;


{ TOAuth2Config }

procedure TOAuth2Handler.SetStore(AValue: TAbstracTOAuth2ConfigStore);
begin
  if FStore=AValue then Exit;
  if Assigned(FStore) then
    FStore.RemoveFreeNotification(Self);
  FStore:=AValue;
  if Assigned(FStore) then
    FStore.FreeNotification(Self);
end;

class function TOAuth2Session.AuthExpiryMargin: Integer;
begin
  Result:=10;
end;

procedure TOAuth2Session.SetAuthExpiryPeriod(AValue: Integer);
begin
  if FAuthExpiryPeriod=AValue then Exit;
  FAuthExpiryPeriod:=AValue;
  AuthExpires:=Now+AValue/SecsPerDay;
end;


procedure TOAuth2Config.Assign(Source: TPersistent);

Var
  C : TOAuth2Config;

begin
  if Source is TOAuth2Config then
    begin
    C:=Source as TOAuth2Config;
    FAuthURL:=C.AuthURL;
    FTokenURL:=C.TokenURL;
    FClientID:=C.ClientID;
    FClientSecret:=C.ClientSecret;
    FRedirectURI:=C.RedirectURI;
    FAccessType:=C.AccessType;
    FDeveloperKey:=C.DeveloperKey;
    FHostedDomain:=C.HostedDomain;
    FIncludeGrantedScopes:=C.IncludeGrantedScopes;
    FOpenIDRealm:=C.OpenIDRealm;
    FAuthScope:=C.AuthScope;
    end
  else
    inherited Assign(Source);
end;

procedure TOAuth2Config.SaveToStrings(L: TStrings);
  Procedure W(N,V : String);

  begin
    L.Add(N+'='+V);
  end;

begin
  W('AuthURL',AuthURL);
  W('TokenURL',TokenURL);
  W('ClientID',ClientID);
  W('ClientSecret',ClientSecret);
  W('RedirectURI',RedirectURI);
  W('AccessType',GetEnumName(TypeInfo(TAccessType),Ord(AccessType)));
  W('DeveloperKey',DeveloperKey);
  W('HostedDomain',HostedDomain);
  W('IncludeGrantedScopes',BoolToStr(IncludeGrantedScopes,True));
  W('OpenIDRealm',OpenIDRealm);
  W('AuthScope',AuthScope);
end;

procedure TOAuth2Session.SaveToStrings(L: TStrings);

  Procedure W(N,V : String);

  begin
    L.Add(N+'='+V);
  end;

begin
  W('AuthCode',AuthCode);
  W('RefreshToken',RefreshToken);
  W('LoginHint',LoginHint);
  W('IDToken',IDToken);
  W('AccessToken',AccessToken);
  W('AuthExpiryPeriod',IntToStr(AuthExpiryPeriod));
  W('AuthExpires',DateTimeToStr(AuthExpires));
  W('State',State);
  W('AuthTokenType',AuthTokenType);
end;

procedure TOAuth2Session.Assign(Source: TPersistent);

Var
  C : TOAuth2Session;

begin
  if Source is TOAuth2Session then
    begin
    C:=Source as TOAuth2Session;
    FAuthCode:=C.AuthCode;
    FRefreshToken:=C.RefreshToken;
    FLoginHint:=C.LoginHint;
    FIDToken:=C.IDToken;
    FAccessToken:=C.AccessToken;
    FAuthExpiryPeriod:=C.AuthExpiryPeriod;
    FAuthExpires:=C.AuthExpires;
    FState:=C.State;
    FAuthTokenType:=C.AuthTokenType;
    end
  else
    inherited Assign(Source);
end;


procedure TOAuth2Session.DoLoadFromJSON(AJSON: TJSONObject);

  Function Get(Const AName,ADefault : String) : String;

  begin
    Result:=AJSON.Get(AName,ADefault);
  end;

Var
  i : Integer;

begin
  AccessToken:=Get('access_token',AccessToken);
  RefreshToken:=Get('refresh_token',RefreshToken);
  AuthTokenType:=Get('token_type',AuthTokenType);
  IDToken:=Get('id_token',IDToken);
  // Microsoft sends expires_in as String !!
  I:=AJSON.IndexOfName('expires_in');
  if (I<>-1) then
    begin
    I:=AJSON.Items[i].AsInteger;
    if (I>0) then
      AuthExpiryPeriod:=I;
    end;
end;

procedure TOAuth2Session.LoadTokensFromJSONResponse(const AJSON: String);

Var
  D : TJSONData;

begin
  D:=GetJSON(AJSON);
  try
    DoLoadFromJSON(D as TJSONObject);
  finally
    D.Free;
  end;
end;

procedure TOAuth2Session.LoadStartTokensFromVariables(const Variables: TStrings);

  Function Get(Const AName,ADefault : String) : String;

  Var
    I : Integer;

  begin
    I:=Variables.IndexOfName(AName);
    if I=-1 then
      Result:=ADefault
    else
      Result:=Variables.ValueFromIndex[i];
  end;

begin
  AuthCode:=Get('code',AuthCode);
  LoginHint:=Get('login_hint',LoginHint);
end;


initialization
  TOAuth2Handler.DefaultConfigClass:=TOAuth2Config;
  TOAuth2Handler.DefaultSessionClass:=TOAuth2Session;
end.