{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Author: Angus Robertson, Magenta Systems Ltd
Description: OAuth2 and OAuth1A authentication, and components to send cloud
email and Tweets, all using the TSslHttpRest component.
Creation: March 2022
Updated: Jan 2023
Version: 8.71
EMail: francois.piette@overbyte.be http://www.overbyte.be
Support: https://en.delphipraxis.net/forum/37-ics-internet-component-suite/
Legal issues: Copyright (C) 2023 by Angus Robertson, Magenta Systems Ltd,
Croydon, England. delphi@magsys.co.uk, https://www.magsys.co.uk/delphi/
This software is provided 'as-is', without any express or
implied warranty. In no event will the author be held liable
for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any
purpose, including commercial applications, and to alter it
and redistribute it freely, subject to the following
restrictions:
1. The origin of this software must not be misrepresented,
you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but is
not required.
2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
4. You must register this software by sending a picture postcard
to the author. Use a nice stamp and mention your name, street
address, EMail address and any comment you like to say.
Overview
--------
TSimpleWebSrv
-------------
This is a simple web server primarily designed for accepting HTTP and HTTPS requests
from REST servers which don't expect real pages to be sent, but also for .well-known
responses generated by applications. Allows listening on multiple IPs and ports.
TRestOAuth
----------
This for handling 0Auth authorization to web apps, by several means. Beware
OAuth is really a concept with differing implementations, so that implementation
may not always be straight forward. OAuth1 and 1A were originally developed for
Twitter and use cryptography, OAuth2 is a simpler and easier to implement version
now widely used by most cloud services without any cryptography (other than SSL).
The conceptual issue about OAuth is that applications should not know any login
details. The login need to be entered through a browser, which then redirects to
a fixed URL which includes an Authorization Code that is subsequently exchanged
for an Access Token that can used by the REST client. Note that Authorization
Codes expire in a few minutes and are immediately exchanged for an Access Token.
This is really all designed for interactive applications, on mobile platforms
in particular. The Access Token is then sent with all HTTPS REST requests as
an 'Authorization: Bearer' header.
Access Tokens often have a limited life and may expire within one to 24 hours.
To avoid user interaction, the token exchange process usually offers a Refresh
Token which can be used to get another Access Token, and this is automatically
handled by TRestOAuth, by refreshing the Access Token before it expires, allowing
your application to keep running. Store the Refresh Token securely as if it were
a password, since it's a potential security risk, it can be easily cancelled if
compromised.
Sometimes the Refresh Token has the same life as the Access Token, with Google
Accounts the Refresh Token remains valid for a few months until the account is
disabled or changed, avoiding needing to login again or refresh within the expiry
period. Beware with Google the Refresh Token is only returned once after initial
login, not after each refresh. Google may also need to approve applications
offering OAuth2, and may show consent warnings during the login process to get
an Authorization Code until this is done.
https://developers.google.com/identity/protocols/OAuth2
Setting up OAuth is complex and requires a lot more information than just a site
user name and password. You normally need to access the desired site and create
an app or client (terminology varies) but will always involve creating a client
ID and client secret, and a redirect URL which will be the local web server. The
default redirect used by TRestOAuth is http:/localhost:8080/. There are also
two API URLs, one for the authorization endpoint (displayed in the browser) and
then the token exchange endpoint for REST requests. Some sites may provide OAuth2
details with the URL (host)/.well-known/openid-configuration as Json, ie:
https://accounts.google.com/.well-known/openid-configuration . Finally, OAuth
may require the token Scope to be specified, it's purpose or access rights
depending on the server. This component includes TOAuthUri records that are
designed to set-up common OAuth2 account settings for Google, Microsoft and
other end points.
Note that in addition to granting tokens using an Authorization Code from a
browser login, some OAuth implementations may support grants for client
credentials alone (ID and secret, without a login) or directly for login and
password (and client ID and secret) which is by far the easiest to use, but not
often available, both are supported by TRestOAuth.
Embedded or Standard Browser
----------------------------
Originally it was considered allowable for native applications to display an
embedded browser window in the application to capture the Authorization Code
during redirect. But that potentially means the application can also capture the
login as well so is no longer best practice, see RFC8252, and some apps will
block the embedded window. The preferred authorization method is for the native
application to launch the standard browser and redirect to localhost where a
small web server runs to capture the Authorization Code.
TRestOAuth supports both embedded and standard browsers, the embedded browser gives
a better user experience with the windows closing automatically once authentication
is complete and not needing a local web server, but may not be supported by Windows
or end points. Launching a web page into the standard browser may replace a page
being viewed, there may be firewall or other problems connecting to the localhost
web server and the browser window remains open upon completion. So the end user
should ideally be given a choice of which browser to use.
ICS includes TOAuthLoginForm which displays browser pages to handle OAuth login.
It supports two browser engines with Delphi 10.4 and later, TEdgeBrowser Chromium
based browser introduced in 2020 for Windows 10 and 11 which should be on most
recent PCs provided Windows Update is used, and the older TWebBrowser using
Microsoft's Shell Doc Object and Control Library (SHDOCVW.DLL), part of Internet
Explorer, which is removed when Edge is installed, but still seems to be available,
and is used on older Windows versions. Edge Chromium can be installed on Windows
7 and later. The form checks for Edge in the registry and for the WebView2Loader.dll,
otherwise uses TWebBrowser.
Note Google no longer supports authentication using TWebBrowser, you will get
script errors and a warning to use another browser, and announced it would no
longer support embedded browsers at all, but a year later Edge still seems to work:
https://developers.googleblog.com/2021/06/upcoming-security-changes-to-googles-oauth-2.0-authorization-endpoint.html
Officially the Microsoft.Web.WebView2 runtime (from GetIt) must be installed for
Edge Chromium to work, but in practice copying WebView2Loader.dll into the same
directory as the executable seems to work, there are Win32 and Win64 versions of
this DLL with the same name, you need the correct version for the build!
Google and Microsoft OAuth2 Email Application Accounts
------------------------------------------------------
To access email using REST APIs or OAuth2/SMTP/POP3 an 'application account'
needs to be created though the provider console. This is generally done once
by the developer and the application API ID and secret are then distributed
with the application (usually hidden). These are then used by OAuth2 when
logging in with an end user account. Note end users don't need to access
the provider console or know it exists.
For Google, the console is https://console.developers.google.com/. Go to
Credentials, Create New Credentials, Create OAuth client ID, Web Application,
name the application something like GMailApp, generate the Client Id and Client
Secret, set the Authorised redirect URI to http://localhost:8080/gmail/ and
enable the Gmail API for your account. All this information enables the
application using this component to access Gmail, once it has also logged
into a Gmail user account usually different to the application account. The
user account password is unknown to the application, the login process returns
temporary tokens that are used instead of a password. During the login process
the end user will be asked to give access to the application, which is why the
name must be recognisable. Google may also need to test your final application
to avoid warnings.
For Microsoft, termininology is more complicated, the console is Microsoft Azure,
https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
while email is called Microsoft Outlook or Office 365 with the APIs called Micosoft
Graph. Permissions and scopes are horrible, still not sure I understand them.
In the Azure console, take App Registrations, New Registration, app name something
like OutLookEmail, redirect URI is http://localhost:8080/microsoft/. Microsoft
also has User Authorities for different types of accounts, defaults to 'consumers'
for Live mail atc, but can be changed to 'common' or an Azure Active Directory
tenant GUID for corporate accounts. Keep the Application (Client) ID offered,
take Certificate & secrets, create client secret and keep that as well, expires
never. Other settings are needed to allow Microsoft to authenticate your final
application. Supported account types should include Multitenant for Office 365,
otherwse just consumer accounts. API permissions should be set to read and
write Mail, SMTP and POP, also profile.
With Google, you can not specify which account the users logs into using the
browser, but after login the component retrieves the email acount name in NewEmail.
With Microsoft, you can provide a user name hint that should cause the browser
to offer that name, but the component can only retrieve the email account
using the claim 'profile' which does not seem to work with SMTP/POP3 claims.
After an OAuth2 browser login, you must save the Refresh Token securely since
it is effectively a password to access the user account from this application
account for months from multiple devices. For unattended applications or
services, the Refresh Token may be copied from another device, beware the token
may finally expire after several months or be cancelled, and will need a fresh
login for a new Refresh Token.
The Access Token need not be saved, for Google and Microsoft it expires after
one hour and will be automatically updated using the Refresh Token.
To simplify loading OAuth2 settings, there is a function IcsLoadRestEmailFromIni
that loads from section RestEmail as follows:
{ default INI section
[RestEmail]
RestEmailType=RestEmailGoogle
ClientId=
ClientSecret=
RefrToken=
MsUserAuth=consumers
OAEdgeCacheDir=
You need to add your specific account settings to the INI file, ideally you
should replaced this function with one that encrypts these settings since
they are private to the developer.
Updates:
May 26, 2022 - V8.69 - baseline, split from OverbyteIcsSslHttpRest.pas unit, moved
TRestOAuth, TSimpleWebSrv, TIcsTwitter and TIcsRestEmail
components here to ease maintenance and use.
Oct 25, 2022 - V8.70 - Added IcsLoadRestEmailFromIni to load TIcsRestEmail secrets
and tokens from INI file. Ideally these settings should
be encrypted!!
Added OAuth2 MsUserAuthority and RestEmail MsUserAuth properties
to access different Microsoft User Authorities, defaults to
consumers but can be changed to common or an Azure Active
Directory tenant GUID for corporate accounts. Note this
requires supported account types include Multitenant.
Log expected RedirectUrl so user can check it's configured
correctly in console.
Jul 19, 2022 - V8.71 - Support TRestOAuth authentication OAuthTypeEmbed (embedded browser) using
new TOAuthBrowser component and TOAuthLoginForm window, that uses
TEdgeBrowser or TWebBrowser to display web page without needing
external browser or web server, traps redirect request to get
authentication code, Edge is Delphi 10.4 or later only.
To use the embedded browser, drop a TOAuthBrowser component on the form
and call it from the TRestOAuth FOnOAuthEmbed or TIcsRestEmail
FOnOAEmbed properties (see REST sample).
Added TRestOAuth EdgeCacheDir property to specify a working directory for
Edge to cache file, if left blank uses the system work directory.
The TRestOAuth and TIcsTwitter StartAuthorization functions now have an
optional Sender parameter that may be set to the form calling it which
is passed to the TOAuthLoginForm window, also the TIcsRestEmail
GetNewToken function.
Note TRestOAuth the LoginHint property is passed to the TOAuthLoginForm
window, displayed and copied to the clipboard so it may be pasted into
the login account field. For Google, it should appear automatically.
For TIcsRestEmail, set the AccountHint property instead.
Don't URL encode OAuth grant_types, some servers don't like that.
Log the parameters passed to OAuth grant requests.
Using Int64 ticks.
pending - login to Twitter fails due to no posted data, but sending tweets works with token..
Note - FMX applications require the FMX conditional to ensure the correct OAuth form is linked.
Pending - more documentation
Pending - OAuth don't spawn browser from Windows service
}
{$IFNDEF ICS_INCLUDE_MODE}
unit OverbyteIcsSslHttpOAuth;
{$ENDIF}
{$I Include\OverbyteIcsDefs.inc}
{$IFDEF COMPILER14_UP}
{$IFDEF NO_EXTENDED_RTTI}
{$RTTI EXPLICIT METHODS([]) FIELDS([]) PROPERTIES([])}
{$ENDIF}
{$ENDIF}
{$B-} { Enable partial boolean evaluation }
{$T-} { Untyped pointers }
{$X+} { Enable extended syntax }
{$H+} { Use long strings }
{$IFDEF BCB}
{$ObjExportAll On}
{$ENDIF}
interface
{$IFDEF USE_SSL}
uses
{$IFDEF MSWINDOWS}
{$IFDEF RTL_NAMESPACES}Winapi.Messages{$ELSE}Messages{$ENDIF},
{$IFDEF RTL_NAMESPACES}Winapi.Windows{$ELSE}Windows{$ENDIF},
{$IFDEF RTL_NAMESPACES}Winapi.ShellAPI{$ELSE}ShellAPI{$ENDIF},
{$IFDEF RTL_NAMESPACES}System.IniFiles{$ELSE}IniFiles{$ENDIF}, { V8.70 }
{$IFDEF COMPILER16_UP}System.IOUtils,{$ENDIF} { V8.71 }
{$ENDIF}
{$IFDEF POSIX}
Posix.Time,
System.IOUtils, { V8.69 }
System.IniFiles, { V8.70 }
Ics.Posix.WinTypes,
Ics.Posix.PXMessages,
{$ENDIF}
{$IFDEF RTL_NAMESPACES}System.Classes{$ELSE}Classes{$ENDIF},
{$IFDEF RTL_NAMESPACES}System.Sysutils{$ELSE}Sysutils{$ENDIF},
{$IFDEF RTL_NAMESPACES}System.TypInfo{$ELSE}TypInfo{$ENDIF},
{$IFDEF RTL_NAMESPACES}System.UITypes, System.UIConsts,{$ENDIF} { V8.69 }
OverbyteIcsSsleay, OverbyteIcsLibeay,
{$IFDEF YuOpenSSL}YuOpenSSL,{$ENDIF YuOpenSSL}
OverbyteIcsWinsock,
OverbyteIcsTypes,
OverbyteIcsUtils,
OverbyteIcsUrl,
{$IFDEF FMX}
Ics.Fmx.OverbyteIcsWndControl,
Ics.Fmx.OverbyteIcsWSocket,
Ics.Fmx.OverbyteIcsWSocketS,
Ics.Fmx.OverbyteIcsHttpProt,
Ics.Fmx.OverbyteIcsSslHttpRest,
Ics.Fmx.OverbyteIcsSslJose,
{$ELSE}
OverbyteIcsWndControl,
OverbyteIcsWSocket,
OverbyteIcsWSocketS,
OverbyteIcsHttpProt,
OverbyteIcsSslHttpRest,
OverbyteIcsSslJose,
{$ENDIF FMX}
OverbyteIcsLogger, { for TLogOption }
OverbyteIcsFormDataDecoder,
OverbyteIcsSuperObject,
OverbyteIcsTicks64,
OverbyteIcsStreams; { V8.68 }
{ NOTE - these components only build with SSL, there is no non-SSL option }
const
THttpOAuthVersion = 871;
CopyRight : String = ' TSslHttpOAuth (c) 2023 F. Piette V8.71 ';
TestState = 'Testing-Redirect';
MimeAppCert = 'application/pkix-cert'; { V8.69 }
OAuthErrBase = {$IFDEF MSWINDOWS} 1 {$ELSE} 1061 {$ENDIF};
OAuthErrNoError = 0;
OAuthErrParams = OAuthErrBase;
OAuthErrBadGrant = OAuthErrBase+1;
OAuthErrWebSrv = OAuthErrBase+2;
OAuthErrBrowser = OAuthErrBase+3;
OAuthErrEvent = OAuthErrBase+4; { V8.65 }
OAuthErrCancelled = OAuthErrBase+5; { V8.71 }
type
{ event handlers }
TSimpleWebSrvReqEvent = procedure (Sender: TObject; const Host, Path, Params: string; var RespCode, Body: string) of object;
TOAuthAuthUrlEvent = procedure (Sender: TObject; const URL: string) of object;
TOAuthEmbedBrowEvent = procedure (Sender: TObject; const URL: string; var Success: Boolean) of object; { V8.71 }
{ property and state types }
TOAuthProto = (OAuthv1, OAuthv1A, OAuthv2);
TOAuthType = (OAuthTypeWeb, OAuthTypeMan, OAuthTypeEmbed);
TOAuthOption = (OAopAuthNoRedir, { OAuth Auth Request do not send redirect_url }
OAopAuthNoScope, { OAuth Auth Request do not send scope }
OAopAuthNoState, { OAuth Auth Request do not send state }
OAopAuthPrompt, { OAuth Auth Request send approval prompt V8.63 }
OAopAuthAccess, { OAuth Auth Request send access type V8.63 }
OAopAuthGrantedScope, { OAuth Auth include granted scope V8.65 Google }
OAopAuthRespMode, { OAuth Auth Request send resp_mode V8.65 Microsoft }
OAopAuthLoginHint); { OAuth Auth Request send resp_mode V8.65 Microsoft }
TOAuthOptions = set of TOAuthOption;
{ TSimpleWebSrv is a simple web server primarily designed for accepting
requests from REST servers which don't expect real pages to be sent }
type
{ forware declaration for TSimpleClientSocket }
TSimpleWebSrv = class;
TSimpleClientSocket = class(TSslWSocketClient)
private
{ Private declarations }
public
{ Public declarations }
WebSrv: TSimpleWebSrv;
RecvBuffer: TBytes;
RecvWaitTot: Integer; // current data in RecvBuffer
RecvBufMax: Integer; // buffer size
HttpReqHdr: String;
OnSimpWebSrvReq: TSimpleWebSrvReqEvent;
{ following are parsed from HTTP request header }
RequestMethod: THttpRequest; // HTTP request header field
RequestContentLength: Int64; // HTTP request header field
RequestHost: String; // HTTP request header field
RequestHostName: String; // HTTP request header field
RequestHostPort: String; // HTTP request header field
RequestPath: String; // HTTP request header field
RequestParams: String; // HTTP request header field
RequestReferer: String; // HTTP request header field
RequestUserAgent: String; // HTTP request header field
procedure CliSendPage(const Status, ContentType, ExtraHdr, BodyStr: String);
procedure CliErrorResponse(const RespStatus, Msg: string);
procedure CliDataAvailable(Sender: TObject; Error: Word);
procedure ParseReqHdr;
end;
TSimpleWebSrv = class(TIcsWndControl)
private
{ Private declarations }
FDebugLevel: THttpDebugLevel;
FWebSrvIP: string;
FWebSrvIP2: string; { V8.65 might need I{v6 as well }
FWebSrvPort: string;
FWebSrvPortSsl: string;
FWebSrvCertBundle: string; { following V8.62 for SSL }
FWebSrvCertPassword: string;
FWebSrvHostName: string;
FWebSrvRootFile: string;
FWebServer: TSslWSocketServer;
FOnServerProg: THttpRestProgEvent;
FOnSimpWebSrvReq: TSimpleWebSrvReqEvent;
FOnSimpWebSrvAlpn: TClientAlpnChallgEvent;
protected
{ Protected declarations }
procedure LogEvent(const Msg : String);
procedure SocketBgException(Sender: TObject;
E: Exception; var CanClose: Boolean);
procedure ServerClientConnect(Sender: TObject; Client: TWSocketClient; Error: Word); virtual;
procedure ServerClientDisconnect(Sender: TObject;
Client: TWSocketClient; Error: Word);
procedure IcsLogEvent (Sender: TObject; LogOption: TLogOption; const Msg : String);
public
{ Public declarations }
{$IFNDEF NO_DEBUG_LOG}
SrvLogger: TIcsLogger;
{$ENDIF}
property WebServer: TSslWSocketServer read FWebServer
write FWebServer; { V8.64 }
constructor Create (Aowner: TComponent); override;
destructor Destroy; override;
function StartSrv: boolean ;
function StopSrv(CloseClients: Boolean = True): boolean ; { V8.65 }
function IsRunning: Boolean;
function ListenStates: String;
published
{ Published declarations }
property DebugLevel: THttpDebugLevel read FDebugLevel
write FDebugLevel;
property WebSrvIP: string read FWebSrvIP
write FWebSrvIP;
property WebSrvIP2: string read FWebSrvIP2
write FWebSrvIP2; { V8.65 }
property WebSrvPort: string read FWebSrvPort
write FWebSrvPort;
property WebSrvPortSsl: string read FWebSrvPortSsl
write FWebSrvPortSsl;
property WebSrvCertBundle: string read FWebSrvCertBundle
write FWebSrvCertBundle; { V8.62 }
property WebSrvCertPassword: string read FWebSrvCertPassword
write FWebSrvCertPassword;
property WebSrvHostName: string read FWebSrvHostName
write FWebSrvHostName;
property WebSrvRootFile: string read FWebSrvRootFile
write FWebSrvRootFile;
property OnSimpWebSrvReq: TSimpleWebSrvReqEvent read FOnSimpWebSrvReq
write FOnSimpWebSrvReq;
property OnServerProg: THttpRestProgEvent read FOnServerProg
write FOnServerProg;
property OnSimpWebSrvAlpn: TClientAlpnChallgEvent read FOnSimpWebSrvAlpn
write FOnSimpWebSrvAlpn; { V8.62 }
end;
{ TRestOAuth is for handling 0Auth authorization to web apps. Beware OAuth
does not normally allow applications to specify the actual login to the
app, this is done via a browser web page }
type
TOAuthUri = record { V8.65 }
CAccName: string;
CConsoleUrl: string;
CAppUrl: string;
CRedirectUrl: string;
CTokenUrl: string;
CReqTokUrl: string; // OAuth1 only
CScope: string;
// CWebSrvIP: string;
// CWebSrvPort: string;
end;
TOAuthUris = array of TOAuthUri;
{ V8.65 these TOAuthUri records are designed to set-up common OAuth2 account
settings, by using the LoadAuthUri method. Note to avoid draggings all
URIs into all applications, they need to be referenced specifically in
applications. The REST sample builds an array to allow them to be selected
from a list. Note scope=offline may be needed to get a refresh token }
const
OAuthUriNone: TOAuthUri = (
CAccName: 'None' );
OAuthUriCertCenter: TOAuthUri = (
CAccName: 'CertCenter Account';
CConsoleUrl: 'https://my.certcenter.com/my/dashboard';
CAppUrl: 'https://www.certcenter.com/oauth2/auth';
CRedirectUrl: 'http://localhost:8080/certcenter/';
CTokenUrl: 'https://api.certcenter.com/oauth2/token';
CScope: 'write' );
OAuthUriGoogle: TOAuthUri = (
CAccName: 'Google Account';
CConsoleUrl: 'https://console.developers.google.com/';
CAppUrl: 'https://accounts.google.com/o/oauth2/auth';
CRedirectUrl: 'http://localhost:8080/gmail/';
CTokenUrl: 'https://accounts.google.com/o/oauth2/token';
CScope: 'https://mail.google.com/' );
// Microsoft identity platform OAuth2
// in the URLs, ' + IcsCRLF +
'' + IcsCRLF;
CliSendPage(RespStatus, 'text/html', '', BodyStr);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ note based on version from OverbyteIcsProxy but cut down to bare minimum }
procedure TSimpleClientSocket.ParseReqHdr;
var
Line, Arg: String;
I, J, K, L, Lines: Integer;
begin
RequestMethod := httpABORT;
RequestContentLength := 0;
RequestHost := '';
RequestHostName := '';
RequestHostPort := '';
RequestPath := '/';
RequestParams := '';
RequestReferer := '';
RequestUserAgent := '';
{ process one line in header at a time }
if Length(HttpReqHdr) <= 4 then Exit; // sanity check
I := 1; // start of line
Lines := 1;
for J := 1 to Length(HttpReqHdr) - 2 do begin
if (HttpReqHdr[J] = IcsCR) and (HttpReqHdr[J + 1] = IcsLF) then begin // end of line
if (J - I) <= 2 then continue; // ignore blank line, usually at start
Line := Copy(HttpReqHdr, I, J - I);
K := Pos (':', Line) + 1;
if Lines = 1 then begin
if (Pos('GET ', Line) = 1) then RequestMethod := httpGet;
if (Pos('POST ', Line) = 1) then RequestMethod := httpPost;
if (Pos('HEAD ', Line) = 1) then RequestMethod := httpHead;
if (Pos('PUT ', Line) = 1) then RequestMethod := httpPut;
L := Pos(' ', Line);
if (L > 0) then Line := Copy(Line, L + 1, 99999); // strip request
L := Pos(' HTTP/1', Line);
if (L > 0) then begin
RequestPath := Copy(Line, 1, L - 1);
L := Pos('?', RequestPath);
if (L > 0) then begin
RequestParams := Copy(RequestPath, L + 1, 99999);
RequestPath := Copy(RequestPath, 1, L - 1);
end;
L := Pos('://', RequestPath); // V8.62 look for full URL sent by proxy
if (L = 4) or (L = 5) then begin
RequestPath := Copy(RequestPath, L + 3, 99999); // strip http://
L := Pos('/', RequestPath); // start of path
if (L > 1) then
RequestPath := Copy(RequestPath, L, 999999); // strip host
end;
end;
end
else if (K > 3) then begin
Arg := IcsTrim(Copy(Line, K, 999)); // convert any arguments we scan to lower case later
if (Pos('Content-Length:', Line) = 1) then RequestContentLength := atoi64(Arg);
if (Pos('Host:', Line) = 1) then begin
RequestHost := IcsLowerCase(Arg); { need to separate host and port before punycoding }
L := Pos(':', RequestHost);
if L > 0 then begin
RequestHostName := IcsIDNAToUnicode(Copy(RequestHost, 1, L - 1)); { V8.64 }
RequestHostPort := Copy(RequestHost, L + 1, 99);
RequestHost := RequestHostName + ':' + RequestHostPort; { V8.64 }
end
else begin
RequestHostName := IcsIDNAToUnicode(RequestHost); { V8.64 }
RequestHostPort := WebSrv.FWebSrvPort;
RequestHost := RequestHostName; { V8.64 }
end;
end;
if (Pos('Referer:', Line) = 1) then RequestReferer := IcsLowercase(Arg);
if (Pos('User-Agent:', Line) = 1) then RequestUserAgent := Arg;
end;
Lines := Lines + 1;
I := J + 2; // start of next line
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TSimpleClientSocket.CliDataAvailable(Sender: TObject; Error: Word);
var
RxRead, RxCount, LoopCounter, HdrLen: Integer;
RespCode, Body: string;
begin
try
LoopCounter := 0;
if RecvWaitTot < 0 then RecvWaitTot := 0; // sanity check
while TRUE do begin
inc (LoopCounter);
if (LoopCounter > 100) then Break; // sanity check
RxCount := RecvBufMax - RecvWaitTot - 1;
if RxCount <= 0 then Break; // sanity check
RxRead := Self.Receive (@RecvBuffer[RecvWaitTot], RxCount);
if RxRead <= 0 then Break; // nothing read
RecvWaitTot := RecvWaitTot + RxRead;
end;
{ search for blank line in receive buffer which means we have complete request header }
HdrLen := IcsTBytesPos(IcsDoubleCRLF, RecvBuffer, 0, RecvWaitTot);
if (HdrLen <= 0) then begin
if (WebSrv.DebugLevel >= DebugBody) then
WebSrv.LogEvent('Waiting for more source data');
Exit;
end ;
HdrLen := HdrLen + 4; // add blank line length
{ keep headers in string so they are easier to process }
{ ignore any body, don't care about POST requests }
SetLength(HttpReqHdr, HdrLen);
IcsMoveTBytesToString(RecvBuffer, 0, HttpReqHdr, 1, HdrLen);
{ see what was sent }
ParseReqHdr;
{ ask user what we should do next }
if (RequestMethod = httpGET) and Assigned(OnSimpWebSrvReq) then begin
RespCode := '';
try { V8.65 always send response even if event crashes }
OnSimpWebSrvReq(Self, RequestHost, RequestPath, RequestParams, RespCode, Body);
except
on E:Exception do
WebSrv.LogEvent('Error Processing Response: ' + E.Message);
end;
if RespCode <> '' then
CliSendPage(RespCode, 'text/html', '', Body)
else
CliErrorResponse('500 Server Error', 'The requested URL ' +
TextToHtmlText(RequestPath) + ' was not processed by the server.');
end
else begin
if WebSrv.DebugLevel >= DebugHdr then
WebSrv.LogEvent({RFC3339_DateToStr(Now) + } 'Server Request Ignored, Host: ' +
RequestHost + ', Path: ' + RequestPath + ', Params: ' + RequestParams); { V8.62 }
CliErrorResponse('404 Not Found', 'The requested URL ' +
TextToHtmlText(RequestPath) + ' was not found on this server.');
end;
except
on E:Exception do
WebSrv.LogEvent('Error Receive Data: ' + E.Message);
end ;
end ;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ TRestOAuth }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
constructor TRestOAuth.Create (Aowner: TComponent);
begin
inherited Create(AOwner);
FWebServer := TSimpleWebSrv.Create(self);
FWebServer.OnServerProg := WebSrvProg; { V8.63 got lost somehow }
FWebServer.OnSimpWebSrvReq := WebSrvReq;
HttpRest := TSslHttpRest.Create(self);
HttpRest.OnHttpRestProg := RestProg;
FWebSrvIP := ICS_LOCAL_HOST_NAME; { V8.65 }
FWebSrvPort := '8080';
FDebugLevel := DebugConn;
FProtoType := OAuthv2;
FAuthType := OAuthTypeWeb;
FRefrMinsPrior := 120;
FRefreshDT := 0;
FScope := '';
FLoginPrompt := 'consent'; { V8.63 }
FResponseMode := 'query'; { V8.65 }
FMsUserAuthority := OAuthMsUserAuthDef; { V8.70 }
OAuthParams := TRestParams.Create(self); { V8.65 }
OAuthParams.PContent := PContUrlencoded;
OAuthParams.RfcStrict := True; { Twitter needs strict RFC }
Randomize; { V8.65 }
FLastWebTick := Trigger64Disabled; { V8.71 }
FRefreshTimer := TIcsTimer.Create(HttpRest);
FRefreshTimer.OnTimer := RefreshOnTimer;
FRefreshTimer.Interval := TicksPerMinute;
FRefreshTimer.Enabled := True;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TRestOAuth.Destroy;
begin
try
FRefreshTimer.Enabled := False;
StopSrv;
FreeAndNil(FRefreshTimer);
FreeAndNil(OAuthParams);
FreeAndNil(HttpRest);
FreeAndNil(FWebServer);
finally
inherited Destroy;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.RestProg(Sender: TObject; LogOption: TLogOption; const Msg: string);
begin
if Assigned(FOnOAuthProg) then
FOnOAuthProg(Self, LogOption, Msg) ;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.WebSrvProg(Sender: TObject; LogOption: TLogOption; const Msg: string);
begin
if Assigned(FOnOAuthProg) then
FOnOAuthProg(Self, LogOption, 'OAuth Web Server ' + Msg); { V8.63 }
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.LogEvent(const Msg : String);
begin
if FDebugLevel = DebugNone then Exit;
if Assigned(FOnOAuthProg) then
FOnOAuthProg(Self, loProtSpecInfo, Msg) ;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.SetError(ErrCode: Integer; const Msg: String);
begin
FLastErrCode := ErrCode;
FLastError := Msg;
LogEvent(Msg);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.SetRefreshDT;
begin
FRefreshDT := 0;
if FRefreshToken = '' then Exit;
if (FExpireDT < 10) then Exit;
if (FRefrMinsPrior < 10) then FRefrMinsPrior := 10;
FRefreshDT := FExpireDT - ((FRefrMinsPrior * 60) / SecsPerDay);
if FRefreshAuto then
LogEvent('Token will Automatically Refresh at: ' + DateTimeToStr(FRefreshDT));
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.SetExpireDT(Value: TDateTime);
begin
if Value <> FExpireDT then begin
FExpireDT := Value;
SetRefreshDT;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.SetRefreshAuto(Value: Boolean);
begin
if Value <> FRefreshAuto then begin
FRefreshAuto:= Value;
SetRefreshDT;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.SetRefreshToken(Value: String);
begin
if Value <> FRefreshToken then begin
FRefreshToken := Value;
SetRefreshDT;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.LoadAuthUri(AuthUri: TOAuthUri); { V8.65 }
var
I: Integer;
begin
FAccName := AuthUri.CAccName;
FConsoleUrl := AuthUri.CConsoleUrl;
FAppUrl := AuthUri.CAppUrl;
FReqTokUrl := AuthUri.CReqTokUrl; { OAuth1A only }
FRedirectUrl := AuthUri.CRedirectUrl;
FTokenUrl := AuthUri.CTokenUrl;
FScope := AuthUri.CScope;
I := Pos(ICS_LOCAL_HOST_NAME, FRedirectUrl);
if I = 8 then begin
FWebSrvIP := ICS_LOCAL_HOST_NAME;
I := IcsPosEx(':', FRedirectUrl, 9);
if I > 0 then begin
FWebSrvPort := Copy(FRedirectUrl, I + 1, 6);
I := Pos('/', FWebSrvPort);
if I > 2 then
FWebSrvPort := Copy(FWebSrvPort, 1, I - 1);
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TRestOAuth.StartSrv: boolean ;
begin
FWebServer.DebugLevel := Self.FDebugLevel;
FWebServer.WebSrvIP := Self.FWebSrvIP;
FWebServer.WebSrvPort := Self.FWebSrvPort;
Result := FWebServer.StartSrv;
FLastWebTick := Trigger64Disabled; { V8.60 don't timeout until request } { V8.71 }
if Result then
LogEvent('OAuth Web Server Started on: ' + IcsFmtIpv6AddrPort(FWebSrvIP, FWebSrvPort))
else
LogEvent('OAuth Web Server Failed to Start');
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TRestOAuth.StopSrv(CloseClients: Boolean = True): boolean ; { V8.65 }
begin
FLastWebTick := Trigger64Disabled; { V8.71 }
Result := FWebServer.StopSrv(CloseClients);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TRestOAuth.SrvIsRunning: Boolean;
begin
Result := FWebServer.IsRunning;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.Close; { V8.65 }
begin
if SrvIsRunning then StopSrv;
if HttpRest.State = httpConnected then HttpRest.Close;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TRestOAuth.RefreshOnTimer(Sender : TObject);
begin
FRefreshTimer.Enabled := False;
try
// auto refresh token
if FRefreshAuto and (FRefreshToken <> '') and (FRefreshDT <> 0) then begin
if Now > FRefreshDT then begin
FRefreshDT := 0;
LogEvent('Starting Automatic Token Refresh');
if NOT GrantRefresh then begin
LogEvent('Automatic Token Refresh Failed: ' + FLastError);
end;
end;
end;
// close web server on idle timeout - 30 minutes
if SrvIsRunning and (IcsElapsedMins64(FLastWebTick) > 30) then begin { V8.71 }
FLastWebTick := Trigger64Disabled;
LogEvent('OAuth Web Server Stopping on Idle Timeout');
StopSrv;
end;
finally
FRefreshTimer.Enabled := True;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ event called by simple web server when any page is requested }
{ V8.71, also called from FormRedirEvent for EdgeBrowser }
procedure TRestOAuth.WebSrvReq(Sender: TObject; const Host, Path, Params: string; var RespCode, Body: string);
var
State, Code, Title, Msg, Error, Redirect, ErrorDesc: String;
oauth_token, oauth_verifier: String;
procedure BuildBody;
begin
Body := ' ' + IcsCRLF +
' ' + IcsCRLF + ErrorDesc;
BuildBody;
Exit;
end;
if (NOT (OAopAuthNoState in FOAOptions)) and
(State = '') or (State <> FRedirState) then begin
StopSrv(False); { V8.65 stop server listening on error, don't close client }
RespCode := '501 Internal Error';
Title := RespCode;
Msg := 'Error: Unexpected State';
BuildBody;
Exit;
end;
if (Code = '') then begin
StopSrv(False); { V8.65 stop server listening on error, don't close client }
RespCode := '501 Internal Error';
Title := RespCode;
Msg := 'Error: Can not find Authorization Code';
BuildBody;
Exit;
end;
FAuthCode := Code;
end;
// if not testing, save new code. try and get token
Title := 'Authorization Code Generated Successfully';
LogEvent('OAuth Web Request, ' + Title + ', ' + Code); { V8.65 }
Msg := ' App Authorization Code: ' + Code + '' + RespStatus + '
' + Msg + '' + Title + '
' + Msg + 'Please Close this Window' + IcsCRLF + { V8.71 }
'' + IcsCRLF;
LogEvent('OAuth Web Response: ' + RespCode);
end;
begin
// ignore favicon requests completely
if Path = '/favicon.ico' then begin
RespCode := '404 Not Found';
Title := RespCode;
Msg := 'Error: File Not Found';
BuildBody;
Exit;
end;
// if called from web server, report URL
if Host <> '' then begin { V8.71 }
FLastWebTick := IcsGetTickCount64; // timeout to close server
LogEvent('OAuth Web Request, Host: ' + Host + ', Path: ' + Path + ', Params: ' + Params);
Redirect := 'http://' + Host + Path;
if Redirect <> FRedirectUrl then
LogEvent('Warning, Differing Redirect URL: ' + Redirect);
end;
// for an OAuth authentication redirect, we don't really care about the path
IcsExtractURLEncodedValue (Params, 'state', State) ; // OAuth2
IcsExtractURLEncodedValue (Params, 'code', Code) ; // OAuth2
IcsExtractURLEncodedValue (Params, 'error', Error) ; // OAuth2
IcsExtractURLEncodedValue (Params, 'oauth_token', oauth_token) ; // OAuth1a
IcsExtractURLEncodedValue (Params, 'oauth_verifier', oauth_verifier) ; // OAuth1a
IcsExtractURLEncodedValue (Params, 'error_description', ErrorDesc); // OAuth2 V8.65
// V8.65 OAuth1A check expected request token, keep code
if (FProtoType = OAuthv1A) then begin
if (oauth_token <> FReqToken) and (oauth_verifier = '') then begin
RespCode := '501 Internal Error';
Title := 'OAuth Authorization Failed';
Msg := 'Error: No OAuth1 Tokens Found';
BuildBody;
Exit;
end;
FAuthCode := oauth_verifier;
end
// OAuth2
else begin
if (Error <> '') then begin
StopSrv(False); { V8.65 stop server listening on error, don't close client }
RespCode := '501 Internal Error';
Title := 'OAuth Authorization Failed';
Msg := 'Error: ' + Error + '
App Token Generated Successfully
' + IcsCRLF + '' + FRedirectMsg + ''; end else Title := 'Failed to Generate App Token'; end; BuildBody; { web page is sent by event handler } end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} function TRestOAuth.TestRedirect: boolean; var StatCode: Integer; begin Result := false; FLastErrCode := OAuthErrNoError; FLastError := ''; if NOT SrvIsRunning then StartSrv; if NOT SrvIsRunning then begin SetError(OAuthErrWebSrv, 'Can Not Test Redirect, Web Server Will Not Start'); Exit; end; if Pos ('http://', FRedirectUrl) <> 1 then begin SetError(OAuthErrParams, 'Can Not Test Redirect, Invalid Redirect URL: ' + FRedirectUrl); { V8.70 } Exit; end; LogEvent('Redirect URL: ' + FRedirectUrl); { V8.70 tell user } FRedirState := TestState; HttpRest.Reference := FRedirectUrl; HttpRest.DebugLevel := FDebugLevel; HttpRest.RestParams.Clear; HttpRest.RestParams.AddItem('state', FRedirState); HttpRest.RestParams.AddItem('code', '12345678901234567890'); StatCode := HttpRest.RestRequest(HttpGET, FRedirectUrl, False, ''); if StatCode <> 200 then SetError(OAuthErrWebSrv, 'Test Redirect Failed to: ' + FRedirectUrl) { V8.70 } else begin LogEvent('Test Redirect OK'); Result := true; end; StopSrv(False); { V8.65 close server but not current client } end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} function TRestOAuth.GetNonce: String; { V8.65 } var I: Integer; begin Result := ''; for I := 1 to 16 do Result := Result + Chr(Random(26) + Ord('A')) + Chr(Random(26) + Ord('a')); end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { OAuth1A get SHA1 digest of request, URL and parameters, Base String } { OAuthParams may already have items } { everything strictly percent URL encooded according to RTF, in alpha order } function TRestOAuth.GetOAuthSignature(const Req, Url: String; ReqTok: Boolean = False): String; { V8.65 } var SignBase, SignKey, Params: AnsiString; begin SignKey := UrlEncodeToA(FClientSecret, CP_UTF8, True) + IcsAmpersand; OAuthParams.AddItem('oauth_consumer_key', FClientId); OAuthParams.AddItem('oauth_nonce', GetNonce); OAuthParams.AddItem('oauth_signature_method', 'HMAC-SHA1'); OAuthParams.AddItem('oauth_timestamp', IntToStr(IcsGetUnixTime)); if ReqTok then begin OAuthParams.AddItem('oauth_token', FReqToken); if FReqTokSecret <> '' then SignKey := SignKey + UrlEncodeToA(FReqTokSecret, CP_UTF8, True); end else begin OAuthParams.AddItem('oauth_token', FAccToken); if FAccTokSecret <> '' then SignKey := SignKey + UrlEncodeToA(FAccTokSecret, CP_UTF8, True); end; OAuthParams.AddItem('oauth_version', '1.0'); OAuthParams.PContent := PContUrlencoded; Params := OAuthParams.GetParameters(True); // sorted name order or hash fails // parameters are urlencoded a second time so only two & in signbase SignBase := AnsiString(IcsUpperCase(Req)) + IcsAmpersand + UrlEncodeToA(Url, CP_UTF8, True) + IcsAmpersand + UrlEncodeToA(String(Params), CP_UTF8, True); Result := String(Base64Encode(IcsHMACDigestEx(SignBase, SignKey, Digest_sha1))); LogEvent('OAuth1 Params: ' + String(Params) + IcsCRLF + 'SignBase: ' + String(SignBase) + IcsCRLF + 'SignKey; ' + String(SignKey) + IcsCRLF + 'Signature: ' + Result); end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { Launch browser with console or account URL } function TRestOAuth.LaunchConsole: boolean; begin Result := False; if FConsoleUrl = '' then begin SetError(OAuthErrParams, 'Can Not Launch Browser, Invalid Redirect URL'); Exit; end; if IcsShellExec(FConsoleUrl) then begin LogEvent('Launched Browser to console: ' + FConsoleUrl); Result := True; end else begin SetError(OAuthErrBrowser, 'Failed to Launch Browser: ' + GetWindowsErr(GetLastError)); end; end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { Start user authorisation generally using an embedded or external browser to enter a login, which then redirects back to a local web server which gets the authorisation code } function TRestOAuth.StartAuthorization: boolean; var BrowserURL, Signature, CallBackOK, S: string; StatCode: Integer; ErrJson: ISuperObject; begin Result := false; FLastErrCode := OAuthErrNoError; FLastError := ''; if (FProtoType = OAuthv1) then begin SetError(OAuthErrParams, 'OAuth1 Not Supported'); Exit; end; if Pos ('http://', FRedirectUrl) <> 1 then begin SetError(OAuthErrParams, 'Can Not Start Authorization, Invalid Redirect URL: ' + FRedirectUrl); Exit; end; if Pos ('https://', FAppUrl) <> 1 then begin SetError(OAuthErrParams, 'Can Not Start Authorization, Invalid App URL: ' + FAppUrl); Exit; end; if (FClientId = '') or (FClientSecret = '') then begin SetError(OAuthErrParams, 'Can Not Start Authorization, Need Client ID and Secret'); Exit; end; HttpRest.DebugLevel := FDebugLevel; { V8.65 } FAccToken := ''; { V8.65 } FExpireDT := 0; { V8.65 } // OAuth1A get request token and build URL for browser if (FProtoType = OAuthv1A) then begin FReqToken := ''; FReqTokSecret := ''; // build base string, used for hashed signature // must include POST content and URL paramaters, sorted OAuthParams.Clear; OAuthParams.AddItem('oauth_callback', FRedirectUrl); LogEvent('Redirect URL: ' + FRedirectUrl); { V8.70 tell user } Signature := GetOAuthSignature('POST', FReqTokUrl, True); // adds commom parameters // adjust base string for Authorization: OAuth header, add signature, leave callback OAuthParams.AddItem('oauth_signature', Signature); HttpRest.ServerAuth := httpAuthOAuth; OAuthParams.PContent := PContCommaList; // change paramter format HttpRest.AuthBearerToken := String(OAuthParams.GetParameters(True)); // bearer is quoted encoded values, comma separated LogEvent('Authorization: OAuth ' + HttpRest.AuthBearerToken); HttpRest.RestParams.Clear; StatCode := HttpRest.RestRequest(httpPOST, FReqTokUrl, False, ''); { V8.71 !!!! fails, POST must havbe params!!! } if (StatCode <> 200) then begin S := HttpRest.ResponseRaw; if S = '' then S := HttpRest.LastResponse else begin if Assigned(HttpRest.ResponseJson) then begin ErrJson := HttpRest.ResponseJson.O['errors']; if Assigned(ErrJson) and (ErrJson.AsArray.Length > 0) then S := ErrJson.AsArray.O[0].S['message']; end; end; SetError(OAuthErrParams, 'Can Not Get OAuth1 Request Token: ' + S); Exit; end else begin // note getting request tokens not access tokens despite the labels! // this request does not return Json for some strange reason IcsExtractURLEncodedValue (HttpRest.ResponseRaw, 'oauth_token', FReqToken) ; IcsExtractURLEncodedValue (HttpRest.ResponseRaw, 'oauth_token_secret', FReqTokSecret) ; IcsExtractURLEncodedValue (HttpRest.ResponseRaw, 'oauth_callback_confirmed', CallBackOK) ; end; if (FReqToken = '') or (FReqTokSecret = '') then begin SetError(OAuthErrParams, 'Can Not Find OAuth1 Request Token in Response: ' + HttpRest.ResponseRaw); exit; end; OAuthParams.Clear; OAuthParams.PContent := PContUrlencoded; OAuthParams.AddItem('oauth_token', FReqToken); if (OAopAuthPrompt in FOAOptions) then OAuthParams.AddItem('force_login', 'true'); // OAuthParams.AddItem('screen_name', '??' ); // prefill login name, if we had it BrowserURL := FAppUrl + '?' + String(OAuthParams.GetParameters); end // OAuth2 build URL for browser else begin FRedirState := 'ICS-' + IntToStr(IcsGetTickCount64); { V8.71 } OAuthParams.Clear; OAuthParams.PContent := PContUrlencoded; OAuthParams.AddItem('response_type', 'code'); OAuthParams.AddItem('client_id', FClientId); if NOT (OAopAuthNoRedir in FOAOptions) then begin OAuthParams.AddItem('redirect_uri', FRedirectUrl); LogEvent('Redirect URL: ' + FRedirectUrl); { V8.70 tell user } end; if NOT (OAopAuthNoState in FOAOptions) then OAuthParams.AddItem('state', FRedirState); if (NOT (OAopAuthNoScope in FOAOptions)) and (FScope <> '') then OAuthParams.AddItem('scope', FScope); if (OAopAuthPrompt in FOAOptions) and (FLoginPrompt <> '') then OAuthParams.AddItem('prompt', FLoginPrompt); { V8.63 none consent select_account } if (OAopAuthAccess in FOAOptions) then begin if FRefreshOffline then OAuthParams.AddItem('access_type', 'offline') { V8.63 neeed so Google supplies refresh token } else OAuthParams.AddItem('access_type', 'online'); end; // if FResource <> '' then // HttpRest.RestParams.AddItem('resource', FResource); { V8.65 } if (OAopAuthRespMode in FOAOptions) and (FResponseMode <> '') then OAuthParams.AddItem('response_mode', FResponseMode); { V8.65 } if (OAopAuthLoginHint in FOAOptions) and (FLoginHint <> '') then OAuthParams.AddItem('login_hint', FLoginHint); { V8.65 } if (OAopAuthGrantedScope in FOAOptions) then OAuthParams.AddItem('include_granted_scopes', true); { V8.65 incremental scopes, keep old ones } { V8.70 Microsoft has several URL variants } if FMsUserAuthority = '' then FMsUserAuthority := OAuthMsUserAuthDef; BrowserURL := StringReplace(FAppUrl, '