HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Using external authentication (Facebook/Twitter/Google) with Approval or Invitation registration

I am creating a new forum for members of an organization. I want to use external authentication plugins (Facebook, Twitter, Google, etc) to allow users to connect to my private community forum. I want to use the Approval registration method so that I can ensure that I only grant access to known members of the organization. However, I found that users who registered via the plugins all bypassed the approval process and became full members immediately upon registering. I have come up with some hacks that make it work the way I want, so I want to share those with the community.

The problem lies in /applications/dashboard/controllers/class.entrycontroller.php. In function Connect (starting on line 280) which is called by these plugins to create the user, UserModel->InsertForBasic is always used to create the user (on line 425), regardless of the configured registration method (Basic, Approval, Invitation, Connect). This causes approvals and invitation checks to be bypassed even if those registration methods are chosen because those are handled in the InsertForApproval and InsertForInvite methods, not InsertForBasic.

I noticed that there is a Register method in UserModel which calls the appropriate InsertForBasic/Approval/Invite method based on the configured registration method. UserModel->Register is called by the various RegisterBasic/Approval/Invitation methods of the Entry controller when a user signs up using Password authentication, and the UserModel->InsertFor_____ methods are never called directly from anywhere else in the controller besides our culprit. Therefore, it made sense to me that replacing the InsertForBasic call with a Register call (they take the same parameters, and Register eventually calls InsertForBasic if appropriate) would do the trick.

It did...sort of. As I had hoped, this change caused it to flow through the logic that set the user as an applicant for me to approve or decline. The problem, however, was in what happened next. When a user applies for an account using Password authentication, they land on /entry/register with the "Your application will be reviewed by an administrator" message from the RegisterThanks view. In contrast, when a user applies using external registration, the popup window closes and the user lands on the default page without seeing the message about their application being reviewed. They are also sent an email telling them they have successfully connected to the forum, but this is premature since they have only applied. So the next challenge was to find a way to display the appropriate message and send the appropriate email once the application is submitted.

Since I knew the process (including the proper confirmation message and email) worked correctly for Password registrations, I decided to try to make use of that existing process rather than copy the missing parts to the Connect method. My premise was that once you have collected the necessary information about the user (whether through the application form or from the external authentication source), the process of creating the registration should be pretty much the same regardless of the authentication method. So I decided that instead of calling UserModel->Register and then writing code to handle the confirmation differently based on the registration method, I would try calling the Register method of the Entry controller. It's the method invoked when the form is submitted for a Password registration, so I figured I could load the Form object with all of the necessary values and then pass control to this method to let it take it from there. I replaced lines 417-449 of /applications/dashboard/controllers/class.entrycontroller.php with the following.

$this->Form->SetFormValue('Password', RandomString(50)); // some password is required
$this->Form->SetFormValue('PasswordMatch', $this->Form->GetFormValue('Password'));
$this->Form->SetFormValue('TermsOfService', '1'); // required but not on connect forms
$this->Form->SetFormValue('DiscoveryText', 'Connect'); // required but not on connect forms
$this->Form->SetFormValue('HashMethod', 'Random');
$this->Form->SetFormValue('Source', $this->Form->GetFormValue('Provider'));
$this->Form->SetFormValue('SourceID', $this->Form->GetFormValue('UniqueID'));
$this->Form->SetFormValue('Attributes', $this->Form->GetFormValue('Attributes', NULL));
$this->Form->SetFormValue('Email', $this->Form->GetFormValue('ConnectEmail', $this->Form->GetFormValue('Email', NULL)));

$this->Register();
return;

A similar change needed to be made a few lines down where the account is created using UserModel->Register after the user is prompted for additional information (like a user name) and posts the form back. I replaced lines 505-519 of class.entrycontroller.php with the following.

$this->Form->SetFormValue('Name', $this->Form->GetFormValue('ConnectName'));
$this->Form->SetFormValue('Password', RandomString(50)); // some password is required
$this->Form->SetFormValue('PasswordMatch', $this->Form->GetFormValue('Password'));
$this->Form->SetFormValue('TermsOfService', '1'); // required but not on connect forms
$this->Form->SetFormValue('DiscoveryText', 'Connect'); // required but not on connect forms
$this->Form->SetFormValue('HashMethod', 'Random');

$this->Register();
return;

This got me closer, but there were still a few problems.

  1. The desired "Your application will be reviewed by an administrator" message was displayed in the popup window, not in the main window as with the Password registration (which doesn't use a popup). The only available next step for the user was to manually close the popup which left the main window on the Sign In form, not indicating that they were now logged in.
  2. The popup also included the message "You need to confirm your email address. Click here to resend the confirmation email." Emails should not need to be confirmed for external authentication, so this was undesired.
  3. When the user logged out and tried to log back in, they were unexpectedly prompted for a password even though they were connected through Facebook.
  4. Creating an account in Invitation registration mode did not work because there was no place to enter the invitation code.

In the following comments, I will explain how I addressed these issues to get external authentication working for Approval and Invitation registration.

(All line numbers I reference are the original lines from version 2.0.18.4, not accounting for my alterations. The three files I changed are included in the attached ZIP file.)

«1

Comments

  • Problem #1 happened because the Register method is designed to work in the main window, not in the popup used by the connect plugins. To address this, I changed the RegistrationXxx methods of the Entry controller to use the _SetRedirect method to close the popup if necessary before redirecting to the landing page. For RegisterBasic, RegisterCaptcha, and RegisterInvitation, I replaced lines 1083-1089, 1045-1051, and 1205-1211 of class.entrycontroller.php with the following.

    $this->_SetRedirect($this->Request->Get('display') == 'popup');
    

    RegisterApproval needed a little more work because after successfully creating the applicant, it simply set the view to RegisterThanks and allowed the page to render without doing any redirect. I changed it to set the redirect target to /entry/register and then perform the redirect (closing the popup if necessary) instead of setting the view. I replaced line 1034 of class.entrycontroller.php with the following.

    // Redirect back to register page to show thanks message
    $this->Form->SetFormValue('Target', '/entry/register');
    $this->_SetRedirect($this->Request->Get('display') == 'popup');
    

    To get it to display the RegisterThanks view after the redirect, I added logic to set the view if the request is not a form post and a valid session exists (indicating that the applicant has already been created and signed in). I replaced line 1039 of class.entrycontroller.php with the following.

    } elseif (Gdn::Session()->IsValid()) {
       // Tell the user their application will be reviewed by an administrator.
       $this->View = "RegisterThanks";
    }
    
  • Problem #2 happened because the original call to UserModel->InsertForBasic in the Connect method I replaced included an override setting NoConfirmEmail to true. The Register method I used in its place has no parameter through which I could supply these settings overrides. So I decided the best solution would be to find a place close to the end of the process and override these options if the form post contained a value for Provider (which would indicate that this user is authenticated externally). The best place I could find is in UserModel->Register just before the InsertForXxx methods are called to update the database. I also found that the CAPTCHA check needed to be skipped when Basic registration is used, so I included that override here as well. I inserted the following after line 701 of /applications/dashboard/models/class.usermodel.php.

    // Do not require email confirmation or CAPTCHA for authentication through external provider
    if ($FormPostValues['Provider']) {
       $Options['NoConfirmEmail'] = TRUE;
       $Options['CheckCaptcha'] = FALSE;
    }
    
  • TNTitan89TNTitan89 New
    edited November 2012

    Problem #3 happened because the code in the Connect method I replaced included the call to UserModel-> SaveAuthentication which saves the association between the user and the external provider. I decided the best place to add that was at the end of the UserModel->Register method after the user is successfully registered. In this place, one call can handle this activity regardless of the configured registration method. I inserted the following after line 720 of class.usermodel.php.

    // save external authentication if provided
    if ($UserID && $FormPostValues['Provider'])
       $this->SaveAuthentication(array(
          'UserID' => $UserID,
          'Provider' => $FormPostValues['Provider'],
          'UniqueID' => $FormPostValues['UniqueID']));
    
  • Problem #4 took a bit more work to solve. With no invitation field on the Connect form, the Register process would raise an error for the missing code and display the form used for the Password authentication method. To keep from getting to that point, I decided to add the invitation field to the Connect form and validate the entry before calling Register to create the user.

    First I copied the form field for the invitation code from the RegisterInvitation view to the Connect view. I inserted the following after line 89 of /applications/dashboard/views/entry/connect.php.

    <li>
       <?php
       if (C('Garden.Registration.Method')=='Invitation') {
          echo $this->Form->Label('Invitation Code', 'InvitationCode');
          echo $this->Form->TextBox('InvitationCode', array('value' => $this->InvitationCode));
       }
       ?>
    </li>
    

    Then I needed to check for a valid invitation code before initiating the transfer of control from Connect to Register in the Entry controller. The code to do this check already existed in the InsertForInvite method of UserModel, so I moved that code to its own function so that I could call it from the Connect method. I inserted the following after line 1406 of class.usermodel.php.

    /**
     * Look for a valid invitation based on code
     */
    public function CheckInvitation($InvitationCode){
       $this->SQL->Select('i.InvitationID, i.InsertUserID, i.Email')
          ->Select('s.Name', '', 'SenderName')
          ->From('Invitation i')
          ->Join('User s', 'i.InsertUserID = s.UserID', 'left')
          ->Where('Code', $InvitationCode)
          ->Where('AcceptedUserID is null'); // Do not let them use the same invitation code twice!
       $InviteExpiration = Gdn::Config('Garden.Registration.InviteExpiration');
       if ($InviteExpiration != 'FALSE' && $InviteExpiration !== FALSE)
          $this->SQL->Where('i.DateInserted >=', Gdn_Format::ToDateTime(strtotime($InviteExpiration)));
    
       return $this->SQL->Get()->FirstRow();
    }
    

    I replaced the original code with a call to this new function. I replaced lines 1219-1229 of class.usermodel.php with the following.

    $Invitation = $this->CheckInvitation($InvitationCode);
    

    Then I added code to the Connect method to call this function to validate the invitation code. I replaced line 415 of class.entrycontroller.php with the following.

    // If invitation is required, check for valid code
    if (C('Garden.Registration.Method')=='Invitation') {
       if ($this->Form->GetFormValue('InvitationCode')) {
          $Invitation = $UserModel->CheckInvitation($this->Form->GetFormValue('InvitationCode'));
          if ($Invitation->InsertUserID <= 0) {
             $this->Form->AddError('The invitation code you entered is invalid.');
             $InvitationError = TRUE;
          }
       } else {
          $InvitationError = TRUE;
       }
    }
    
    if ($this->Form->GetFormValue('Name') && ValidateRequired($this->Form->GetFormValue('Email')) 
        && (!is_array($ExistingUsers) || count($ExistingUsers) == 0) && !$InvitationError) {
    
  • These changes seem to have everything working smoothly for using external authentication with either Basic, Approval, or Invitation registration method. Please feel free to suggest other approaches or highlight any mistakes or oversights I might have made.

  • Would you mind posting the entire modified file?

  • LincLinc Detroit Admin

    @TNTitan89 Thanks for tackling this and posting your solutions. Have you used GitHub before? If you have future projects like this you can submit them to us as a pull request.

    I've added you to the Developer role so you can post in that category.

  • ToddTodd Chief Product Officer Vanilla Staff

    Wow. @TNTitan89 you are a star.

  • @Anonymoose: I attached the ZIP file containing the three modified files to the original post, but it's not there now. Maybe one of the mods removed it, or maybe I did it wrong. I am attempting to attach it again using this comment.

  • @Lincoln: I'd be glad to contribute through GitHub. I've never used it before, but I'm sure I could get the hang of it with a little instruction.

  • @Todd: Glad to contribute a little something. I've done a couple of small projects with Code Igniter, so I understand the basics of the MVC pattern. That was enough to allow me to track things down and patch them up.

  • @TNTitan89 Thanks so much!

  • peregrineperegrine MVP
    edited December 2012

    @TNTitan89

    this ranks among the best write-ups I've seen on this forum if not the best.
    In terms of troubleshooting, step by step, ultimate solution, downloadable file.

    We definitely need more of these for the developer community to thrive and prosper.

    If you, TNTitan89, or anyone else can produce similar "treatises" - I, for one, am all ears.

    we also need a category - for the best of the best - or "cream of the crop" or where duplicate discussions like this excellent one could be placed, for easy referral, so it doesn't get lost in the numerous "bonk" ,"badge", and "css" questions.

    I'd also nominate some of the discussions related to annotation of themes, etc. be duplicated in the similar best of best.

    These kind of breakdowns are essential to enhance the learning curve and promote more well-written addons. I'm sure there are alot of people would like to write plugin or share some of their knowledge but just don't know how, and discussion annotations would go a long way in that direction.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • LincLinc Detroit Admin
    edited December 2012

    @peregrine We don't really need this discussion bronzed, we just need it incorporated into core for 2.1. :) I added a link to it on the related issues on GitHub for the release. We need to make this discussion irrelevant by acting on the excellent documentation. :D

  • @Lincoln

    with all due respect. the issue and solution are not what I am talking about.
    I am talking about the "process" which shed alot of light on methodology of troubleshooting through the vanilla framework. Perhaps, I am the only one who will not use facebook, twitter, or whatever - but gained so much more from the process itself.

    annotation and troubleshooting methodology is what I was referring to.

    But I accept your answer

    We need to make this discussion irrelevant by acting on the excellent documentation. :D

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • LincLinc Detroit Admin

    @peregrine My bad, obviously my perspective is different and I was reading it with different purpose.

  • peregrineperegrine MVP
    edited December 2012

    @Lincoln said

    @peregrine My bad, obviously my perspective is different and I was reading it with different purpose.

    no worries. some of us "ain't" as smart as you guys ;).

    I'm still struggling with concept of rendering, fetching views and using form submission with customized autocompletes. would love to see even more in-depth discussion revolving around these concepts. I can usually follow coding techniques but still lack the full comprehension of the concept.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • LincLinc Detroit Admin

    Ehhhh, 'rendering' is really just 'how to puke the html'. Fetching a view is just finding it's location in the file system so we know where it is when it's time to 'render' it (a fancy 'include', basically).

  • Wow, guys! I really appreciate your generous compliments. When I do more with Vanilla, I'll definitely share my work (and explain my thought process). I messed around with GitHub a little, so I think I have a handle on the basics. If I do any work there, I will check with @Lincoln to make sure I am doing it right.

  • Thanks TNTitan99, i am looking for, just multi selection of registration methods, so users car directly register and invite.

Sign In or Register to comment.