TF-Login

Description

Port of http://www.smalltalkhub.com/#!/~sergio101/TF-Login/

Details

Source
GitHub
Dialect
pharo (40% confidence)
License
MIT
Stars
2
Forks
1
Created
June 12, 2018
Updated
Dec. 11, 2019
Topics
authentication pharo seaside smalltalk smalltalkhub-port

Categories

Web

README excerpt

Originally from http://www.squeaksource.com/TFLogin.html, but tests were failing. It turns out that this was written for pharo 1.1.1. I wanted to use pharo 2, so I refactored the parts that didn't work in pharo 2. The following are the changes i made:

- ReadStream was updated to use Fuel.
- The tests now only support ZnClient and ZnServer.

All tests now pass, except for testCapacity test. I will look at this in the next week, but I thought it would be worthwhile to get it out.

This package provides basic user authentication, registration, and account management for Seaside.

Features include:

- User login component with username and password text fields, forgot username, forgot password, register, and login buttons.
- User registration component with optional BowWave reCaptcha spambot protection.
- Edit account settings component allows user to change username, password, email address and application-specific properties.
- Optional email confirmation for registration and account changes. Email content is provided by the host application. Examples are included.
- Multi-part HTML/plain text email.
- Configurable confirmation email timeouts and cookie retention periods.
- Forgot username and forgot password support.
- New password validation by the host application (to enforce password rules like minimum-length, etc.)
- Options to allow empty passwords, remember username in cookie, and automatic login on return to the website.
- Host application can provide login filter block for such things as disabling accounts or limiting login frequency.
- Host application can use TFLogin's email confirmation mechanism for application-specific purposes. Additional user-related objects may be included in the account edit form.
- Persistence is provided by a storage adaptor that can be replaced with the persistence mechanism of your choice. The provided default file storage adaptor scales to 100,000 users. Each user's information is saved in a separate versioned file.
- An application properties dictionary is provided that allows you to store and retrieve additional objects along with the user account information.
- All settings are available in the Seaside configuration page. Test/demo application included.
- TFLogin Guide document included (see below).
- Tested with Seaside 3.0.3 and Pharo 1.1.1. Email does NOT work with prior versions of Seaside.

All feedback is welcome.

# TFLogin Guide

Note that the tests included in the TFLogin package require the following packages to run:

    http://www.squeaksource.com/Soup
        Soup

    http://www.squeaksource.com/ZincHTTPComponent
        Zinc-HTTP

## Integrating TFLogin with Your Application

Take a look at the TLTestApp to see an example of the steps below.

There are a number of steps to introducing TFLogin authentication to an existing Seaside application. They are described below:

1. Create `MyCoolComponent class>>#initialize` like the following:
```smalltalk
initialize
	
	| application |
	application := WAAdmin register: self asApplicationAt: self handlerName.
	application preferenceAt: #sessionClass put: TLSession.

	"Add TL-specific configuration options"
	application configuration
		addParent: TLConfiguration instance;
		addParent: WAEmailConfiguration instance
```
2. If you plan to use reCaptcha to protect your registration form from spammers, then you will need the BowWave reCaptcha package that you can obtain by evaluating:
```smalltalk
    Gofer it
        url: 'http://www.squeaksource.com/BowWave';
            package: 'BowWave-Captcha-Core';
            package: 'BowWave-Captcha-Recaptcha';
            load.
```
Then add lines like the following to `MyCoolComponent class>>#initialize`, substituting your public and private reCaptcha keys that you obtain free from http://www.google.com/recaptcha.
```smalltalk	
    application configuration parents add: BWRecaptchaConfiguration instance.
    application preferenceAt: #publicKey put: '{{your-public-key}}'.
    application preferenceAt: #privateKey put: '{{your-private-key}}'.
```
Note that these preferences can also be set using your application's Seaside configuration page.

If you do not plan to use reCaptcha spambot protection, then it is not necessary to install the BowWave package.

3. You can also set TFLogin preferences in `MyCoolComponent class>>#initialize` if you wish, or you can set the using the Seaside application config page.  Here is an example of what can be done in the initialize method:
```smalltalk
	application preferenceAt: #sendRegistrationConfirmationEmail put: true.
	application preferenceAt: #confirmationTimeoutMinutes put: 10.
	application preferenceAt: #useRecaptchaInRegistrationForm put: false.	
	application preferenceAt: #smtpServer put: 'localhost'.	
	application preferenceAt: #confirmEmailChangeViaEmail put: false.
	application preferenceAt: #confirmAccountChangesViaEmail put: false.
	application preferenceAt: #allowEmptyPasswords put: false.
	application preferenceAt: #allowUsernameChange put: false.
	application preferenceAt: #allowRememberUsername put: true.
	application preferenceAt: #allowAutomaticLogin put: false.
```	
Note that these preferences can also be set using your application's Seaside configuration page.

4. In your component's instance-side `#initialize` method (not the class initialize method as described above) put the following to make the TFLogin components known to your application:
```smalltalk
initialize

	super initialize.
	loginComponent := (TLLoginComponent appName: '<your-app-name>').
	loginComponent onAnswer: [ :user | user ifNotNilDo: [ :u | self loggedIn: u ] ].
	self children add: loginComponent.
	editAccountComponent := (TLEditAccountComponent loginComponent: loginComponent).
	editAccountComponent onAnswer: [ self editingAccount: false ].
	self children add: editAccountComponent.
```
If you don't already have one, you will need a children method that looks like this:
```smalltalk
children
	^ children ifNil: [ children := Bag new ]
```	

5. If you will be using email confirmation, then you will need to tell TFLogin where your email sending methods are like this (also in the application initialize method). An example of the email sending methods referenced here is presented later on in this guide.
```smalltalk
	loginComponent registrationConfirmationEmailSender: [ :url :email :timeout | 
		self
			sendRegistrationConfirmationEmailTo: email
			confirmUrl: url
			timeoutMinutes: timeout  ].
	loginComponent passwordResetEmailSender: [ :url :email :timeout |
		self
			sendPasswordResetUrl: url
			to: email
			timeout: timeout ].
	loginComponent usernameReminderEmailSender: [ :usernames :email | 
		self
			sendUsernameReminderFor: usernames
			to: email ].
	editAccountComponent emailChangeConfirmationEmailSender: [ :url :email :timeout :newUser |
		self
			sendEmailChangeConfirmationTo: email
			confirmUrl: url
			timeout: timeout
			newUser: newUser  ].
	editAccountComponent accountChangeConfirmationEmailSender: [ :url :email :timeout :newUser |
		self
			sendAccountChangeConfirmationTo: email
			confirmUrl: url
			timeout: timeout
			newUser: newUser ].
```

6. If you want to support automatic login using username/password cookies for your users, you will need to include the following in your application's initialRequest method: 
```smalltalk
	loginComponent cookieLogin
```	
This will log the user in immediately before your application has had a change to display the loginComponent if they have the correct cookies defined (as would be the case if they checked the "Log me in automatically when I return to this site" checkbox when they last logged in.)

7. Here is an example of a mail-sending method. The text would of course vary depending on your application and the specific message being sent.
```smalltalk
sendRegistrationConfirmationEmailTo: email confirmUrl: url timeoutMinutes: timo
	"Compose and send email. Answer true on success, false on failure."

	| textBody htmlBody emailOk appName |

	appName := 'Login Test App'.
	
	"Compose a pain text message."
	textBody := (WriteStream on: String new)
		<< 'This email is in response to your request to register at '; << appName; << '.'; crlf;crlf;
		<< 'Direct your browser to the following URL within ';<< timo asString; << ' minutes to confirm your registration.'; crlf;crlf;
		<< '         '; << url; crlf;crlf;
		<< 'If you did not attempt to register for a'; << appName; << ' account then this message was sent in error and should be ignored.'.	

	"Compose a nice HTML message."
	htmlBody := WAHtmlCanvas builder fullDocument: true; render: [ :html |
		html div
			style: 'font-size:11pt;';
			with: [
				html div
					style: 'margin-bottom: 10px;';
					with: 'This email is in response to your request to register at ', appName, '.'.
				html text: 'Click on the link below within ', timo asString, ' minutes to confirm your registration.'.
				html div
					style: 'margin-left:20pt;margin-top:10px;margin-bottom:10px;';
					with: [
						html anchor
							url: url;
							with: 'Confirm registration'].
		 		html text: 'If the link above is unresponsive, copy and paste the URL below into your browser''s address field to confirm your registration.'.
				html div
					style: 'margin-left:20pt;margin-top:10px;margin-bottom:10px;';
					with: url.
				html text: 'If you did not attempt to register for a', appName, ' account then this message was sent in error and should be ignored.']].

	"Send the message."
	(emailOk := self sendEmailTo: email subject:  appName, ' Registration - action required' text: textBody html: htmlBody)
		ifFalse: [ Transcript cr; show: url ].

	^ emailOk

sendEmailTo: toAddress subject: subj text: textBody html: htmlBody
	"Send multi-part MIME email message."
	
	| sem em|
	em := TLMailMessage empty.
	em addAlternativePart: textBody contents contentType: 'text/plain'.
	em addAlternativePart: htmlBody contents contentType: 'text/html'.
	sem := em
		seasideMailMessageFrom: 'Registrar@' , self ema
← Back to results