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 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////