Friday, October 2, 2009

Disabling standard Authenticator and creating new CMS entry point

This post outlines removing visibility of the standard email/password authentication in favour of your own front end Authenticator, but still having an access point to the standard Authenticator to authenticate yourself before redirecting to the CMS.

1. Disable MemberAuthenticator, so only customer login shows on normal /Security/login requests

_config.php:

Authenticator::register_authenticator('CustomerAuthenticator'); 
Authenticator::set_default_authenticator('CustomerAuthenticator'); 
Authenticator::unregister_authenticator('MemberAuthenticator');

2. Create rule to have a new CMS entry point (in this case "/manager" ), in reality this is just a pointer to the standard MemberAuthenticator, now disabled for all standard security login requests.

_config.php:

Director::addRules(100, array('manager' => 'AdminLogin_Controller',));

3. Create a new controller to handle this entry point, ensure any bad logins get redirected back to it.

class AdminLogin_Controller extends Page_Controller { 

  function init() { 
    Session::set('BadLoginURL', '/manager');
    Session::set('BackURL', '/admin');
    parent::init(); 
  } 

  function Form() { 
    return new MemberLoginForm($this, "LoginForm"); 
  } 

  function Title() { 
    return "Admin Log in"; 
  } 
}

Wednesday, September 30, 2009

Customize your own SilverStripe Login Form and Authenticator

This is how I created a new Authenticator and associated Login Form. In this I was using another database table to use as the identifier.

First, create your new Login Form (MyLoginForm.php). This controls input fields redirects, etc.

<?php

class MyLoginForm extends LoginForm {

 protected $authenticator_class = 'MyAuthenticator';

 //we add fields in the constructor so the form is generated when instantiated
 function __construct($controller, $name, $fields = null, $actions = null, $checkCurrentUser = true) {
  //create your authenticator input here, e.g. username, but it could be any credentials
  //add your Authenticator to the form
  $fields = new FieldSet(
   new TextField('UserName', 'User Name'),
   new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this),
   new CheckboxField("Remember", "Remember me next time?")
  );

  $actions = new FieldSet(
   new FormAction('dologin', 'Log in')
  );

  //LoginForm does its magic
  parent::__construct($controller, $name, $fields, $actions);
 }


 // this function controls the redirect based on success/failure
 public function dologin($data) {
  if ($this->performLogin($data)) {
   Director::redirect(Director::baseURL());
  } else {
   Director::redirectBack();
  }
 }


 //call our own Authenticator
 public function performLogin($data) {
  if ($member = MyAuthenticator::authenticate($data, $this)) {
   $member->LogIn(isset($data['Remember']));
   return $member;
  } else {
   return null;
  }
 }

}

?>
 
Now, create your new Authenticator that calls the custom login form you created, takes the input and authenticates.

<?php
class MyAuthenticator extends Authenticator {

 //authenticate is called by the login form you created
 public static function authenticate($RAW_data, Form $form = null) {
  $SQL_user = Convert::raw2sql($RAW_data['UserName']);
  $member = DataObject::get_one("MyMember", "UserName = '$SQL_user'");
  if ($member) {
   return $member;
  } else {
   if ($form) $form->sessionMessage("That doesn't seem to be the right user name. Please try again.", "bad");
   return false;
  }
 }

 //Tell this Authenticator to use your custom login form
 //The 3rd parameter MUST be 'LoginForm' to fit within the authentication framework
 public static function get_login_form(Controller $controller) {
  return Object::create("MyLoginForm", $controller, "LoginForm");
 }

 //give a title to the MyAuthenticator tab (when multiple Authenticators are registered)
 public static function get_name() {
  return "User Name";
 }

}

?>

Lastly, register your new Authenticator in mysite/_config.php:

Authenticator::register_authenticator('MyAuthenticator');
(optional) Authenticator::set_default_authenticator('MyAuthenticator');
(optional) Authenticator::unregister_authenticator('MemberAuthenticator');

If you want to use your own form template, add it to /themes/mysite/templates/includes/MyLoginForm.ss

Notes:

  • This authenticator simply checks a fictional class 'MyMember' against the input username and passes if such a member is found. Obviously this is insecure in most cases. This is where you fill out the authenticate() method to authenticate securely using whatever credentials you receive from your custom login form.
  • More complicated functionality like redirecting to previous pages, redirecting based on groups, tracking login attempts, locking out users, adding non-existent users, etc are not handled. For inspiration on this look to the MemberAuthenticator and MemberLoginForm classes to see how SilverStripe handles it's own authentication and look into the External Authentication module.
  • I have another post on disabling the standard MemberAuthenticator for front-end users and creating a distinct access point for the CMS, using the standard MemberAuthenticator.

Monday, September 21, 2009

Enabling PHP mail on Snow Leopard using your ISP SMTP Server and verified sender

When trying to get postfix/php to send mail on my development box using my ISP account my emails were getting bounced as spam. This was not fixed by other internet posts on the subject involving editing php.ini and /etc/hostconfig.

Trying to send to my google account:
Sep 22 12:02:07 development postfix/smtp[65590]: E822C13C426: host gmail-smtp-in.l.google.com[209.85.222.93] said: 421-4.7.0 [60.234.235.17] Our system has detected an unusual amount of 421-4.7.0 unsolicited mail originating from your IP address. To protect our 421-4.7.0 users from spam, mail sent from your IP address has been temporarily 421-4.7.0 blocked. Please visit http://www.google.com/mail/help/bulk_mail.html 421 4.7.0 to review our Bulk Email Senders Guidelines. 33si2020982pzk.56 (in reply to end of DATA command)
Trying to send to my hotmail account:
Sep 22 11:46:29 development postfix/smtp[65417]: 9C41113C3E6: to=, relay=mx3.hotmail.com[65.55.37.88]:25, delay=0.97, delays=0.15/0.03/0.6/0.2, dsn=5.0.0, status=bounced (host mx3.hotmail.com[65.55.37.88] said: 550 DY-001 Mail rejected by Windows Live Hotmail for policy reasons. We generally do not accept email from dynamic IP's as they are not typically used to deliver unauthenticated SMTP e-mail to an Internet mail server. http://www.spamhaus.org maintains lists of dynamic and residential IP addresses. If you are not an email/network admin please contact your E-mail/Internet Service Provider for help. Email/network admins, please visit http://postmaster.live.com for email delivery information and support (in reply to MAIL FROM command))

All I needed to do to get postfix sending emails was:
  1. Telnet to my ISP mail server and check it would send email from a trusted source (my ISP account)
  2. Tell postfix to relay through my ISP's mail server:
    • sudo vi /etc/postfix/main.cf
    • relayhost = mail.orcon.net.nz
  3. Create an authentic sender address that postfix would use:
    • your mail log (mail.log) will show a send email address looking something like login@machine.local where login is your username on your computer and machine.local reflects your computer address on your home network. Hotmail, gmail, etc expect a verifiable address not something local like this. So we change it. Copy the local address as we need to create a mapping for it.
    • create a postfix generic mapping
      • sudo vi /etc/postfix/generic
      • login@machine.local myemail@myisp.com, or
      • login@machine.local me@gmail.com
      • I just added this line at the end of the file
    • create the hash db:
      • sudo postmap /etc/postfix/generic
    • add the mapping to postfix:
      • sudo vi /etc/postfix/main.cf
      • smtp_generic_maps = hash:/etc/postfix/generic
      • I put this in the section under # ADDRESS REWRITING comment
    • start/reload postfix:
      • sudo postfix start, or
      • sudo postfix reload (if it's already running)
That was it; mails were now being sent through and accepted by the destination mail server. This requires I send email when connect to the net using my ISP. If I can I'll create another post for using the same ISP mail server but connected using another ISP (for all you café coders out there).

After that the following php tested fine:

<?php

$name = "Test Mailer"; //senders name
$email = "anyone@somedomain.com"; //senders e-mail adress
$recipient = "testinbox@gmail.com"; //recipient
$mail_body = "PHP test body"; //mail body
$subject = "PHP test mail"; //subject
$header = "From: ". $name . " <" . $email . ">\r\n"; //optional headerfields

mail($recipient, $subject, $mail_body, $header); //mail command

?>

Saturday, September 19, 2009

Put SilverStripe Menus into Groups

For splitting up top level menus...


class Page extends SiteTree {

public static $db = array(
'MenuGroup' => "Enum('First,Second,Third','First')"
);

function getCMSFields() {
$fields = parent::getCMSFields();
$menuField = new DropdownField(
'MenuGroup',
'MenuGroup',
singleton('Page')->dbObject('MenuGroup')->enumValues()
);
$fields->addFieldToTab("Root.Behaviour", $menuField);
return $fields;
}

...

}

class Page_Controller extends ContentController {

function menuItems($menuGroup) {
$whereStatement = "ParentID=0 AND ShowInMenus = 1 AND MenuGroup = '$menuGroup'";
return DataObject::get("Page", $whereStatement);
}

...

}


And in your template:


<% control menuItems(First) %>
$MenuTitle
<% end_control %>


Check to see if existing Pages get the default menu group set. New pages will have the default 'First'. Don't forget to rebuild!

Credit: