Manual Project Configuration

The manual process involves quite a few steps to copy files and hook in the process class and UI components. Most of these steps are simple, but there are quite a few of them so it's important to follow instructions carefully.

For demonstration purposes I'll use SecurityTest and SecurityProcess as the project and process class names of the project to integrate into

Copy Files and Folders

First we need to copy files both to the Web and Deploy (code) folders. The SecurityManager is fairly isolated so copying files is pretty straight forward.

Web

Copy files from the UserSecurityManager project folder to your Web Connection Project folder:

  • Copy the web\UserManager folder int a new web\UserManager folder in your app
  • Copy the web\views\_Login.wcs file into your web\views folder

Note the _login.wcs file is very important as it ensures that you see the additional login features in the login form and that login operations are also routed to the proper login operation in the User Security Manager process.

Add Web Sign In Menu in views\_layoutpage.wcs

In order to be able to explicitly sign in and out, you'll want to use a menu option on the main menu most likely. We've provided a pre-made partial control that you can use for any script or template pages or more specifically on any page that uses a Master Layout page. The easiest and most common way is to add the partial login control to your layout page.

To add this control to your Layout page, go into your views\_layoutpage.wcs page and add the following <%= RenderPartial() %> link into the menu header:

<nav class="banner-menu-top float-right">
    <a href="#" class="hidable">
        <i class="fas fa-book"></i>
        Link
    </a>
    <a href="~/">
        <i class="fas fa-home"></i>
        Home
    </a>

    <!-- ADD THIS TO SHOW THE Login/Logout Menu -->
    <%= RenderPartial("~/UserManager/LoginMenu_Partial.usm") %>
</nav>

Sign in Widget only shows in Content Pages with Layout

Note this example embeds the signin menu on pages that use the _layoutpage.wcs as the master layout. Any other script/template pages have to explicitly embed this partial control using the syntax above.

For raw code driven pages, you can manually embed the HTML as needed into a Bootstrap menu:

lcHtml = Response.ExpandScriptToString("~/usermanager/LoginMenu_Partial.usm")

This widget is defined as a bootstrap dropdown meant to be dropped into the menu, but you can customize or create a new widget that works for other styled integration points.

Important: Static HTML pages cannot show signin information because authentication validation requires FoxPro code to run.

Code

  • Copy deploy\UserSecurityManager folder to your projects deploy\UserSecurityManager

This folder contains:

  • UserSecurityManagerProcess.prg
  • AppUserSecurity.prg
  • AppUserSecurity.dbf/fpt/cdx

These files are the code and data dependencies required to run the UserSecurity Manager.

The UserSecurityManagerProcess.prg file is a self-contained wwProcess implementation that provides the stock authentication features provided by this library. This is the process class that is mapped by the .usm extension.

The AppUserSecurity.prg becomes your application specific subclass of the wwUserSecurity class where you can set application specific settings for your user security instance. Specifically you can customize the filename, whether or not encryption is used and whether emails are validated or not.

Add Code to SecurityTestMain.prg

In order for the UserSecurityManagerProcess.prg to be hooked up to your existing application you need to excplicitly hook up this class and reference the library and dependencies.

Reference AppUserSecurity.prg

In the YourApp::OnInitCompleted() method add the following:

PROTECTED FUNCTION OnInitCompleted()

*** Hook in UserSecurityManager
SET PATH TO ".\UserSecurityManager" ADDITIVE
DO wwEncryption      && Required library for encrypted passwords

*** The following two are dependencies that are added in AppUserSecurity defined below
DO AppUserSecurity   && Add custom security class 

*** Hook up config class to main server Configuration
ADDPROPERTY(this.oConfig,;
            "oUserSecurityManagerProcess",;
            CREATEOBJECT("UserSecurityManagerProcessConfig"))
ENDFUNC

Note: The auto-generated code puts the above code into the OnLoad() rather than OnInitCompleted(). This is due to the fact that the default Web Connection templates don't generate OnInitCompleted() reliably. The auto-config includes a note that recommends moving the code from OnLoad() to OnInitCompleted().

This loads two configuration classes:

  • AppUserSecurity which is a wwUserSecurity sub-class
  • UserSecurityManagerProcessConfig configuration class
  • Adds the User Security class to the Server Configuration

AppUserSecurity.prg holds both of the classes for simplicity and you can customize these classes as you see fit - Specifically AppUserSecurity can point at other files or file locations or if necessary use a different data source to retrieve user security data.

Add USM Extension to your Process Handling

Add the following extension mapping to your SecurityTestServer::Process() method in the area where extensions are mapped:

CASE lcExtension == "USM"
	DO UserSecurityManagerProcess with THIS        

You'll also need a scriptmap for .usm. You can either manually add it.

Add the USM Scriptmap to your YourApp.ini

In YourApp.ini add usm to the ServerConfig::ScriptMaps setting:

[ServerConfig]
Virtual=SecurityTest
ScriptMaps=wc,wcs,md,st,usm
IISPath=IIS://localhost/w3svc/1/root

If you're using IIS you can then just do (running under Admin)

DO SecurityTest_Config.prg

For IIS Express or if you want to configure manually for IIS, add the scriptmap manually to web\web.config:

<configuration
	
    <webConnectionConfiguration>
        <!-- *** add USM here *** -->
        <add key="LiveReloadExtensions" value=".sp,.usm,.wc,.wcs,.html,.htm,.css,.js,.ts" />
    </webConnectionConfiguration>
	
	<system.webServer>
		<handlers>
			<add name=".wcs_wconnect-module" path="*.wcs" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode"/>
			...
			<!-- *** add USM here *** -->
			<add name=".usm_wconnect-module" path="*.usm" verb="*" type="Westwind.WebConnection.WebConnectionHandler,WebConnectionModule" preCondition="integratedMode"/>
		</handlers>
	</system.webServer>
</configuration>	

If you want to use the local Web Connection Web Server (.NET Core Middleware) add the .usm extension to the webconnectionwebserversettings.xml:

<?xml version="1.0" encoding="utf-8"?>
<WebConnectionWebServerSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <HandledExtensions>.sp,.usm,.wc,.wcs,.wcsx</HandledExtensions>
   ...
   <LiveReloadExtensions>.sp,.usm,.wc,.wcs,.md,.html,.htm,.css,.js,.ts</LiveReloadExtensions>
</WebConnectionWebServerSettings>

Customize the AppUserSecurity Class

AppUserSecurity.prg contains a subclass of wwUserSecurity class in which you can customize the behavior of your user security class.

You can specify whether new accounts have to be validated, and whether passwords are encrytped:

SET PROCEDURE TO AppUserSecurity ADDITIVE

*************************************************************
DEFINE CLASS AppUserSecurity AS wwUserSecurity
*************************************************************
#IF .F.
*:Help Documentation
*:Topic:
Class AppUserSecurity

*:Description:
Create a custom User Security class that sets up that 
customizes user security. You should rename this class
for each application you create.

Then change the cAuthenticationUserSecurityClass in 
UserSecurityManagerProcess
*:ENDHELP
#ENDIF

*** overridden properties

*-- Alias for the user file 
calias = "AppUserSecurity"

*-- Filename for the user file.
cfilename = "AppUserSecurity"

*-- If .t. requires Email validation of new accounts
lRequireValidation = .T.

*-- if set encrypts the passwords
cPasswordEncryptionKey = "619fasd34ads"

ENDDEFINE
*EOC AppUserSecurity 


*************************************************************
DEFINE CLASS UserSecurityManagerProcessConfig AS wwConfig
*************************************************************

cHTMLPagePath = "c:\WebConnectionProjects\UserSecurityManager\Web\"
cDATAPath = ""
cVirtualPath = "/UserSecurityManager/"

cEmailSenderName = "User Administration Manager"
cEmailSender = "admin@your-domain.com"
cEmailReplyTo = "noreply@your-domain.com"
cCompanyName = "ACME Products"
cCookieName = "_SecurityTest"

*** Determines whether the user can delete his or her own profile
lAllowUserToDeleteProfile = .T.

ENDDEFINE

Note you might be wondering how this class gets hooked up: The UserSecurityManagerProcessConfig class is hooked up in the SecurityTestServer::OnLoad() code shown earlier.

*** Hook up config class to main server Configuration
ADDPROPERTY(this.oConfig,;
           "oUserSecurityManagerProcess",;
           CREATEOBJECT("UserSecurityManagerProcessConfig"))

Configure your Process Class

In order for the processing to work we need to override the default Authentication behavior in your process class.

Configuration Settings Properties

First we need to tell the process class to use User Security Authentication and to use the AppUserSecurity class we configured earlier. Set the following properties at the top of the class in the class header:

cAuthenticationMode = "UserSecurity"  && `Basic` is default
cAuthenticationUserSecurityClass = "AppUserSecurity"

OnProcessInit()

Next we need to enable session state in your process class and enable authentication. The following example explicitly forces authentication to all requests except the home page and login/logout:

FUNCTION OnProcessInit
LOCAL lcScriptName, llIgnoreLoginRequest


*** Share the cookie with the UserSecurityManager cookie
THIS.InitSession(Server.oConfig.oUserSecurityManagerProcess.cCookieName,3600,.T.)

*** Global Authentication Mode
LOCAL lnLoginMode
lnLoginMode = 0  && 0-no auto authentication, 1 - Authenticate but don't force a login  2- always force a login to unauthenticated requests
DO CASE
 CASE lnLoginMode = 1
    this.Authenticate("ANY")  && apply logged in user but don't force login
 CASE lnLoginMode = 2
	*** Authenticate each request and force a login
	*** to all requests EXCEPT the ones in the list
	lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))

	*** Update this list with any endpoints that
	*** SHOULD NOT AUTHENTICATE. Leave out extensions.
	llIgnoreLoginRequest =  INLIST(lcScriptName,;
	  "default")
	  
	 IF !THIS.Authenticate("any","",llIgnoreLoginRequest)
	   IF !llIgnoreLoginRequest
	     RETURN .F.
	   ENDIF
	ENDIF
ENDCASE

* ... additional code here

ENDFUNC

Authenticate Per Page

If you use lnLoginMode = 2 above every page except those explicitly excluded will force authentication.

If you want to selectively authenticate specific pages you can use THIS.Authenticate() in a process method and RETURN .F. if not authenticated:

FUNCTION PriviligedContentPage()

IF !THIS.Authenticate("ANY")
   RETURN .F.  && Forces Authentication dialog if not logged in
ENDIF

*** this.oUser lets you check for a specific user or level
*** or whatever else you hook up for validation on that object
IF (this.oUser.Level < 5)
   this.ErrorMsg("Access Denied","You don't have the necessary rights to access this page.")
   RETURN
ENDIF

* ... secret sauce

Response.ExpandScript()
ENDFUNC

If THIS.Authenticate("ANY") fails to authenticate the user, a 401 request will be returned and the user is automatically redirected to the Login.usm page.

This allows for more control over the process, but it's more explicit and requires diligence to lock down each and every request that is restricted.

User Authentication Status

Once a user is logged in you can use various Process authentication properties to check for authentication status and get access to the User object.

Use Process.lIsAuthenticated, Process.cAuthenticatedUser, Process.cAuthenticatedUserName to check for user login status. For access to the user profile you can check Process.oUser which gives access to the data from the user table. If a user is not logged in, the Process.oUser object is available, but all values are empty. Always use Process.lIsAuthenticated to check if the user is in fact authenticated before checking Process.oUser.

All login/logout and other UI based authentication operations are essentially deferred to the UserSecurityManagerProcess class, so no additional code needs to be hooked up in your application class. Other than checking for security and accessing the Process.oUser to get security information you generally don't need to worry about behavior

Ready, Set, Authenticate

That's it - you should now be able to handle authentication in your Web Connection application.


© West Wind Technologies, 1996-2020 • Updated: 05/08/20
Comment or report problem with topic