open-insight/FRAMEWORKS/STPROC/HTTP_AUTHENTICATION_SERVICES.txt
2024-03-25 15:15:48 -07:00

554 lines
30 KiB
Plaintext

Function HTTP_Authentication_Services(@Service, @Params)
/***********************************************************************************************************************
This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
permission from SRP Computer Solutions, Inc.
Name : HTTP_Authentication_Services
Description : Handler program for all HTTP authentication.
Notes : Authentication techniques will vary depending upon the application so the code in the
AuthenticateRequest service will need to be customized as necessary.
Parameters :
Service [in] -- Name of the service being requested
Param1-10 [in/out] -- Additional request parameter holders
Response [out] -- Response to be sent back to the Controller (MCP) or requesting procedure
History : (Date, Initials, Notes)
02/25/15 dmb [SRPFW-91] Original programmer.
06/10/15 dmb [SRPFW-91] Add checks to make sure Username and Password are populated before attempting to
authenticate against the USERS table. This prevents easy authentication if the USERS table
isn't being managed well.
02/25/16 dmb [SRPFW-108] Add support for the GetEnableAuthenticateFlag service. If disabled, then automatically
authenticate the user.
02/25/16 dmb [SRPFW-108] Add support for the GetRealmValue service. Use this instead of hard-coding the
realm.
12/01/16 dmb Update the AuthenticateRequest service to verify authentication requirements of the current
URL using the URLRequiresAuthentication service.
07/01/17 dmb [SRPFW-184] Refactor using Enhanced BASIC+ syntax.
10/22/18 dmb [SRPFW-253] Add support for checking for whitelisted IPs in the AuthenticateRequest service.
10/31/18 dmb [SRPFW-254] Add GetWebAccountPassword, SetWebAccountPassword, and ValidateWebAccountPassword
services.
10/31/18 dmb [SRPFW-254] Update the AuthenticateRequest service to use the ValidateWebAccountPassword
service rather than relying upon a hardcoded USERS table.
11/01/18 dmb [SRPFW-256] Update NewPasswordTimeToLive$ equate to use the GetNewPasswordTimeToLive service
rather than the hardcoded value.
11/01/18 dmb [SRPFW-256] Update OldPasswordTimeToLive$ equate to use the GetOldPasswordTimeToLive service
rather than the hardcoded value.
11/09/18 dmb [SRPFW-256] Update ValidateWebAccountPassword service to implement the containment action if
too many failed password attempts have been attempted.
11/20/18 dmb [SRPFW-256] Add GetWebAccountEnabledStatus service. Update the AuthenticateRequest service
to use it before attempting to validate the password.
11/21/18 dmb [SRPFW-257] Add ResetWebAccountPassword service.
11/21/18 dmb [SRPFW-257] Update SetWebAccountPassword service to support a flag that ignores expiration
date.
11/23/18 dmb [SRPFW-257] Add SetAuthenticatedAccountID and GetAuthenticatedAccountID services.
12/12/18 dmb [SRPFW-257] Add SetAuthenticatedPassword and GetAuthenticatedPassword services.
06/24/19 dmb [SRPFW-276] Update the ValidateWebAccountPassword service to reset the invalid password
attempt counter for an account if a valid password is passed in.
12/09/19 dmb [SRPFW-296] Update all calls to Memory_Services to use a specific cache name.
06/30/20 dmb [SRPFW-313] Update the AuthenticateRequest service to return a 403 status code rather than
a 511 status code if the IP making the request is not permitted.
07/27/20 dmb [SRPFW-313] Replace references to the IPIsPermitted service with the IsIPPermitted service.
***********************************************************************************************************************/
#pragma precomp SRP_PreCompiler
$insert APP_INSERTS
$insert SERVICE_SETUP
$insert HTTP_INSERTS
Equ SecondsPerHour$ to 60 * 60 ; // 60 minutes * 60 seconds = 3600
Equ SecondsPerDay$ to 24 * SecondsPerHour$ ; // 24 hours * 60 minutes * 60 seconds = 86400
Equ NewPasswordTimeToLive$ to HTTP_Services('GetNewPasswordTimeToLive') * SecondsPerHour$ ; // Convert hours to seconds
Equ OldPasswordTimeToLive$ to HTTP_Services('GetOldPasswordTimeToLive') * SecondsPerHour$ ; // Convert hours to seconds
Equ CacheName$ to 'SRPHTTPFramework'
Declare function Database_Services, RTI_CreateGUID
Declare subroutine Database_Services
GoToService else
Error_Services('Add', Service : ' is not a valid service request within the HTTP Authentication services module.')
end
Return Response OR ''
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Service Parameter Options
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Options BOOLEAN = True$, False$
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------------------------------------
// AuthenticateRequest
//
// Returns a boolean value indicating the success of the authentication attempt. Default method is built around
// HTTP Basic Authentication.
//----------------------------------------------------------------------------------------------------------------------
Service AuthenticateRequest()
// All response headers that need to be set, regardless of authentication, should be handled here.
// 1. Access-Control-Allow-Origin must always be returned for CORS purposes.
HTTP_Services('SetResponseHeaderField', 'Access-Control-Allow-Origin', '*')
EnableAuthentication = HTTP_Services('GetEnableAuthenticationFlag')
FullEndPointURL = HTTP_Services('GetFullEndPointURL')
URLRequiresAuthentication = HTTP_Services('URLRequiresAuthentication', FullEndPointURL)
// Set the default status code and phrase if authentication fails.
StatusCode = 401
StatusPhrase = ''
If EnableAuthentication AND URLRequiresAuthentication then
HTTPMethod = HTTP_Services('GetHTTPRequestMethod')
HTTPRemoteAddr = HTTP_Services('GetHTTPRemoteAddr')
// Verify that the client IP is permitted. If there are no whitelisted IPs, then all IPs are permitted.
IsIPPermitted = HTTP_Services('IsIPPermitted', HTTPRemoteAddr)
If IsIPPermitted EQ True$ then
If HTTPMethod _EQC 'OPTIONS' then
// OPTIONS methods are never authenticated. Allow the user to be provisionally authenticated since the method
// will remains as OPTIONS throughout the entire API.
UserAuthenticated = True$
end else
// Assume the user is not authenticated until otherwise proven.
UserAuthenticated = False$
// The follow code provides a skeleton for support HTTP Basic authorization. This is a REST friendly
// authentication protocol and is documented in the core HTTP specification. Because REST does not preserve the
// state, all requests are authenticated regardless of previous authentication successes. HTTP Basic should
// only be used if https:// is being used. Otherwise, the credentials are being passed through as plain text.
// HTTP Basic uses the Authorization request header. However, the Authorization request header field does not
// always work with web server products when being passed to a third-party service. So, if the standard header
// returns nothing then check the custom X-Authorization request header.
AuthorizationB64 = HTTP_Services('GetRequestHeaderField', 'Authorization')
If AuthorizationB64 EQ '' then AuthorizationB64 = HTTP_Services('GetRequestHeaderField', 'X-Authorization')
If AuthorizationB64 NE '' then
// All HTTP Basic credentials should be Base64 encoded (in addition to encrypted via https://). Decode
// the credentials.
Authorization = SRP_Decode(AuthorizationB64[7, 999], 'BASE64')
// HTTP Basic credentials are always colon (:) delimited. Typically this will come through as
// Username:Password, but there could be other formats if the application requires it. For instance, for
// applications supporting multiple customers wherein each customer has their own group of users, the
// format could look like this CustomerID/Username:Password. This provides, in a sense, a three-part
// identifier. The following parsing logic would need to be adjusted as needed.
Username = Authorization[1, ':']
Password = Authorization[Col2() + 1, 999]
EnabledStatus = HTTP_Authentication_Services('GetWebAccountEnabledStatus', Username)
If EnabledStatus EQ True$ then
// Only authenticate if a username and password is provided. This prevents authenticating in the event
// the USERS row is missing a password or the USERS table has a blank row.
If (Username NE '') AND (Password NE '') then
// Below is where you would place your logic to validate the username, password, and any other credentials
// that were passed in. This code uses the default HTTP Framework WEB_ACCOUNTS table.
UserAuthenticated = HTTP_Authentication_Services('ValidateWebAccountPassword', Username, Password, False$)
// A successful login should set the WWW-Authenticate response header field with the appropriate value. The
// credentials are stored in memory so they can be retrieved by other services as needed.
If UserAuthenticated then
UserAuthenticated = True$
HTTP_Authentication_Services('SetAuthenticatedAccountID', Username)
HTTP_Authentication_Services('SetAuthenticatedPassword', Password)
// The realm attribute is a part of the HTTP authentication specification and is used to help identify all
// resources that belong to the same authentication. Typically this will be the same value for all requests
// within the same application. The branded name or OpenInsight name of the application would be a good
// example to use here.
Realm = HTTP_Services('GetRealmValue')
HTTP_Services('SetResponseHeaderField', 'WWW-Authenticate', 'xBasic realm="' : Realm : '"')
end
end
end else
// IP address making the request is not permitted. Do not authenticate the user.
StatusCode = 403
StatusPhrase = 'Account ' : Username : ' is disabled.'
UserAuthenticated = False$
end
end
end
end else
// IP address making the request is not permitted. Do not authenticate the user.
StatusCode = 403
StatusPhrase = HTTPRemoteAddr : ' is not a permitted IP address.'
UserAuthenticated = False$
end
end else
// Force the user to be authenticated since authentication is not enabled.
UserAuthenticated = True$
end
// Non-authenticated requests should have a 401 status code returned.
If Not(UserAuthenticated) then
HTTP_Services('SetResponseError', '', '', StatusCode, StatusPhrase, FullEndpointURL)
end
Response = UserAuthenticated
end service
//----------------------------------------------------------------------------------------------------------------------
// CleanUp
//
// Runs any clean up processes as needed to prepare the engine for the next request.
//----------------------------------------------------------------------------------------------------------------------
Service CleanUp()
// This service is called from HTTP_MCP before sending the response back to the caller. Any application specific
// logic that stores data in memory or attaches customer specific database tables should be properly closed out
// to avoid subsequent requests from having innappropriate access.
end service
//----------------------------------------------------------------------------------------------------------------------
// GetWebAccountEnabledStatus
//
// Gets the enabled status for the indicated web account.
//----------------------------------------------------------------------------------------------------------------------
Service GetWebAccountEnabledStatus(AccountID)
EnabledStatus = ''
If AccountID NE '' then
WebAccountRow = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
If Error_Services('NoError') then
@DICT = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
@ID = AccountID
@RECORD = WebAccountRow
EnabledStatus = {ACCOUNT_ENABLED}
If EnabledStatus NE True$ then EnabledStatus = False$ ; // Always default to disabled unless explicitly enabled.
end
end else
Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
end
Response = EnabledStatus
end service
//----------------------------------------------------------------------------------------------------------------------
// GetWebAccountPassword
//
// Gets the current password for the indicated web account. If the CreateIfNew flag is set to True$, a new password will
// be generated if no password currently exists. This new password will be added to the web account.
//----------------------------------------------------------------------------------------------------------------------
Service GetWebAccountPassword(AccountID, CreateIfNew)
Password = ''
If CreateIfNew NE True$ then CreateIfNew = False$
If AccountID NE '' then
WebAccountRow = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
If Error_Services('NoError') then
@DICT = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
@ID = AccountID
@RECORD = WebAccountRow
Begin Case
Case ({CURRENT_PASSWORD} EQ '') AND (CreateIfNew EQ True$)
Password = HTTP_Authentication_Services('ResetWebAccountPassword', AccountID, CurrentPassword)
Case ({CURRENT_PASSWORD} EQ '') AND (CreateIfNew EQ False$)
Error_Services('Add', 'No password exists for Account ID ' : AccountID)
Case Otherwise$
Password = {CURRENT_PASSWORD}
End Case
end
end else
Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
end
Response = Password
end service
//----------------------------------------------------------------------------------------------------------------------
// ResetWebAccountPassword
//
// Resets the current password (or creates a new one) for the indicated web account. This new password will be added to
// the web account.
//----------------------------------------------------------------------------------------------------------------------
Service ResetWebAccountPassword(AccountID, CurrentPassword)
Password = ''
If AccountID NE '' then
WebAccountRow = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
If Error_Services('NoError') then
@DICT = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
@ID = AccountID
@RECORD = WebAccountRow
// Password is based on a random GUID and then encoded as Base64.
Password = RTI_CreateGUID('B')
HTTP_Authentication_Services('SetWebAccountPassword', AccountID, CurrentPassword, Password, True$)
If Error_Services('HasError') then Password = ''
end
end else
Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
end
Response = Password
end service
//----------------------------------------------------------------------------------------------------------------------
// ValidateWebAccountPassword
//
// Validates the password for the indicated web account. If the CurrentOnly argument is set to True$, then only the
// current password associated with the web account will be validated. Otherwise, the old password will also be valided
// using the expiration date and time associated.
//----------------------------------------------------------------------------------------------------------------------
Service ValidateWebAccountPassword(AccountID, Password, CurrentOnly)
Valid = False$ ; // Assume False$ for now.
ErrorMessage = ''
If CurrentOnly NE True$ then CurrentOnly = False$
If (AccountID NE '') AND (Password NE '') then
WebAccountRow = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
If Error_Services('NoError') then
@DICT = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
@ID = AccountID
@RECORD = WebAccountRow
ThisSeconds = Date() * SecondsPerDay$ + Time()
Begin Case
Case Password EQ {CURRENT_PASSWORD}
ExpireSeconds = {CURRENT_PASSWORD_EXPIRE_DATE} * SecondsPerDay$ + {CURRENT_PASSWORD_EXPIRE_TIME}
If ThisSeconds LE ExpireSeconds then
Valid = True$
end else
ErrorMessage = 'Password is expired. A new one needs to be requested.'
end
Case (Password EQ {OLD_PASSWORD}) AND (CurrentOnly EQ False$)
ExpireSeconds = {OLD_PASSWORD_EXPIRE_DATE} * SecondsPerDay$ + {OLD_PASSWORD_EXPIRE_TIME}
If ThisSeconds LE ExpireSeconds then
Valid = True$
end else
ErrorMessage = 'Password is expired. A new one needs to be requested.'
end
Case Otherwise$
ErrorMessage = 'Password is invalid.'
End Case
If ErrorMessage EQ '' then
// Reset the number of invalid password attempts for the account.
{INVALID_PASSWORD_ATTEMPTS} = 0
Database_Services('WriteDataRow', 'WEB_ACCOUNTS', @ID, @RECORD, True$, False$, True$)
end else
// Update the total invalid password attempts for this server.
Attempts = HTTP_Services('GetTotalInvalidPasswordAttempts')
Attempts += 1
HTTP_Services('SetTotalInvalidPasswordAttempts', Attempts)
// Update the total invalid password attempts for this account.
InvalidPasswordAttempts = {INVALID_PASSWORD_ATTEMPTS} + 1
{INVALID_PASSWORD_ATTEMPTS} = InvalidPasswordAttempts
Database_Services('WriteDataRow', 'WEB_ACCOUNTS', @ID, @RECORD, True$, False$, True$)
InvalidPasswordLimit = HTTP_Services('GetInvalidPasswordLimit')
If InvalidPasswordAttempts GE InvalidPasswordLimit then
ContainmentAction = HTTP_Services('GetContainmentAction')
Begin Case
Case ContainmentAction _EQC 'Disable Server'
HTTP_Services('SetServerEnabled', False$)
Case ContainmentAction _EQC 'Quarantine Account'
{ACCOUNT_ENABLED} = False$
WebAccountRow = @RECORD
Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
End Case
ActionDetails = ''
ActionDetails<1> = Fmt('Containment Action:', 'L#35') : ContainmentAction
ActionDetails<2> = Fmt('Invalid Password Limit:', 'L#35') : InvalidPasswordLimit
ActionDetails<3> = Fmt('Total Invalid Password Attempts:', 'L#35') : Attempts
ActionDetails<4> = Fmt('Account ID:', 'L#35') : AccountID
ActionDetails<5> = Fmt('Total Account Invalid Attempts:', 'L#35') : InvalidPasswordAttempts
HTTP_Authentication_Services('ContainmentActionNotification', ActionDetails)
end
Error_Services('Add', ErrorMessage)
end
end
end else
Error_Services('Add', 'AccountID or Password argument was missing in the ' : Service : ' service.')
end
Response = Valid
end service
//----------------------------------------------------------------------------------------------------------------------
// SetWebAccountPassword
//
// Sets a new password for the indicated web account. If no current password already exists, then the new password will
// be added to the web account automatically. Otherwise, the current password will be verified before allowing a new
// password to be set.
//----------------------------------------------------------------------------------------------------------------------
Service SetWebAccountPassword(AccountID, CurrentPassword, NewPassword, OverrideExpireDate)
If OverrideExpireDate NE True$ then OverrideExpireDate = False$
If (AccountID NE '') AND (NewPassword NE '') then
WebAccountRow = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
If Error_Services('NoError') then
@DICT = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
@ID = AccountID
@RECORD = WebAccountRow
If {CURRENT_PASSWORD} EQ '' then
// This is a new password for this web account. Accept the new password.
CreateDate = Date()
CreateTime = Time()
CreateSeconds = CreateDate * SecondsPerDay$ + CreateTime
ExpireSeconds = CreateSeconds + NewPasswordTimeToLive$
ExpireDate = Int(ExpireSeconds / SecondsPerDay$)
ExpireTime = Mod(ExpireSeconds, SecondsPerDay$)
{CURRENT_PASSWORD} = NewPassword
{CURRENT_PASSWORD_CREATE_DATE} = CreateDate
{CURRENT_PASSWORD_CREATE_TIME} = CreateTime
{CURRENT_PASSWORD_EXPIRE_DATE} = ExpireDate
{CURRENT_PASSWORD_EXPIRE_TIME} = ExpireTime
WebAccountRow = @RECORD
Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
end else
// A current password already exists.
Valid = HTTP_Authentication_Services('ValidateWebAccountPassword', AccountID, CurrentPassword, True$) OR (OverrideExpireDate EQ True$)
If Valid EQ True$ then
Begin Case
Case CurrentPassword EQ NewPassword
// New password must be different than the current password.
Error_Services('Add', 'New password must be different than the current password.')
Case Otherwise$
// Current password is valid and new password is different.
// Make the current password the old passowrd. Reset the expiration date and time as
// needed.
CurrentPassword = {CURRENT_PASSWORD}
CurrentPasswordCreateDate = {CURRENT_PASSWORD_CREATE_DATE}
CurrentPasswordCreateTime = {CURRENT_PASSWORD_CREATE_TIME}
{OLD_PASSWORD} = CurrentPassword
{OLD_PASSWORD_CREATE_DATE} = CurrentPasswordCreateDate
{OLD_PASSWORD_CREATE_TIME} = CurrentPasswordCreateTime
ThisSeconds = Date() * SecondsPerDay$ + Time()
ExpireSeconds = ThisSeconds + OldPasswordTimeToLive$
ExpireDate = Int(ExpireSeconds / SecondsPerDay$)
ExpireTime = Mod(ExpireSeconds, SecondsPerDay$)
{OLD_PASSWORD_EXPIRE_DATE} = ExpireDate
{OLD_PASSWORD_EXPIRE_TIME} = ExpireTime
// Set the new password information.
CreateDate = Date()
CreateTime = Time()
CreateSeconds = CreateDate * SecondsPerDay$ + CreateTime
ExpireSeconds = CreateSeconds + NewPasswordTimeToLive$
ExpireDate = Int(ExpireSeconds / SecondsPerDay$)
ExpireTime = Mod(ExpireSeconds, SecondsPerDay$)
{CURRENT_PASSWORD} = NewPassword
{CURRENT_PASSWORD_CREATE_DATE} = CreateDate
{CURRENT_PASSWORD_CREATE_TIME} = CreateTime
{CURRENT_PASSWORD_EXPIRE_DATE} = ExpireDate
{CURRENT_PASSWORD_EXPIRE_TIME} = ExpireTime
WebAccountRow = @RECORD
Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
End Case
end
end
end
end else
Error_Services('Add', 'AccountID or NewPassword argument was missing in the ' : Service : ' service.')
end
end service
//----------------------------------------------------------------------------------------------------------------------
// SetAuthenticatedAccountID
//
// Sets the account ID that was successfully authenticated for this request.
//----------------------------------------------------------------------------------------------------------------------
Service SetAuthenticatedAccountID(AccountID)
If AccountID NE '' then
Memory_Services('SetValue', ServiceModule : '*AuthenticatedAccountID', AccountID, CacheName$)
end
end service
//----------------------------------------------------------------------------------------------------------------------
// GetAuthenticatedAccountID
//
// Gets the successfully authenticated account ID for this request.
//----------------------------------------------------------------------------------------------------------------------
Service GetAuthenticatedAccountID()
AccountID = Memory_Services('GetValue', ServiceModule : '*AuthenticatedAccountID', '', '', CacheName$)
Response = AccountID
end service
//----------------------------------------------------------------------------------------------------------------------
// SetAuthenticatedPassword
//
// Sets the password that was successfully authenticated for this request.
//----------------------------------------------------------------------------------------------------------------------
Service SetAuthenticatedPassword(Password)
If Password NE '' then
Memory_Services('SetValue', ServiceModule : '*AuthenticatedPassword', Password, CacheName$)
end
end service
//----------------------------------------------------------------------------------------------------------------------
// GetAuthenticatedPassword
//
// Gets the successfully authenticated password for this request.
//----------------------------------------------------------------------------------------------------------------------
Service GetAuthenticatedPassword()
Password = Memory_Services('GetValue', ServiceModule : '*AuthenticatedPassword', '', '', CacheName$)
Response = Password
end service
//----------------------------------------------------------------------------------------------------------------------
// ContainmentActionNotification
//
// Handles notification protocols when a containment breach has occured. This handler is mostly a placeholder for
// developers to add their own custom protocol action.
//----------------------------------------------------------------------------------------------------------------------
Service ContainmentActionNotification(ActionDetails)
end service
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////