Section 18.2. Authentication

18.2. Authentication

When a client attempts to connect to the server, FlashCom:

  1. Accepts the connection at the network level

  2. Reads the information passed by the client when connect( ) was called

  3. Places the client in a pending state from which it cannot send or receive data

  4. Calls the application.onConnect( ) method

The information passed into the onConnect( ) method is used by a FlashCom application to authenticate the person using the client. One way to authenticate a user is to create a .swf that will pass the user's username and password in the connect( ) call:

 nc.connect("rtmp:/ch18Samples/simpleAuthDemo", userName, password); 

Shortly, we'll address the fact that passing the username and password via RTMP isn't a great idea, because RTMP sends the data as plain text allowing passwords to be stolen. We'll discuss RTMPS in a minute. First, let's just see how a FlashCom application can authenticate a user.

When an application receives the username and password, it can look them up in a database or directory server. If the username and password match, the client connection is accepted, and the client is removed from the pending state. If the username and password don't match, the connection to the FlashCom Server is broken after sending a message to the client. The message can include a custom property, in this case msg :

 application.rejectConnection(client, {msg: "Invalid Credentials!"}); 

Chapter 11 and Chapter 14 contain examples of simple username and password authentication.

Other credentialsfor example, an access token or ticketmay be used. In addition to user- or system-supplied credentials, connection information should be checked. For example, the IP address of the connecting .swf may have to match a list of valid addresses, and the URL of the connecting .swf may have to match a specific address or partial address.

The entire process is based on the idea that only the user knows , and therefore can provide, her secret password. Anything that increases the possibility that her password can become known by someone else reduces the level of security of the system. Of course, if the user tells her password to someone else, the password has been fatally compromised. Some passwords are better than others because they are harder to guess, so password policies are often used to require the use of stronger and frequently changed passwords. Maintaining good password policies is an essential part of keeping credentials secure. However, there are some unique challenges in keeping credentials secret while Flash connects to FlashCom.

18.2.1. Network or Packet Sniffing

As of FlashCom 1.5.2, the RTMP protocol does not provide any level of encryption. RTMP information in a connection is encoded into AMF, MPEG, Nelly Moser, or other data formats. All these formats were designed to be efficiently decoded into ActionScript data, video, or audio. If an attacker can gain access to the network traffic being communicated between a .swf client and FlashCom, all the RTMP information can be decoded, including the username and password transmitted during the connection process. Therefore, before a username and password authentication scheme is used, the network connection must be secured against eavesdropping; otherwise , another more secure authentication method must be found.

Not all networks are equal in resisting eavesdropping. For example, a well-controlled switched network within a single organization may make it extremely difficult to intercept data flowing between clients and the server within the same network. If a FlashCom application is used only within the organization's network, passing a username and password when connecting via RTMP is acceptable. However, you should never simply assume without checking that an organization's network is secure. There may be wireless areas or other risks that are not immediately obvious.

Organizations often deploy a virtual private network (VPN) to secure access to their core networked resources within their own physical network and out onto the Internet. VPNs work by providing an encrypted tunnel that all information between the client and core services in the organization travels within. The security of VPNs is generally very goodincluding the authentication process, which normally involves the use of certificates in addition to usernames and passwords before a VPN connection is accepted. If connections to the FlashCom Server will always pass through a VPN, simple username and password authentication is also acceptable. In fact, it may be possible to create a system in which VPN users do not have to log in a second time to a FlashCom application after authenticating to the VPN. Instead, when a client attempts to connect, a FlashCom application can query the VPN system to identify each user connecting via the VPN. See the "Web applications and portals" section later in this chapter.

Another way to secure network traffic is to have each Flash client request an RTMPS connection. An RTMPS connection is similar to a tunneled RTMPT connection, except that tunneling is done using SSL. FlashCom 1.5.2 and earlier do not support SSL, so you must use additional hardware or software to decrypt tunneled traffic before it reaches FlashCom. More information on tunneling and using RTMPS is available at:

http://www.macromedia.com/devnet/mx/flashcom/articles/firewalls_proxy.html

For many, if not most, FlashCom applications, there is no guarantee that connections will occur across secured networks. In these cases, passing usernames and passwords across RTMP is strongly discouraged. Fortunately, many organizations already have the resources in place to provide protection for usernames and passwords. All that is needed is a web server and application server that support SSL, a database, and a Flash Remoting gateway.

The remainder of this section explores alternatives to username and password authentication over RTMP.

18.2.2. One-Time Ticket Systems

One way to protect a username and password is never to send them directly to FlashCom. In summary, a ticket can be used only once and only within a narrow time period to authenticate the user.

The sequence for a so-called ticketing system is as follows :

  1. The Flash movie passes the username and password via SSL to a web server.

  2. The web server hands the authentication request to an application server that looks up the username and password.

  3. If the user's credentials are valid, the server returns a single-use ticket to the Flash movie.

  4. The ticket acts like a one-time pass to connect to FlashCom. The Flash movie connects to FlashCom and passes the ticketinstead of the username and passwordin the connect( ) call.

  5. The FlashCom application checks with the application server that the ticket is valid and accepts the connection if it is.

The advantage of a ticketing system is that the username and password are passed over the encrypted SSL connection only. From then on, a ticket replaces the username and password as a way to authenticate a user.

The ticket is a long but unique string of characters that is used only once. An attacker who sniffs the ticket in transit to the server never sees the user's username or password and cannot use the same ticket later to connect to FlashCom, because a ticket can be used only once.

To use a web server that supports SSL to authenticate users, the following systems must be in place:

  • An SSL-equipped web server that can pass requests to the application server

  • A Flash Remoting gateway such as the one included with ColdFusion (see Chapter 12)

  • An application server that can query the authentication database

  • An authentication database such as a directory system or relational database

  • The Flash Communication Server

The interaction diagram in Figure 18-1 shows step-by-step how the client gets a single-use ticket.

Figure 18-1. A client passes a username and password via HTTPS and receives a single-use ticket. Separately, the ticket is stored in a server-side database for later authentication.

In Figure 18-1, the same database is used to check the user's username and password and to store the user's ticket after it has been generated. When a directory server is used, the user's credentials are checked against the directory server, but a separate database is required to store, retrieve, and delete tickets.

The client in Figure 18-1 is the Flash movie talking to the web server (don't mistake it for a normal browser connection).

Once the Flash client has a ticket, it can use the ticket to connect to a FlashCom application instance. The instance must check the ticket before accepting the client. Figure 18-2 shows step-by-step how the client connects to FlashCom using a ticket.

Figure 18-2. The client passes the ticket when connecting to FlashCom. If the ticket is valid, the client is allowed to connect. The ticket is deleted.

When the web/application server responds to FlashCom's request to check a ticket, it sends all the user information the FlashCom application needs, including things like the user's name and application roles. The user information is normally stored in the authentication database but may include information the user provided when logging in.

To illustrate one way to create a single-use ticket system, the book's web site provides sample files that use ColdFusion as an application server. The application uses two tables: a user table named PFCS_USER that contains usernames and passwords and a table named PFCS_TICKET that holds tickets. Table 18-1 shows the PFCS_USER table structure.

Table 18-1. PFCS_USER table

Attribute name

Datatype

Notes

userName

varchar(100)

Primary key

firstName

varchar(100)

 

lastName

varchar(100)

 

email

varchar(255)

 

password

varchar(255)

Should be encrypted


The PFCS_USER table structure could have been different. Instead of a userName attribute being the primary key, a sequence number (or autoincrementing number) is often used. If there is any possibility that a person's username will change, a sequence number is recommended. The password should also be encrypted.

It is a poor security practice to store unencrypted passwords in a database. To check the submitted password, encrypt it with the same algorithm used to encrypt the original password stored in the database and compare the two, as demonstrated a bit later under "Generating a ticket and returning user information."


This means that if a user forgets his password, you can't send him back his original (because you store only the encrypted version); instead, send the user a new, temporary password and ask him to change it after he logs in successfully.

Table 18-2 shows the structure for the PFCS_TICKET table.

Table 18-2. PFCS_TICKET table

Attribute name

Datatype

Notes

ticketID

char(35)

Primary key created using CreateUUID( ), which produces a unique 35-character string. If using char, the string length must exactly match what the hash produces.

userName

varchar(100)

Foreign key on PFCS_USER(userName); see Table 18-1.

ticket

char(32)

A pseudo- randomly generated string that has been hashed using MD5. If using char, the string length must exactly match what the hash produces.

ip

varchar(30)

IP address of the Flash client.

issueDateTime

Date

MS SQL requires you declare the datatype as DateTime instead of Date.

staleDateTime

Date

MS SQL requires you declare the datatype as DateTime instead of Date.


It is possible to generate tickets that are guaranteed to be unique and so, in theory, a ticket could act as the primary key of the PFCS_TICKET table. However, in this example, a separate ticketID is used as a primary key. The ticketID could be generated using a sequence number, but for added security, ColdFusion's CreateUUID( ) function is used because it generates a unique sequence of characters that is much harder to guess than a sequence number. Generating the ticket is described later. The IP address of the Flash client is stored as a further check that the same client that provided credentials initially is the one connecting to FlashCom with a ticket. Finally, the issueDateTime and staleDateTime dates define the range of time for which the ticket is valid. In most systems, the issueDateTime is not needed, because if the current time is greater than the staleDateTime , the ticket is no longer valid. If a ticket is preallocatedfor example, to be used once during a meetingthe issueDateTime may be useful, so we have kept it in the implementation.

18.2.2.1 Requesting a ticket

A Flash client can request a ticket by passing the user's username and password over an SSL connection to any script capable of authenticating the user and storing and returning a ticket. With ColdFusion, a CFML page can be used. However, since a Flash Remoting gateway is required so that FlashCom can communicate with an application server, a ColdFusion component is used. Example 18-1 shows the complete listing from a simple test client. It uses the newer Flash Remoting classes for ActionScript 2.0.

Example 18-1. Flash ticket authentication code
 import mx.remoting.Service; import mx.remoting.PendingCall; import mx.rpc.RelayResponder; import mx.rpc.ResultEvent; import mx.rpc.FaultEvent; import mx.controls.*; // Create a   _global.pfcs.user   object to hold the username and password. _global.pfcs = {user:{userName:"", password:""}}; // Create a Remoting ServiceProxy that points to the   user.cfc   file. //   user.cfc   has a   getTicket(userName, password)   method and is located in the //   pfcs/ch18/ticketAuth   subdirectory of the web server's document root directory. authService = new Service(   "https://flash-communications.net/flashservices/gateway",   null,   "pfcs.ch18.ticketAuth.user",   null,   new RelayResponder(this, "onAuthResult", "onAuthFault") ); //   onAuthResult( )   handles a result event from the   getTicket( )   call. function onAuthResult (ev) {   var result = ev.result;   if (result.SUCCESS != 1) {     showConnectionError("Invalid Credentials.", ">>> Authentication Alert!!! <<<");     return;   }   nc.pendingConnection = true;   nc.connect("rtmp:/ch18Samples/ticketAuthDemo", result.TICKETID, result.TICKET); } // onAuthFault( ) handles fault events that occur after calling   getTicket( )   . function onAuthFault (ev) {   showConnectionError("Error connecting to authentication server or database.",             ">>> Authentication Alert!!! <<<"); } // Create and initialize the NetConnection object. nc = new NetConnection( ); nc.pendingConnection = false; nc.onStatus = function (info) {   switch (info.code) {     case "NetConnection.Connect.Success":       _root.gotoAndPlay("Main");       break;     case "NetConnection.Connect.Rejected":       showConnectionError("Invalid Credentials.", info.code);       break;     case "NetConnection.Connect.Closed":       if (this.pendingConnection)         showConnectionError("Server closed the connection.", info.code);       break;     case "NetConnection.Connect.Failed":       showConnectionError("Unable to reach communication server.", info.code);       break;   } }; function showConnectionError (message, title) {   Alert.show(message, title);   nc.pendingConnection = false;   enableLoginForm(true); } //   doLogin( )   is called when a user submits her username and password. function doLogin (userName, password) {   enableLoginForm(false);   pfcs.user.userName = userName;   pfcs.user.password = password;   authService.getTicket({userName:userName, password:password}); } //   doLogout( )   is called when a user clicks the logout button. function doLogout ( ) {   nc.pendingConnection = false;   nc.close( );   _root.gotoAndPlay("Login"); } 

The preceding script creates a service object, authService , that refers to the users.cfc ColdFusion component:

 authService = new Service(   "https://flash-communications.net/flashservices/gateway",   null,   "pfcs.ch18.ticketAuth.user",   null,   new RelayResponder(this, "onAuthResult", "onAuthFault") ); 

The CFC contains the getTicket( ) method the client will call and is located in the pfcs/ch18/ticketAuth subdirectory of the web server's document root directory. The RelayResponder instance is used to route onResult( ) calls to the onAuthResult( ) function on the main timeline ( this ) and to route onFault( ) calls to the onAuthFault( ) function.

When the doLogin( ) function is called, the authService object is used to call the getTicket( ) method of the user.cfc :

 authService.getTicket({userName:userName, password:password}); 

If all goes well, the onAuthResult( ) method is called and passed a result event that contains a result object:

 function onAuthResult (ev) {   var result = ev.result;   if (result.SUCCESS != 1) {     showConnectionError("Invalid Credentials.", ">>> Authentication Alert!!! <<<");     return;   }   nc.pendingConnection = true;   nc.connect("rtmp:/ch18Samples/ticketAuthDemo", result.TICKETID, result.TICKET); } 

If the result object's SUCCESS property equals 1, login was successful and the result object will also contain TICKETID and TICKET properties. Otherwise, the username and password passed to getTicket( ) were either invalid or some other problem occurred. If user.cfc is not reachable or an unhandled exception occurs, onAuthFault( ) is called.

18.2.2.2 Generating a ticket and returning user information

Example 18-2 shows the getTicket( ) function from the user.cfc file. It creates the ticket to return to the user after accepting the username and password as inputs.

Example 18-2. The getTicket( ) function from user.cfc
 <cffunction name="getTicket" access="remote" returnType="struct" output="false">   <cfargument name="userName" type="string" required="true">   <cfargument name="password" type="string" required="true">   <cfset result=StructNew( ) />   <cfset result.success = 0 />   <cftry>     <cfquery datasource="pfcs" name="userQuery">       SELECT userName, firstName, lastName, email       FROM PFCS_USER       WHERE userName = <cfqueryparam                         cfsqltype="cf_sql_varchar"                          value="#userName#">       AND password = '#Hash(password)#'     </cfquery>     <cfif userQuery.RecordCount neq 1>       <cfreturn result>     </cfif>     <cfset ticketID = CreateUUID( ) />     <cfset ticket = Int(Rand( )* 1000000000000) & TimeFormat(Now( ), "hms")>     <cfset ticket = Hash(ticket)>     <cfquery datasource="pfcs" name="insertTicket">       INSERT into PFCS_TICKET       (ticketID, ticket, userName, ip, issueDateTime, staleDateTime)       VALUES       ('#ticketID#', '#ToString(ticket)#',        '#userQuery.userName#', '#CGI.REMOTE_ADDR#',        #CreateODBCDateTime(Now( ))#,        #CreateODBCDateTime( DateAdd( "n", 5, Now( ) ) )#)     </cfquery>     <cfset result.success = 1 />     <cfset result.userName = userQuery.userName />     <cfset result.firstName = userQuery.firstName />     <cfset result.lastName = userQuery.lastName />     <cfset result.email = userQuery.email />     <cfset result.ticket = ticket />     <cfset result.ticketID = ticketID />     <cfcatch type="Any">        <cfreturn result />     </cfcatch>   </cftry>   <cfreturn result /> </cffunction> 

In the preceding CFML script, setting required to TRue in the <cfargument> tags ensures that the function will not execute unless both the userName and password are passed in. The <cfset> tags generate a result struct (or object) with an initial success property set to 0.

Even though struct property names , such as success , are lowercase in the CFML code, when the struct is passed back to Flash or FlashCom, all the property names are uppercase, such as SUCCESS , and must be referred to that way.


Then the username and password are looked up in the user table using a SELECT statement:

 SELECT userName, firstName, lastName, email FROM PFCS_USER WHERE userName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#userName#"> AND password = '#Hash(password)#' 

The <cfqueryparam> tag is used to ensure that an attacker cannot use an SQL injection attack, in which an attempt is made to execute an SQL command in place of passing a plain username. Regardless of what text is passed in the userName variableincluding SQL fragmentsit is treated only as a string to compare against userName values in the database.

In the SELECT statement, a simple WHERE clause, as follows:

 WHERE userName = '#userName#' 

should never be used, because it is not secure against SQL injection attacks. If you do not hash passwords (i.e., you store them in your database in plain text, which is not recommended), use the <cfqueryparam> tag as follows to avoid SQL insertion attacks:

 AND password = <cfqueryparam        cfsqltype="cf_sql_varchar" value="#password#"> 

The password clause of the SELECT statement warrants explanation:

 AND password = '#Hash(password)#' 


The plain text password passed to the getTicket( ) method is passed to the Hash( ) function before being compared to passwords in the database. The Hash( ) function performs an MD5 one-way hash of the plain password into a string of text that cannot be decoded to retrieve the original password. For example, if the text string "bigSecret" is passed into the Hash( ) function, it returns a 128-bit value formatted as a 32-character string of hexadecimal characters, such as 975F81B906D47185D164EA64163834C3.

Some MD5 functions return lowercase letters A through F . For example, "bigSecret" may hash to 975f81b906d47185d164ea64163834c3. Use the same hash function to hash a password before saving it into a database and before looking a password up. To avoid problems, make sure every function used to hash a password produces the same letter case in the result.

The Hash( ) function is used in this example because passwords are not stored in the database in plain text; instead, the database stores a hash of the original password. Because hashes are one-way (the original string can't be retrieved from the hashed value) and it is computationally infeasible to produce two strings that hash to the same value (making it infeasible to guess a second string whose hash matches that of the password), hashes can serve in place of the actual password without revealing it. When a user provides a password, the server hashes the submitted password and checks whether it matches the hashed one in the database. If it does, the server can assume the user provided the correct password.

Since the password submitted to the getTicket( ) function is hashed to a 32-character string, no SQL insertion attack can be used in the password variable. If you do not hash passwords because you are storing them in your database in plain text (not recommended), then you must use the <cfqueryparam> tag to avoid insertion attacks.

If the userName and hashed password inputs do not match the username and passwordwhich was hashed when storedof one and only one record in the database, the result object is returned with a success value of 0 (indicating invalid credentials).

If the user's credentials do match, a ticket and ticketID are generated:

 <cfset ticketID = CreateUUID( ) /> <cfset ticket = Int(Rand( )* 1000000000000) & TimeFormat(Now( ), "hms")> <cfset ticket = Hash(ticket)> 

The CreateUUID( ) function generates a 35-character string representation of a 128-bit number. The string is guaranteed to be unique and is based on the system time, the IEEE 802 Host ID, and a random number. It is used as the primary key in the PFCS_TICKET table. The ticket itself is created initially as a random integer between 0 and 1,000,000,000,000, concatenated with the current time, and then MD5 hashed.

Then a new record is inserted into the PFCS_TICKET table with the new ticketID and ticket values:

 INSERT into PFCS_TICKET (ticketID, ticket, userName, ip, issueDateTime, staleDateTime) VALUES ('#ticketID#', '#ToString(ticket)#', '#userQuery.userName#',  '#CGI.REMOTE_ADDR#',  #CreateODBCDateTime(Now( ))#, #CreateODBCDateTime( DateAdd( "n", 5, Now( ) ) )#) 

The CGI object and its REMOTE_ADDR property are used to retrieve the IP address of the Flash client. Checking the IP address is not foolproofit can be spoofed. However, it is good practice to make sure that the IP address used when the ticket was generated matches the IP address of the client when it connects to FlashCom.

The Now( ) function returns the current date/time in ColdFusion, and the DateAdd( ) function adds time to a date/time object to produce a new date/time. In this case, the date/time produced is 5 minutes from now. If the client does not present the ticket to FlashCom in less than 5 minutes, the ticket will be considered stale and therefore invalid. A typical record in the PFCS_TICKET table is illustrated in Table 18-3.

Table 18-3. Record in the PFCS_TICKET table

Field name

Contents

ticketID

3ACFC25F-103A-8D7D-0C0E9751EC07F8E7

ticket

367F0308AFF69B898D3A034D37C75CF0

userName

blesser

ip

127.0.0.1

issueDateTime

6/18/2004 10:50:20 PM

staleDateTime

6/18/2004 10:55:20 PM


Provided all goes wellthe user's credentials were found in the database and a new ticket record was storedthe result object is updated with information about the user, the ticketID, and ticket values, and the success property is set to 1.

Before going on to describe how the client uses the ticket and ticketID to log into FlashCom, it is worth looking at directory server queries.

18.2.2.3 Authenticating against directory servers

In many organizations, usernames and passwords are stored in directory servers and can be checked against the directory server instead of looking them up in a database. The simplest and most secure way is often to use the user's credentials to bind (connect to) the directory server and search for his own entry in it. Example 18-3 shows a ColdFusion < cfldap > tag that could be used to replace the SELECT statement in Example 18-2.

Example 18-3. Using <cfldap> to look up a user's directory entry
 <cfldap   action="Query"   name="userQuery"   server="ldap1.flash-communications.net"   attributes="uid, cn, mail, sn"   start="ou=people, o=flash-communications.net, o=flash-communications"   filter="uid=#userName#"   username="uid=#userName#,             ou=people, o=flash-communications.net, o=flash-communications"   password="#password#" /> 

Example 18-3 performs a query named userQuery against an LDAP server named ldap1.flash-communications.net (that server doesn't exist so don't try the actual tag as shown). The query requests the user ID ( uid ), common name ( cn ), email address ( mail ), and surname ( sn ) attributes of the user's entry in the directory. The starting point for the search is a node in the directory tree. In this case, the search begins in the organizational unit ( ou ) named "people", which is a child of the "flash-communications.net" and "flash-communications" organization ( o ) nodes. The search filter states that the uid must match the username property. The filter does not need to include the password because the user is authenticated while binding to the directory. If the user's password is invalid, the bind attempt will be rejected and no search will take place. The username and password attributes of the <cfldap> tag are used to authenticate against and bind to the directory. However, the username cannot simply be passed to the directory. It must be specified in a way that matches a uid attribute in the directory. In this case, the uid is an attribute of an entry in the "people" ou . The username is therefore set as follows:

 "uid=#userName#, ou=people, o=flash-communications.net, o=flash-communications" 

Example 18-3 illustrates only one directory query. It assumes all user records are in the "people" organizational unit. Every directory server can have its own unique schema and access controls that may require a very different set of search criteria. Consult with the administrators of a directory before attempting to connect to one.

Although we've changed authentication schemes from a database to a directory server, the results of our ColdFusion CFC getTicket( ) method don't change as far as the Flash client can tell. So even if using a directory server for authentication, the getTicket( ) method still returns a result object with ticketID and ticket properties to the Flash client, in this case to the onAuthResult( ) method.

18.2.2.4 Connecting to FlashCom with a ticket

Once the client's onAuthResult( ) method receives a ticket, it can attempt to connect to FlashCom as follows (note that it passes the ticket ID and ticket instead of a username and password as credentials):

 nc.connect("rtmp:/ch18Samples/ticketAuthDemo", result.TICKETID, result.TICKET); 

In the example, the TICKETID and TICKET properties are passed as separate parameters in the connect( ) call. They could also be passed as properties of a credentials object. However they are passed to the application, the important point is that no user information such as the username or password is passed. An attacker that has successfully sniffed the network traffic between the Flash client and FlashCom will see only two long hashed strings similar to those shown in Table 18-3. There is a low risk that an attacker can intercept the ticket and impersonate the user (if the ticket hasn't been presented yet) as discussed later under "One-time ticket systems summary."

Barring such diversions , let's see how the ticket is authenticated by a FlashCom application.

18.2.2.5 Checking the ticket and returning user information

When a FlashCom application receives the connection request, it has to check the ticketID and ticket values before allowing the connection:

 this.authService.checkTicket(new AuthResponder(this, pendingID),   {ticketID:ticketID,    ticket:ticket,    ip:client.ip,    hostIdentifier:"k38djskl3o9"}); 

Shortly, we'll show how FlashCom can use Flash Remoting to call the checkTicket( ) method of the same user.cfc that includes the getTicket( ) method. However, checkTicket( ) might easily be part of another CFC or even on another server that is not reachable from the Internet. Example 18-4 shows the user.cfc file's checkTicket( ) function in its entirety, which we'll discuss after you've perused the code. We'll have a look at the Server-Side ActionScript that calls it later.

Example 18-4. The checkTicket( ) function in user.cfc
 <cffunction name="checkTicket" access="remote" returnType="struct" output="false">   <cfargument name="ticketID" type="string" required="true">   <cfargument name="ticket" type="string" required="true">   <cfargument name="ip" type="string" required="true">   <cfargument name="hostIdentifier" type="string" required="true">   <cfset result=StructNew( ) />   <cfset result.success = 0 />   //Check that the user agent begins with "FlashCom"   <cfif (FindNoCase("FlashCom", CGI.HTTP_USER_AGENT) neq 1)>     <cfreturn result />   </cfif>   //Perform a combined IP check and host password lookup   <cfset ipList=StructNew( )>   <cfset ipList["127.0.0.1"] = "k38djskl3o9">   <cfset ipList["172.28.253.102"] = "cmvhw1s0lv">   <cfset ipList["192.168.0.1"] = "0kn59sxnd">   <cfif (ipList[CGI.REMOTE_ADDR] neq hostIdentifier)>     <cfreturn result />   </cfif>   <cftry>     <cfset rightNow = CreateODBCDateTime( Now( ) ) >     <cfquery datasource="pfcs" name="userTicketQuery">       SELECT u.userName, u.firstName, u.lastName, u.email       FROM PFCS_USER u, PFCS_TICKET t       WHERE         t.ticketID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticketID#">         AND t.ticket = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticket#">         AND t.ip = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ip#">         AND (#rightNow# BETWEEN t.issueDateTime AND t.staleDateTime)         AND u.userName = t.userName     </cfquery>     <cfif userTicketQuery.RecordCount neq 1>       <cfreturn result />     </cfif>     <cfquery datasource="pfcs" name="deleteTicket">       DELETE FROM PFCS_TICKET       WHERE         ticketID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticketID#">         AND ticket = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticket#">     </cfquery>     <cfset result.success = 1 />     <cfset result.userName = userTicketQuery.userName />     <cfset result.firstName = userTicketQuery.firstName />     <cfset result.lastName = userTicketQuery.lastName />     <cfset result.email = userTicketQuery.email />     <cfcatch type="Any">       //Uncomment the next line for debugging purposes       //cfset result.error = cfcatch.detail /       <cfreturn result />     </cfcatch>   </cftry>   <cfreturn result /> </cffunction> 

Before checking the database to see if the ticket is valid, the checkTicket( ) function performs some simple checks to reduce the possibility that something other than an authorized FlashCom Server is attempting to call it. First, it checks the user agent string sent by FlashCom using the CGI.HTTP_USER_AGENT property. FlashCom always provides a string similar to "FlashCom/1.5.2", so we simply check that the string begins with "FlashCom". The CGI.HTTP_USER_AGENT property can be spoofed, but it is an easy and good thing to check.

Then the script does a combined IP check and host password lookup. It retrieves the IP address via the CGI.REMOTE_ADDR property and compares the hostIdentifier argument against the host identifier in the ipList . The hostIdentifier is not the domain name of each FlashCom Server. It is a password-like string such as "k38djskl3o9". If the IP address is not in the ipList or the hostIdentifier does not match the text in the ipList for the IP address specified by CGI.REMOTE_ADDR , the function simply returns. Hardcoding a passwordwhich is effectively what the hostIdentifier isis not a best practice and an attacker can also spoof an IP address. However, all three measures raise the bar on what an attacker must do to call the checkTicket( ) function successfully.

To provide better security, the checkTicket( ) function can be moved to a script hosted on a secured Flash Remoting server behind a firewall that can be reached only by permitted hosts . Even then, these simple checks are valuable in case an allowed host is briefly compromised.

Assuming the caller has passed our security checks, the function gets the current time and looks up the ticket in the database using a SELECT statement:

 SELECT u.userName, u.firstName, u.lastName, u.email FROM PFCS_USER u, PFCS_TICKET t WHERE t.ticketID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticketID#"> AND t.ticket = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ticket#"> AND t.ip = <cfqueryparam cfsqltype="cf_sql_varchar" value="#ip#"> AND (#rightNow# BETWEEN t.issueDateTime AND t.staleDateTime) AND u.userName = t.userName 

The query is run against two tables so that some personal information can be retrieved from the PFCS_USER table at the same time as the ticket is checked in the PFCS_TICKET table. For convenience, short aliases for the tables, u and t , are created and used throughout the statement to make it clear which table each attribute belongs to:

 FROM PFCS_USER u, PFCS_TICKET t 

The two tables are joined on their userName columns :

 AND u.userName = t.userName 

Since there will be only one matching user record in the PFCS_USER table and one matching ticket entry in the PFCS_TICKET table, a successful query will return only one record. If the ticket, ticketID, and IP address are not all found in a record, no records will be returned. The query also makes sure the ticket is not stale by verifying that the current time is between the time the ticket was issued and the time it is set to expire:

 AND (#rightNow# BETWEEN t.issueDateTime AND t.staleDateTime) 

If the query is successful, the result object is updated with user information, given a success property of 1, and returned to FlashCom. Again, the user's personal information was never sent unencrypted to the client but is now available both to the client via the earlier call to getTicket( ) and to FlashCom via the call to checkTicket( ) .

18.2.2.6 Authentication in FlashCom

It's time to have a look at how FlashCom calls the checkTicket( ) method and handles the results from it. Instead of providing a simple demonstration script, I'll cover how to use an authentication object, which makes authentication easier to manage in real-world FlashCom applications. Example 18-5 shows the main.asc file that uses an Auth class (not shown but available on the book's web site) to create an object capable of authenticating clients.

Example 18-5. A main.asc script with Auth class and onAuthenticate( ) method
 load("Auth.asc"); application.onAppStart = function ( ) {   // Create a global   pfcs   object with a   clients   object to   // store client objects by userName.   pfcs = {clients:{}};   // Create an authentication object to authenticate clients.   // In this case we specify ticket authentication.   auth = new Auth("ticketAuth"); }; application.onConnect = function (client, ticketID, ticket) {   return auth.authenticate(client, ticketID, ticket); }; application.onAuthenticate = function (client, result) {   // Make sure the authenticated user has not already logged in.   if (pfcs.clients[result.USERNAME]) {     application.rejectConnection(client, {msg:"You are already logged in."});     return;   }   // Set up the client's user record.   client.pfcs = {user:{}};   var user = client.pfcs.user;   user.userName = result.USERNAME;   user.firstName = result.FIRSTNAME;   user.lastname = result.LASTNAME;   user.email = result.EMAIL;   // Store the client in the   pfcs.clients   object by   userName   .   pfcs.clients[user.userName] = client;   // Accept the client's connection request.   application.acceptConnection(client); }; application.onDisconnect = function (client) {   var userName = client.pfcs.user.userName;   delete pfcs.clients[userName];   trace("Deleted pfcs clients entry for " + userName); }; 

When the preceding main.asc script loads, it also loads the Auth.asc file that contains the definition for the Auth class. In the onAppStart( ) method, an Auth object is created, and the constructor is passed the string "ticketAuth". Behind the scenes, the Auth object will load configuration information to customize its authenticate( ) method to perform ticket-based authentication. The class can also load other authentication methods , which we'll look at later.

When a client attempts to connect to the FlashCom application, the application's onConnect( ) method is called. In this example, onConnect( ) simply calls Auth.authenticate( ) and passes back its return value. Therefore, if authenticate( ) returns false , the client connection is immediately refused .

If it returns null , the client is placed in a pending state. If the Auth object cannot authenticate the client later, it will simply reject the client's connection. However, if the client is authenticated, the application.onAuthenticate( ) method is called and the result object returned to it. Any application logic required to further check or initialize an accepted client is performed in the onAuthenticate( ) method. The onAuthenticate( ) method is not intrinsic to the application object. It is added dynamically to it in the main.asc file.

In Example 18-5, the global pfcs.clients object is checked to make sure the user has not already logged in. If not, the client is added to the pfcs.clients object and the user's personal information is added to the client object before her connection is accepted. When the client disconnects, she is removed from the pfcs.clients object.

Example 18-6 shows the authenticate( ) , onAuthResult( ) , and onAuthStatus( ) methods of the Auth object used when "ticketAuth" is passed to the Auth constructor. This code is loaded by the Auth.asc file from the ticketAuth/authMethod.asc file.

Example 18-6. The authenticate( ), onAuthStatus( ), and onAuthResult( ) methods when "ticketAuth" has been specified
 Auth.prototype.name = "ticketAuth"; Auth.prototype.authenticate = function (client, ticketID, ticket) {   // The static   Text.trim( )   method trims leading and trailing whitespace. It is   // defined in the   Text.as   file, which is provided on the book's web site.   ticketID = Text.trim(ticketID);   ticket = Text.trim(ticket);   if (!ticketID  !ticket) {     application.rejectConnection(client,         {msg:"Empty or missing user name or ticket."});     return false;   }   var pendingID = this.freeID++;   this.pendingClients[pendingID] = client;   this.authService.checkTicket(new AuthResponder(this, pendingID),     {ticketID:ticketID,      ticket:ticket,      ip:client.ip,      hostIdentifier:this.hostIdentifier});   return null; }; Auth.prototype.onAuthStatus = function (pendingID, status) {   var client = this.removePendingClient(pendingID);   if (status.description) msg = status.description;   else msg = "Error: Can't reach authentication service.\n";   if (!client) return;   application.rejectConnection(client, {msg: msg}); }; Auth.prototype.onAuthResult = function (pendingID, result) {   var client = this.removePendingClient(pendingID);   if (!client) return;   if (result.SUCCESS != 1) {     application.rejectConnection(client, {msg: "Invalid Credentials"});   }   else {     if (application.onAuthenticate) application.onAuthenticate(client, result);   } }; 

Before calling authService.checkTicket( ) , the authenticate( ) method places the client in a pendingClients object where it can be retrieved later by its pendingID . When it calls the checkTicket( ) function, it creates a new AuthResponder object and passes it a reference to itself and the pendingID . The responder 's onResult( ) method (not shown) will be called if checkTicket( ) returns a result, and its onStatus( ) method (also not shown) will be called if there is an unhandled error. The onResult( ) method will use the reference to the auth object to call the Auth.onAuthResult( ) method and the responder's onStatus( ) method will call the Auth.onAuthStatus( ) method. Both Auth methods are passed the pendingID of the client.

The authenticate( ) method returns null unless it finds something immediately wrong with the ticketID or ticket. The value it returns is also returned by the application.onConnect( ) method. If null is returned, the client is kept in a pending state by FlashCom while it waits for the call to return and the Auth object to decide whether to accept the client or not.

The server-side onAuthResult( ) method is similar to the Flash client's onAuthResult( ) method in Example 18-1. If the result.SUCCESS property is not 1, it rejects the client's connection attempt. If it is 1, it calls the application.onAuthenticate( ) method and passes it a reference to the client and the result from the checkTicket( ) call.

The onAuthResult( ) and onAuthStatus( ) methods have to retrieve the client object from the pendingClients object. They use the removePendingClient( ) method, shown in Example 18-7, to do so.

Example 18-7. The Auth.removePendingClient( ) method
 Auth.prototype.removePendingClient = function (pendingID) {   var client = this.pendingClients[pendingID];   delete this.pendingClients[pendingID];   return client; }; 

The rest of the Auth class and the AuthResponder code is not shown but is available from the book's web site. The code is organized with the Auth.asc file in the application's home directory. Subdirectories hold the various authentication methods. Each subdirectory, for example the ticketAuth subdirectory, also holds a config.asc file that contains information about the host and authentication service. Example 18-8 lists a ticketAuth/config.asc file.

Example 18-8. An authentication method config.asc file
 // DO NOT EDIT THIS LINE. Auth.current = Auth.instances[Auth.instances.length - 1]; // Edit the following lines so that you correctly set the remoting host // and remoting service name you need to access for authentication. // For example: // Auth.current.gatewayURL = "http://myHost.myDomain.com/flashservices/gateway/"; // Auth.current.serviceName = "my.path.to.a.service"; // OK TO EDIT FROM HERE ON. Auth.current.gatewayURL  = "http://localhost:8500/flashservices/gateway/"; Auth.current.serviceName = "pfcs.ch18.ticketAuth.user"; Auth.current.hostIdentifier = "k38djskl3o9"; 

18.2.2.7 One-time ticket systems summary

One-time ticket systems provide excellent protection for sensitive information such as usernames and passwords by using an HTTPS-enabled web application. If you have a ticketing system in place, you can also send other sensitive data such as financial information via HTTPS rather than through FlashCom. However, ticket systems like the one described here are not perfect. They can still be compromised by a sophisticated man-in-the-middle attack if an attacker is able not only to sniff but also intercept the ticket and then forward it to FlashCom from his own client. To put the risk in perspective, an attack of that nature is very difficult to do on today's networks.

18.2.3. One-Time Hash Systems

Under some circumstances, an SSL-enabled web server may not be available. In that case, a one-time hashing scheme can be used. In practice, it is somewhat less secure and more limited than using an SSL-enabled web application and ticketing system. One-time hashing relies on the client receiving a unique string of text called a challenge . The client combines the challenge text with the user's password, performs a one-way hash of the combined text, and returns the hash to the server. The server performs the same operationcombining the user's password and the challenge text and hashing it. If the hash sent by the client matches the server's version of the hash, the client is allowed to connect. On its own, a one-time hash system cannot protect the user's username during the connection process or things like financial information the way SSL can, but it can protect the user's password. Every time the user authenticates, a different challenge message is sent and, therefore, the resulting hash that is sent back is also different. There are different one-time hash schemes. Another name for them is challenge-response authentication . The Challenge Handshake Authentication Protocol (CHAP) described in RFC 1994 is a good example.

We've already used a one-way hash function named Hash( ) in ColdFusion that implements the MD5 algorithm. Another one-way hash algorithm is named SHA1. Instead of a 128-bit value, it produces a 160-bit value and is considered stronger than MD5 (the longer the hash result, the harder to reverse it). If the text "bigSecret" is hashed with an SHA1 function, the text string 6b9b36293b36e4c410ad0b5a-51bc9eb8af1be63b is returned. It is a 40-character hexadecimal representation of the hashed value.

The challenge string is required because the client must send a different hashed string every time it tries to connect. If it sent the same string each time, someone sniffing the network would not learn the original password but would be able to log into the system using the hashed text. The fact that the challenge phrase is sent in plain text and could be intercepted poses a potential problem. The same challenge phrase should not be issued more than once to a client. If an attacker gets a challenge phrase that is associated with a particular user login, she may be able to retrieve the correct hash and send it. Random numbers are used as challenge strings, so the scheme described in this chapter is good but not perfect.


A drawback to using a one-time hashing system with FlashCom is that it requires a Flash client to request a challenge string, receive it, and then send back the hash of the challenge string and password. FlashCom does not permit a three-step conversation to occur between a client and FlashCom until after a client has been accepted. When a client connects to FlashCom, the values passed into the connect( ) method are available to the server, but Flash cannot receive challenge text and send a response without the connection being accepted.

There are two ways around the problem. One approach is to use a web application to provide a challenge phrase and then have the client try to connect to FlashCom using that phrase. An alternative is for FlashCom to provide the challenge phrase. Once the challenge phrase is obtained from FlashCom or a web applicationwe don't deal directly with the web application portion herethe remainder of the problem is the same.

In the latter scenario, the client attempts to connect to FlashCom twice. The first time it passes FlashCom the user's username and is rejected. When FlashCom rejects the connection, it passes back the challenge phrase in the object specified as the second parameter to the application.rejectConnection( ) method:

 application.rejectConnection(client, {challenge:"challengeText"}); 

When the client's connection is rejected, it can get the challenge phrase from the application property of the info object passed back to the onStatus( ) method of the client's NetConnection object:

 nc.onStatus = function (info) {   challengeString = info.application.challenge;   // Perform a hash of the challenge text and attempt a reconnection. } 

Then it can attempt to connect to FlashCom again with a hash of the challenge text ( challengeString in this example) and the user's password.

To demonstrate a working one-time hash system, we'll use the "connect to FlashCom twice" approach and the SHA1 algorithm. However, because passwords are MD5-hashed before being stored in the sample application's database, we'll also be using MD5. However, there is a small problem: neither Flash nor FlashCom provide built-in MD5 or SHA1 hash functions. Fortunately, Paul Johnston has made available JavaScript implementations of both MD5 and SHA1 under BSD licensing. For the sample introduced here, I've ported his scripts to ActionScript 2.0 for use in Flash and ActionScript 1.0 for use with FlashCom. The source is not shown here but is available from the book's web site.

18.2.3.1 Connecting twice with Flash

To simplify the process of attempting to connect to FlashCom to get the challenge text and then connecting on the second try, two NetConnection objects can be used. One is responsible for the first connection attempt that will be rejected (but will obtain the challenge text) and the other for the second attempt that may be accepted. Example 18-9 includes code that creates both challenge_nc and main_nc NetConnection objects.

Example 18-9. Using two NetConnection objects to log into FlashCom
 import mx.controls.*; import SHA1; import MD5; // Create a   _global.pfcs.user   object to hold the username and password. _global.pfcs = {user:{userName:"", password:"", cpHash:""}}; sha1 = new SHA1( ); md5 = new MD5( ); // Create the NetConnection object we will use to connect to FlashCom // the second time. main_nc = new NetConnection( ); main_nc.pendingConnection = false; main_nc.onStatus = function (info) {   switch (info.code) {     case "NetConnection.Connect.Success":       _root.gotoAndPlay("Main");       break;     case "NetConnection.Connect.Rejected":       this.pendingConnection = false;       showConnectionError(info.application.msg, info.code);       break;     case "NetConnection.Connect.Closed":       if (this.pendingConnection) {         showConnectionError(info.application.msg, info.code);       }       break;     case "NetConnection.Connect.Failed":         showConnectionError("Can't connect to the server.", info.code);       break;   } }; // Create the NetConnection object we will use to connect the first time // in order to get the challenge text. If the connection is rejected and // the challenge text is returned, the   main_nc   object will be used // to attempt the second connection. challenge_nc = new NetConnection( ); challenge_nc.pendingConnection = false; challenge_nc.onStatus = function (info) {   switch (info.code) {     case "NetConnection.Connect.Success":       showConnectionError("Application Error.", info.code);       break;     case "NetConnection.Connect.Rejected":       this.pendingConnection = false;       var challenge = info.application.CHALLENGE;       if (challenge) {         var id = info.application.ID;         var hashedPassword = md5.hex_md5(pfcs.user.password).toUpperCase( );         var finalHash = sha1.hex_sha1(hashedPassword + challenge);         main_nc.connect("rtmp:/ch18Samples/chapAuthDemo", id, finalHash);       }       else {         showConnectionError(info.application.msg, info.code);       }       break;     case "NetConnection.Connect.Closed":       if (this.pendingConnection) {         showConnectionError(info.application.msg, info.code);       }       break;     case "NetConnection.Connect.Failed":       showConnectionError("Can't connect to the server.", info.code);       break;   } }; //   showConnectionError( )   displays an alert and reenables the login form. function showConnectionError (message, title) {   Alert.show(message, title);   challenge_nc.pendingConnection = false;   enableLoginForm(true); } //   doLogin( )   is called when a user submits his username and password. function doLogin (userName, password) {   enableLoginForm(false);   pfcs.user.userName = userName;   pfcs.user.password = password;   challenge_nc.pendingConnection = true;   challenge_nc.connect("rtmp:/ch18Samples/chapAuthDemo", userName); } //   doLogout( )   is called when a user clicks the logout button. function doLogout ( ) {   main_nc.pendingConnection = false;   main_nc.close( );   _root.gotoAndPlay("Login"); } 

When the user attempts to log in, the challenge_nc NetConnection object is used to pass the user's username to FlashCom:

 challenge_nc.connect("rtmp:/ch18Samples/chapAuthDemo", userName); 

The FlashCom Server will check whether the username is valid. If it is, FlashCom will reject the connection but will also return an application object containing a challenge ID and challenge text. The challenge ID is not strictly necessary, but is used in this example to look up the challenge text. The application object is checked within the challenge_nc object's onStatus( ) method, an excerpt of which is shown here:

 case "NetConnection.Connect.Rejected":   this.pendingConnection = false;   var challenge = info.application.CHALLENGE;   if (challenge) {     var id = info.application.ID;     var hashedPassword = md5.hex_md5(pfcs.user.password).toUpperCase( );     var finalHash = sha1.hex_sha1(hashedPassword + challenge);     main_nc.connect("rtmp:/ch18Samples/chapAuthDemo", id, finalHash);   }   else {     showConnectionError(info.application.msg, info.code);   }   break; 

If there is no info.application.CHALLENGE property, then the username was not found and a login error is reported to the user. However, if a CHALLENGE property is returned, the following steps are taken:

  1. The plain text password is hashed using an MD5 hash so that it matches the hashed password in the authentication database. Omit this step if you are storing passwords in your authentication database in plain text (not recommended).

  2. The hashed password and challenge text are concatenated together into a single string of text.

  3. The concatenated password and challenge text are hashed using the SHA1 algorithm.

  4. The main_nc NetConnection object is passed the challenge ID and the final hash from Step 3 and used to attempt to connect to FlashCom a second time.

Here are the three lines of code that perform the four aforementioned steps:

 var hashedPassword = md5.hex_md5(pfcs.user.password).toUpperCase( ); var finalHash = sha1.hex_sha1(hashedPassword + challenge); main_nc.connect("rtmp:/ch18Samples/chapAuthDemo", id, finalHash); 

After these lines execute, the main_nc.onStatus( ) method will be called and we will know whether we are allowed to connect.

If you are storing passwords in your authentication database in plain text (not recommended), omit Step 1 and change the code to:

 var finalHash = sha1.hex_sha1(pfcs.user.password + challenge); main_nc.connect("rtmp:/ch18Samples/chapAuthDemo", id, finalHash); 

18.2.3.2 Managing connection attempts in FlashCom

The main.asc file for our two-connection-attempts example is not shown but is almost identical to the main.asc file shown in Example 18-5. The only difference is that a different authentication method is used by the Auth object:

 auth = new Auth("chapAuth"); 

The chapAuth subdirectory contains a config.asc and an authMethod.asc file. Example 18-10 lists most of the authMethod.asc file. The entire file can be downloaded from the book's web site.

Example 18-10. An excerpt of the chapAuth/authMethod.asc file
 Auth.prototype.name = "chapAuth"; load("SHA1.asc"); Auth.prototype.authenticate = function (client, id, hash) {   id = Text.trim(id);   hash = Text.trim(hash);   if (!id && !hash) {     application.rejectConnection(client,           {msg:"Empty or missing user name or hash."});     return false;   }   var pendingID = this.freeID++;   this.pendingClients[pendingID] = client;   if (id && !hash) {     // Assume client has passed a username and requires a challenge phrase     // in return.     this.authService.setChallenge(new AuthResponder(this, pendingID),         {userName:id,ip:client.ip,hostIdentifier:this.hostIdentifier})   }   else {     // Assume the client is presenting a challengeID and hash     // and remember the hash the client provided.     client.__hash = hash;     this.authService.getChallenge(new AuthResponder(this, pendingID),        {id:id,ip:client.ip,hostIdentifier:this.hostIdentifier})   }   return null; }; Auth.prototype.onAuthResult = function (pendingID, result) {   var client = this.removePendingClient(pendingID);   if (!client) return;   if (result.SUCCESS != 1) {     application.rejectConnection(client, {msg: "Invalid Credentials"});     return;   }   if (result.CHALLENGE) {     application.rejectConnection(client, result);     return;   }   if (result.USERNAME) {     var hash = client.__hash;     delete client.hash;     if (hash == SHA1.hex_sha1(result.PASSWORD + result.TICKET)) {       if (application.onAuthenticate)         application.onAuthenticate(client, result);       return;     }   }   application.rejectConnection(client, {msg: "Invalid Credentials"}); }; 

The authenticate( ) method in the preceding code is called from within the application.onConnect( ) method whenever a client tries to connect. If both parameters are missing, the connection is simply rejected after placing an error message in the application object, as shown in this excerpt:

 if (!id && !hash) {   application.rejectConnection(client,     {msg:"Empty or missing user name or hash."});   return false; 

However, if only the id parameter has been passed in, then we assume the client has passed in a username and is requesting a challenge. In that case, the setChallenge( ) method of user.cfc is called. If the username passed to it is valid, it creates a new ID and challenge, stores them (along with the username and IP address) in a database, and returns the id and challenge properties in a result object. If the username is not valid, nothing is stored and a SUCCESS property of 0 is returned. For convenience, the PFCS_TICKET table shown in Table 18-2 is used to store the challenge text and ID. The challenge is stored in the ticket column and challenge ID in the ticketID column.

If both an id and hash are passed into the authenticate( ) method, the hash is stored as a property of the client object so it can be retrieved later. The user.cfc 's getChallenge( ) method is called with the id . The getChallenge( ) method will also return a result object. If the challenge text was found, the result.SUCCESS property will be 1.

Before setChallenge( ) and getChallenge( ) are called, the client is placed in a pending state. When setChallenge( ) or getChallenge( ) returns, the onAuthResult( ) method will be called. It retrieves the pending client and looks at the result object to determine which method is returning. If the SUCCESS property is not 1, it simply rejects the client connection attempt and returns an error message. However, if a CHALLENGE property is returned, it rejects the connection and returns the result object that includes the challenge ID and challenge text to the client. Finally, if the result.USERNAME property is present, then a result record has been returned that contains the challenge ID, challenge text, personal user information, and, most importantly, the user's password:

 var hash = client.__hash; delete client.hash; if (hash == SHA1.hex_sha1(result.PASSWORD + result.TICKET)) {   if (application.onAuthenticate)     application.onAuthenticate(client, result);   return; } 

All that is necessary when the challenge text is returned by the getChallenge( ) function is to combine it with the password retrieved from the database, hash it with SHA1, and compare it to the hash submitted by the client. If the two hashes match, the client has authenticated.

The setChallenge( ) and getChallenge( ) functions are not reproduced here but can be found on the book's web site.

18.2.4. Single Sign-On

Unless you need to provide significantly different levels of security within a portal or Internet application, there is no reason to force users to log in more than once per session. For example, if you need to add a communication feature such as video chat to an existing web application, why make the user log in a second time to chat? If a user is already logged in to a web application, the application should be able to get her username or user ID from her session information and generate a single-use ticket that Flash can use to connect to FlashCom. The same is true of portals.

18.2.4.1 Web applications and portals

There are a number of ways to implement single sign-on systems. Many web applications and portals use different authentication schemes and make user information available in different ways. It is not possible to review them all here. Instead, here is a simple example that uses a ColdFusion page that both loads a .swf file and provides it with a single-use ticket, ticketID, and other information it can use to connect to FlashCom. The page is protected by an Application.cfm page (not shown here) that forces a user to log in before the page can be displayed. See "Role-Based Authorization" later in this chapter for more information on Application.cfm files.

The page shown in Example 18-11 uses the FLASHVARS parameter to create ticketID , ticket , and userName variables on the main timeline of a movie called webPortalTicketTestClient.swf .

Example 18-11. The main.cfm page
 <html> <head> <title>webPortalTicketTestClient</title> </head> <body bgcolor="#ffffff"> <cfquery datasource="pfcs" name="userQuery">   SELECT userName, firstName, lastName, email   FROM PFCS_USER   WHERE userName = '#GetAuthUser( )#' </cfquery> <cfif userQuery.RecordCount eq 1>   <cfset ticketID = CreateUUID( ) />   <cfset ticket = Int(Rand( )* 1000000000000) & TimeFormat(Now( ), "hms")>   <cfset ticket = Hash(ticket)>   <cfquery datasource="pfcs" name="insertTicket">     INSERT into PFCS_TICKET     (ticketID, ticket, userName, ip, issueDateTime, staleDateTime)     VALUES     ('#ticketID#', '#ToString(ticket)#', '#userQuery.userName#',      '#CGI.REMOTE_ADDR#', #CreateODBCDateTime(Now( ))#,      #CreateODBCDateTime( DateAdd( "n", 5, Now( ) ) )#)   </cfquery>   <cfset flashVars="userName=#GetAuthUser( )#" />   <cfset flashVars="#flashVars#&firstName=#userQuery.firstName#" />   <cfset flashVars="#flashVars#&lastName=#userQuery.lastName#" />   <cfset flashVars="#flashVars#&email=#userQuery.email#" />   <cfset flashVars="#flashVars#&ticket=#ticket#" />   <cfset flashVars="#flashVars#&ticketID=#ticketID#" /> </cfif> <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"         codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/ swflash.cab#version=7,0,0,0"         width="550" height="400" id="webPortalTicketTestClient"         align="middle"> <param name="allowScriptAccess" value="sameDomain" /> <param name="movie" value="webPortalTicketTestClient.swf" /> <param name="quality" value="high" /> <cfoutput><param name="FLASHVARS" value="#flashVars#" /></cfoutput> <cfoutput> <embed src="webPortalTicketTestClient.swf"        quality="high"        width="550" height="400"        name="webPortalTicketTestClient" align="middle"        allowScriptAccess="sameDomain"        type="application/x-shockwave-flash"        pluginspage="http://www.macromedia.com/go/getflashplayer"        FLASHVARS="#flashVars#" /> </cfoutput> </object> </body> </html> 

The page uses a SELECT statement to retrieve the user's personal information such as first and last name:

 SELECT userName, firstName, lastName, email FROM PFCS_USER WHERE userName = '#GetAuthUser(  )#' 

The ColdFusion function GetAuthUser( ) returns the username of the person logged in to the ColdFusion session.

The script creates a ticketID and ticket and inserts a row into the PFCS_TICKET table using identical code to Example 18-2. Then, the information is assembled into a URL-encoded query string named FLASHVARS , which is used in the <param> and <embed> tags so that the data will be loaded into the Flash movie. For example, here is the <param> tag:

 <cfoutput><param name="FLASHVARS" value="#flashVars#" /></cfoutput> <cfoutput> 

Since the CFML page provides the Flash movie all the information it needs to connect and authenticate to FlashCom, the page does not have to show the user a login screen. Instead, it simply connects. Example 18-12 shows a short client-side ActionScript excerpt that uses the ticket and ticketID timeline variables created by the FLASHVARS parameter to connect to FlashCom:

Example 18-12. Connecting to FlashCom using a ticket and ticketID supplied by a FlashVars parameter
 import mx.controls.*; // Create and initialize the NetConnection object. nc = new NetConnection( ); nc.pendingConnection = false; nc.onStatus = function (info) {   switch (info.code) {     case "NetConnection.Connect.Success":       _root.gotoAndPlay("Main");       break;     case "NetConnection.Connect.Rejected":       showConnectionError("Invalid Credentials.", info.code);       break;     case "NetConnection.Connect.Closed":       if (this.pendingConnection)         showConnectionError("Invalid Credentials.", info.code);       break;     case "NetConnection.Connect.Failed":       showConnectionError("Unable to reach communication server.", info.code);       break;   } }; nc.connect("rtmp:/ch18Samples/ticketAuthDemo", ticketID, ticket); function showConnectionError (message, title) {    Alert.show(message, title);    nc.pendingConnection = false;    _root.gotoAndPlay("Error"); } 

In Example 18-12, the .swf connects to the same FlashCom application as Example 18-1 did. The application calls the same method via Flash Remoting to check the ticket and accept the client's connection. See Example 18-2 and Example 18-3.

18.2.4.2 Flash Remoting with FlashCom logins

Sometimes a Flash movie must log in directly to an application server using Flash Remoting and also connect to FlashCom. In that case, an approach similar to Example 18-1 through Example 18-3 can be used to generate a ticket. The services should be available only to clients that have logged into the Remoting application. Once a Flash movie has logged in, it should be able to call a service's getTicket( ) function without having to send another username and password. The getTicket( ) method should be able to get the client's username from the application server without the client having to send it again. If ColdFusion is being used, GetAuthUser( ) can be used to get the user's username. An example of protecting ColdFusion components with an Application.cfm file and logging clients into an application is provided under "Role-Based Authorization" later in this chapter.

18.2.5. Checking Connection Information

When a client attempts to connect to FlashCom, information about the client is made available via properties of the client object. The most important of these are the IP address of the client, the URL from which the .swf was loaded, and the agent or name of the Player or software trying to connect. For example, when a Flash movie is loaded into the Flash Player in my machine from:

http://flash-communications.net/technotes/clientInformation/index.html

the following properties of the client object are displayed:

 ip: 64.231.161.59 agent: WIN 7,0,14,0 referrer: http://flash-communications.net/technotes/clientInformation/    clientInformation.swf protocol: rtmp 

The IP address is the address my ISP provided for my machine today, the agent indicates the version of the Flash Player and platform it is running on, and the referrer shows the location from which the .swf was loaded into the Flash Player. If another FlashCom application connects to the application, it might receive the following information:

 ip: 64.231.161.59 agent: FlashCom/1.5.2 referrer: rtmp://_defaultVHost_:1935/getClientInfo/_definst_ protocol: rtmp 

In this case, the agent reflects the fact that a FlashCom Server, not the Flash Player, is connecting. The referrer is the name of the FlashCom application and instance running in the default virtual host (Vhost) on my machine. While it is possible for an attacker to spoof information sent to FlashCom, it is important to check the information anyway. There are two ways to check the information. One is to use FlashCom's configuration files to define access rules, and the other is to write code to check the client's connection information in each application. In general, you should use the server's .xml files to define adaptor- and Vhost-level rules wherever you can and only do checks that are required by applications in your scripts. The blanket protection provided by configuring the server is better than relying on each script to control access.

The <Allow> , <Deny> , and <Order> tags in the Adaptor.xml file can be used to control access to an adaptor by full or partial IP address or domain name. (They have no direct relationship to the <Allow> , <Deny> , and <Order> tags in the Server.xml file discussed in Chapter 10. Those are used to limit access to the Admin Service application only.) The <Allow> tag (and under some circumstances the <DNSSuffix> tag) in a Vhost.xml file can perform referrer checks based on allowed domain names. Information on configuring the server using these XML tags is available in the Managing Flash Communication Server document available from Macromedia at:

http://www.macromedia.com/support/flashcom/documentation.html

An introduction to using the tags with some examples is also available on Macromedia's site:

http://www.macromedia.com/devnet/mx/flashcom/articles/firewalls_proxy06.html

Unfortunately , not every organization that offers a FlashCom hosting service will custom configure the Adaptor.xml and Vhost.xml files for you or allow you to configure them yourself. In that case, you must write code to check the client connection information yourself.

18.2.5.1 IP checks

IP checks are not difficult to code. IP addresses are returned as dot-delimited strings. They can be split and joined as needed, or string searches or pattern matching can be performed on them. For example, if you want to allow any client to connect from within Ryerson University's class B address, you need to make sure that every client's address begins with 141.117. You can check with a simple string search:

 if (client.ip.indexOf("141.117.") != 0) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

If you have to check only one address this way, using a string search works well. However, if you need to check multiple addresses, consider using an object as an associative array:

 validIPs = {}; validIPs["141.117.35.14"] = true; validIPs["141.117.35.15"] = true; validIPs["141.117.35.16"] = true; 

To check a client when it tries to connect, look up its IP address in the object:

 if (!validIPs[client.ip]) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

To check based on a partial IP address is not much more difficult:

 validIPs = {}; validIPs["141.117.35"] = true; validIPs["141.117.27"] = true; validIPs["141.117.72"] = true; 

You can extract a partial address using the substring( ) and indexOf( ) methods:

 var partialIP = client.ip.substring(0,client.ip.lastIndexOf(".")); if (!validIPs[partialIP]) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

Or you can use the split( ) and join( ) methods:

 var partialIP = client.ip.split("."); // Split the string into an array. partialIP.length = 3;                 // Keep the first three elements. partialIP = partialIP.join(".");      // Reassemble the string into a partial IP. 

If the IP addresses are of varying lengths, a little more work is required:

 validIPs = {}; validIPs["141.117"] = true; validIPs["209.29.148"] = true; validIPs["192.168.0.23"] = true; 

The full IP address, as well as partial versions of it, has to be tested . For example, you can test it by looking up the IP and, if it does not match, removing a token and trying again until there are no tokens left in the IP:

 var ip = client.ip var validAddress = false; do {   if (validIPs[ip]) {     validAddress = true;     break;   }   ip = ip.substring(0, ip.lastIndexOf(".")); } while(ip.length > 0); if (!validAddress) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

One thing you cannot code in Server-Side ActionScript that the server's configuration tags can do is a reverse lookup to try to match an IP address against an allowed domain name. However, you could make a reverse lookup available via an application server and Flash Remoting.

Checking the client's IP address is especially valuable when you need to distinguish between Flash clients and other application instances connecting to the same instance. Normally, you will have a list of FlashCom Server addresses:

 fcsIPs = {}; fcsIPs["141.117.101.222"] = true; fcsIPs["141.117.27.180"] = true; 

that are easy to check:

 if (fcsIPs[client.ip]) {   return(flashcomAuth.authenticate(client, credentials)); } else {   return(clientAuth.authenticate(client, credentials)); } 

18.2.5.2 Referrer checks

Imagine the scenario in which someone decompiles one of your .swf files. In the code, he finds the RTMP address of an application on your server. Rather than paying for his own server and bandwidth, he writes his own Flash movie that connects to your application in order to create his own chat or video streaming application. Then he distributes his movie on the Internet. As this has actually happened , you should be asking yourself what would stop someone from using your application as his own. The first and best protection is to force users to log in, whenever possible. But not all applicationssuch as streaming on-demandshould require logins.

Another way to stop someone from using your application is to check the referring address the Flash Player sends FlashCom when it connects. This assures that the .swf file was served from a known location, such as your server and not someone else's.

The best and safest place to do referrer checking is using the <Allow> tag in each Vhost.xml file. However, you can script referrer checks as easily as IP checks. For example, to check against a list of valid .swf locations, build a list of URLs:

 referrerList = {}; referrerList["http://flash-communications.net/technotes/test.swf"] = true; referrerList["http://echo.ryerson.ca/textPublisher/main.swf"] = true; referrerList["http://echo.ryerson.ca/onDemand/streamPlayer.swf"] = true; 

Then check each client's referrer property:

 if (!referrerList[client.referrer]) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

If the referrerList contains URLs for the hosts, you can use split( ) and join( ) again:

 referrerList = {}; referrerList["http://flash-communications.net"] = true; referrerList["http://www.ryerson.ca"] = true; referrerList["http://echo.ryerson.ca"] = true; 

For example, in an onConnect( ) method:

 var partialReferrer = client.referrer.split("/"); partialReferrer.length = 3; partialReferrer = partialReferrer.join("/"); if (!referrerList[partialReferrer]) {   application.rejectConnection(client, {msg:"Access Denied."});   return false; } 

If necessary, you can also extract the hostname from the referrer property:

 var hostName = client.referrer.split("/")[2]; 

While it is possible to write a program that will send false referrer information to a server, it is extremely difficult or impossible to distribute a Flash movie that will trick a Flash Player into sending false referrer information. (It is possible, of course, to create a custom Player, as the .swf format is publicly available, but the attacker would need to get his Player distributed to potential users.) So, while you should never assume you are receiving correct referrer information, you should implement referrer checking to reduce the chances that someone is going to hijack your server and bandwidth. The "Role-Based Authorization" section later in this chapter includes other things you can do to prevent your server and bandwidth from being hijacked by someone else's applications.

Another problem that has been reported is a site including a .swf or framing a page containing a .swf that belongs to another site without permission. For example, a "parasite" might claim to offer free chat without investing in a FlashCom Server. Such a site would simply include a .swf belonging to someone else in a web page using a full URL:

 <param name="movie" value="http://host.anotherDomain.com/freeChat.swf" /> 

A referrer check within FlashCom will not catch the problem because the .swf file is being served from the correct server. One strategy for discouraging parasites is to have the web server do a referrer check on the page loading the .swf file. If it is not an HTML page from the correct domain, the request is refused. See, for example, Fernando Fl ³rez's post that describes using an Apache .htaccess file to protect a .swf with a URL rewrite rule:

http://blog.function.com/en/archives/000209.php

If your HTML page containing the .swf is framed, a web server referrer check on the .swf will not help. JavaScript can be used to detect framing and to escape from it in some cases. However, there is no foolproof way to stop someone from including content from another site. Non-technical means such as complaining to a hosting service or litigation may be necessary. However, when appropriate, the entire problem can be avoided by requiring each user to authenticate before accessing content.

For other suggestions on avoiding misappropriation of .swf content, see the chapter on security in Flash Hacks (O'Reilly).

18.2.5.3 Agent checks

Another, more common way to distinguish between an application instance and client connecting to an application is to check the client.agent property. For example:

 if (client.agent.indexOf("FlashCom") == 0) {   return(flashcomAuth.authenticate(client, credentials)); } else {   return(clientAuth.authenticate(client, credentials)); } 

Each authentication object should also check the IP address and other information to make sure another application instance, rather than an attacker sending false user agent information, is really connecting.

18.2.6. Some Warnings and Caveats

Even though the front matter of this book provides the usual warnings that exclude the publisher and author from liability, I want to provide an extra note of caution here.

The code in this chapter has been tested, and much of it has been used in production environments. However, you should be very cautious before deploying the code shown here in a live environment.


The code, some of which is intended for demonstration, may have undiscovered bugs (check the book's errata page for the latest corrections). Or you may introduce bugs or other security holes in adapting it to your environment. It is also possible that your web server, application server, or FlashCom Server itself has hidden security flaws. Before going into production, you must test and retest your system for security problems.

18.2.6.1 Flash Remoting

As of FlashCom 1.5.2, the only way for FlashCom to communicate directly with an application server is via a Flash Remoting gateway. However, Macromedia's gateway cannot control what web services a client can connect to using the gateway. It is true that you can protect ColdFusion CFC files using an Application.cfm file, but the gateway itself does not provide any security controls. Fortunately, Alon Salant of Carbon Five created a servlet filter called FlashGatekeeper. Information on FlashGatekeeper is available from:

http://carbonfive. sourceforge .net/flashgatekeeper
18.2.6.2 Anonymous connections

From a security perspective, allowing anonymous connections is dangerous. You have no idea who is connecting, and the people who connect without logging in know this. Worse, some users assume because they have not logged in to an application that they can do anything they want without fear of being identified or held responsible. And, unfortunately, they are often right. Some anonymous users are also capable of so poisoning conversations in lobbies and chat sessions that most users will simply leave and never return. In fact, applications in which users are allowed to interact or send information to the server should not allow anonymous connections. In other words, anonymous users should be quarantined.



Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net