Posts Tagged ‘ smalltalk

Patching Seaside for deployment

I’m currently in the process of deploying a Seaside application on my server at the University, and I noticed something that I needed to fix before I went any further. By default, the server adapters that ship with Seaside will listen on all interfaces for connections. Since I plan on proxying my Pharo image behind an Apache server, with Apache doing all of the authentication, I did not want my Seaside server to be accessible to outside clients on the port its listening on. This would allow them to bypass my authentication mechanisms I have in place.

I dug through the Seaside code a bit and found what I needed to patch. Note here, I only am patching the WAListenerAdaptor since that is the adaptor which supports Comet (which my application makes heavy use of).

Here is the original code:

WAListenerAdaptor>>listenLoop
	| socket |
	socket := Socket newTCP.
	socket 
		listenOn: port
		backlogSize: 50.
	socket isValid ifFalse: [ self error: 'Cannot listen on port ' , port greaseString ].
	
	[ 
	[ socket isValid ifFalse: [ ^ self listenLoop ].
	self waitForConnection: socket ] repeat ] ifCurtailed: 
		[ (Delay forMilliseconds: 10) wait.
		socket destroy ]

And the patched code:

WAListenerAdaptor>>listenLoop
	| socket |
	socket := Socket newTCP.
	socket 
		listenOn: port
		backlogSize: 50
		interface: NetNameResolver loopBackAddress.
	socket isValid ifFalse: [ self error: 'Cannot listen on port ' , port greaseString ].
	
	[ 
	[ socket isValid ifFalse: [ ^ self listenLoop ].
	self waitForConnection: socket ] repeat ] ifCurtailed: 
		[ (Delay forMilliseconds: 10) wait.
		socket destroy ]

Notice how I changed the socket initialization message to include the interface keyword, and supplied it with the loop back address. Now my application is only accessible via 127.0.0.1 or localhost, and not via its external IP.

The alternative would have been to leave the code unpatched and instead write some iptables firewall rules to block on the port the Seaside adaptor is listening on, but this seemed like a simpler solution and allows me to leave the rest of my system untouched. Also, this solution is the only possible way to do it if you do not have root access to the machine to add iptables rules.

Apache-based authentication for Seaside

I recently posted about a completely Smalltalk-based solution for LDAP authentication for Seaside applications. After I got that working for my application, I realized that once I submitted my application to our security group here on campus for review, it would never pass inspection due to the fact that user passwords are accessible to my code. I can imagine many scenarios where programmer access to user credentials is unacceptable.

Fortunately, our university provides its own kerberos-based authentication solution for applications called Bluestem. It provides the user first with a screen to enter their username, then a second screen to enter their university kerberos password. Upon successful authentication, the user is given access to the proper resources on the server. Bluestem is written in Perl and there exists an Apache module to integrate it with web applications. I previously set up the server I plan to deploy my Seaside application on for Bluestem authentication for other web applications that I host on it, but I did not know of a way that I could use this type of authentication with Seaside since I was planning on using mod_proxy to proxy requests from my external-facing Apache server to my headless Pharo VM.

While I was researching how to do this, I came across an Apache module that someone has posted on Github at https://github.com/aimxhaisse/mod-proxy-add-user which takes the authenticated user name set by Apache and forwards it on to servers being proxied. I was able to compile it (manually, the Makefile appeared to be broken) and get it up and running on my CentOS server within a few minutes. I configured my Apache configuration file with:

<Proxy *>
    Order deny,allow
    Allow from all
    ProxyAddUser On
    ProxyAddUserKey "HTTP_X_FORWARDED_USER"
</Proxy>

And low and behold, I was able to access the authenticated user name in my proxied Seaside application with:

username := self class decodeUserIdFrom: (self requestContext request headerAt: 'http_x_forwarded_user' ifAbsent: [username := 'cemeyer2']).

Let me explain a little bit. In most cases, just self requestContext request headerAt: ‘http_x_forwarded_user’ will give you the authenticated user name, but our university’s kerberos solution sets this variable to ‘username@uiuc.edu/kerberos’, so the decodeUserIdFrom message strips off everything but the user name. The above line also is useful for when accessing the application when not being proxied behind university authentication (such as during development), as it will default to return my user name if the header is not found.

Integrating LDAP Authentication with Seaside

For one of my projects at school, I am building an application using the Seaside web development framework. The project will eventually become a piece of courseware for a class I teach, and as such has requirements for authentication. I wanted to use our university’s authentication system to accomplish this. It took a fair amount of digging through the Seaside source code, and following the dead ends that the documentation provided (for example, WAAuthMain doesn’t even exist).

I decided to use the smalltalk LDAPLayer package to allow me to do what I needed to do. First order of business was to set up a way to do encrypted connections to the LDAP server. The LDAPLayer package does not support SSL connections out of the box (as far as i could tell), so rather than try to modify the source to allow it, I decided to run a local stunnel instance on my machine to provide a SSL proxy to the directory:


sudo stunnel -c -d 389 -r ad.uiuc.edu:636

I then can connect to localhost on the regular LDAP port, 389, and it will be securely tunneled to our campus’ Active Directory cluster.

The next step was to write an authenticator that would do the actual authentication. Mine turned out to look something like:

LDAPAuthenticator>>#verifyPassword: aPassword forUser: aUsername
	| dn req tempConn |
	aPassword ifNil: [^false].
	aPassword ifNil: [^false].
	dn := self getDNForUsername: aUsername .
	tempConn := LDAPConnection
		to: self class hostname
		port: self class port.
	req := tempConn
		bindAs: dn
		credentials: aPassword.
	[req wait] on: LDAPException do: [ :exception | tempConn disconnect. ^false ].
	tempConn disconnect.
	^true

I also implemented LDAPAuthenticator>>#getDNForUsername which converts a username into a fully qualified distinguished name for binding to the directory with. In addition, LDAPAuthenticator class>>#hostname and LDAPAuthenticator class>>#port return localhost and 139 respectively. It is important to note that the message implemented has to exactly match #verifyPassword:forUsername: and the message also needs to respond with false if either are nil (it was a bug that threw me off for a bit). Then its simply a matter of tying it all together in the initialize method of the application’s root component:

initialize
	"self initialize"
	| app authFilter |
	app := (WAAdmin register: self asApplicationAt: 'mypath')
		addLibrary: PTDeploymentLibrary;
		addLibrary: SUDeploymentLibrary;
		addLibrary: CTLibrary;
		yourself.
	app preferenceAt: #sessionClass put: MyCustomSession.
	authFilter := WAAuthenticationFilter new.
	authFilter realm: 'Active Directory. Use your username and Active Directory Password'.
	authFilter authenticator: LDAPAuthenticator new.
	app addFilter: authFilter .

Once that is complete, authentication works like a charm. Of course I could modify the authenticator to only allow members of certain groups, etc, but for my needs, letting anyone with valid credentials is quite alright. I also wanted to tie a user object representing the logged in user to my custom session, so I did the following to get the username of the authenticated user in my overridden #initialrequest in my root component:

username := self requestContext request user.

That took care of the whole mess for me…