package HTML::FormHandlerX::Form::Login;
use 5.006;
use strict;
use warnings;
=head1 NAME
HTML::FormHandlerX::Form::Login - An HTML::FormHandler login form.
=head1 VERSION
Version 0.14
=cut
our $VERSION = '0.14';
$VERSION = eval $VERSION;
=head1 SYNOPSIS
Performs login form validation, including changing passwords, forgotten passwords, and resetting passwords.
If you are working under Catalyst, take a look at L or L.
Registering...
$form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email password confirm_password ) ] );
$form->process( params => { email => $email,
password => $password,
confirm_password => $confirm_password,
} );
Login with either an C B C parameter.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email password ) ] );
$form->process( params => { email => $email, password => $password } );
Changing a password...
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( old_password password confirm_password ) ] );
$form->process( params => { old_password => $old_password,
password => $password,
confirm_password => $confirm_password,
} );
Forgot password, just validates an C, or C.
Use this to create a C to send to the user to verify their email address.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email ) ] );
$form->process( params => { email => $email } );
if ( $form->validated )
{
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
my $token = $form->token;
}
Coming back from an email link, if the form validates, you would show the password reset form (carry the token in a hidden field or cookie).
$form = HTML::FormHandlerX::Form::Login->new( active => [ qw( token ) ] );
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
$form->process( params => { token => $token } );
When trying to actually reset a password...
$form = HTML::FormHandlerX::Form::Login->new( active => [ qw( token password confirm_password ) ] );
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
$form->process( params => { token => $token,
password => $password,
confirm_password => $confirm_password,
} );
=head1 DESCRIPTION
This module will validate your forms. It does not perform any actual authentication, that is still left for you.
=head2 Register
You can register with either an C or C.
Using C brings in validation using L.
C/C, C and C are all required fields, so will fail validation if empty.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email password confirm_password ) ] );
$form->process( params => { email => $email, password => $password, confirm_password => $confirm_password } );
=head2 Login
You can choose between C and C for the unique identifier.
Using C brings in validation using L.
C/C and C are all required fields, so will fail validation if empty.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email password ) ] );
$form->process( params => { email => $email, password => $password } );
=head2 Change Password
Instantiate the form by activating the 3 fields: C, C, and C.
All 3 fields are required, and validation will also check the C matches the C.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( old_password password confirm_password ) ] );
$form->process( params => { old_password => $old_password,
password => $password,
confirm_password => $confirm_password,
} );
if ( $form->validated ) { }
=head2 Forgot Password
Provide the C B C to validate, the form will then have a C for you.
You can then send this C to the user via email to verify their identity.
You need to supply a (private) C to make sure your Cs are not guessable. This can be anything you like.
Tokens expire by default after 24 hours from the date/time of issue. To change
this, either supply an epoch timestamp of when to expire, or give a human-friendly format of how long to wait. We like things like:
2h - 2 hours
3d - 3 days
4w - 4 weeks
5m - 5 months
If you specify C the value of this field in the form will be included in the token. This can be useful when the token is sent back, to identify the user.
my $form = HTML::FormHandlerX::Form::Login->new( active => [ qw( email ) ] );
$form->process( params => { email => $email } );
if ( $form->validated )
{
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
$form->add_token_field( 'email' );
$form->token_expires( '3h' );
my $token = $form->token;
}
The token is comprised of a L hash, so can be a tad long, but has much less chance of collisions compared to an MD5.
=head2 Reset Password - Stage 1
You will usually give the token to the user in an email so they can verify they own the email address.
This step is for just showing the user a reset-password form.
The first step when the user comes back to reset their password, is to check they have not fiddled with the token.
You can safely skip this step, we check the token again when they/you actually try to change the password, this just lets you stop them in their tracks a little sooner.
Setting the C is required, and must obviously be the same C as used in the forgot-password call.
C as you did during the forgot-password process. This will populate the unique identifier field for you.
$form = HTML::FormHandlerX::Form::Login->new( active => [ qw( token ) ] );
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
$form->add_token_field( 'email' );
$form->process( params => { token => $token } );
if ( $form->validated ) { }
=head2 Reset Password - Stage 2
You have now shown the user a form to enter a new password (and confirm it).
Either hidden in that form, or as a cookie, you have also stored the token.
$form = HTML::FormHandlerX::Form::Login->new( active => [ qw( token password confirm_password ) ] );
$form->token_salt( 'SoMeThInG R4nD0M AnD PR1V4te' );
$form->add_token_field( 'email' );
$form->process( params => { token => $token,
password => $password,
confirm_password => $confirm_password,
} );
if ( $form->validated ) { }
If you specified the C as C, you can now collect that from the form as the record to update safely.
$form->field( 'email' )->value;
And now know which user to update.
=cut
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use Digest::SHA qw( sha512_hex );
use Email::Valid;
=head1 METHODS
=head2 Attributes
=head3 token
$form->token
Returns a unique string for the C or C validated by the form.
You typically send this to the users email.
=cut
has token => ( is => 'rw', isa => 'Str', lazy_build => 1 );
=head3 token_fields
$form->add_token_field( 'email' );
Specifies which fields to include in the token for you to identify which user it is trying to reset their password when they come back.
Either C or C is normal.
=cut
has _token_fields => ( is => 'rw',
isa => 'ArrayRef[Str]',
default => sub { [] },
traits => ['Array'],
handles => { token_fields => 'elements',
add_token_field => 'push',
}
);
=head3 token_salt
$form->token_salt
Your own (random string) salt used to create the reset-password token.
=cut
has token_salt => ( is => 'rw', isa => 'Str', default => '' );
=head3 token_expires
$form->token_expires
Dictates how long the token is valid for, default is 1 day.
Possible formats are 2h, 3d, 6w, 1m, or an epoch timestamp.
=cut
has token_expires => ( is => 'rw', isa => 'Int', default => 86400 ); # 1 day
=head2 Fields
=head3 token
$form->field('token')
This field is used when attempting to reset a password.
=cut
has_field token => ( type => 'Hidden',
required => 1,
messages => { required => "Missing token." },
wrapper_attr => { id => 'field-token', },
tags => { no_errors => 1 },
inactive => 1,
);
=head3 email / username
$form->field('email')
$form->field('username')
The C field, or use the specific C field for extra validation (employing Email::Valid).
=cut
has_field email => ( type => 'Email',
required => 1,
messages => { required => 'Your email is required.' },
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-email' },
inactive => 1,
);
has_field username => ( type => 'Text',
required => 1,
messages => { required => 'Your username is required.' },
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-username' },
inactive => 1,
);
=head3 old_password
$form->field('old_password')
Required when changing a known password.
C has a built-in length restriction for C fields of 6-characters, we drop that to 1-character, it is up to you to come with your own rules.
=cut
has_field old_password => ( type => 'Password',
minlength => 1,
required => 1,
messages => { required => "Your old password is required." },
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-old-password', },
inactive => 1,
);
=head3 password
$form->field('password')
Used for logging in, changing and/or resetting a password to something new.
C has a built-in length restriction for C fields of 6-characters, we drop that to 1-character, it is up to you to come with your own rules.
=cut
has_field password => ( type => 'Password',
minlength => 1,
required => 1,
messages => { required => "Your password is required." },
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-password', },
inactive => 1,
);
=head3 confirm_password
$form->field('confirm_password')
Required for changing and/or resetting the password.
=cut
has_field confirm_password => ( type => 'PasswordConf',
required => 1,
password_field => 'password',
messages => { required => "You must confirm your password." },
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-confirm-password', },
inactive => 1,
);
=head3 remember
$form->field('remember')
Useful for a "remember me" checkbox.
=cut
has_field remember => ( type => 'Checkbox',
tags => { no_errors => 1 },
wrapper_attr => { id => 'field-remember', },
inactive => 1,
);
=head3 submit
$form->field('submit')
The submit button.
=cut
has_field submit => ( type => 'Submit',
value => '',
wrapper_attr => { id => 'field-submit', },
);
=head2 Validation
=head3 validate_token
The internal validation of the token when attempting to reset a password.
=cut
sub validate_token
{
my ( $self, $field ) = @_;
my @token_parts = split( ':', $field->value );
my $token = pop @token_parts;
if ( $token ne sha512_hex( $self->token_salt . join( '', @token_parts ) ) )
{
$field->add_error("Invalid token.");
}
my $time = pop @token_parts;
if ( time > $time )
{
$field->add_error("Expired token.");
}
}
=head3 html_attributes
This method has been populated to ensure all fields in error have the C CSS class assigned to the labels.
=cut
sub html_attributes
{
my ($self, $field, $type, $attr, $result) = @_;
if( $type eq 'label' && $result->has_errors )
{
push @{$attr->{class}}, 'error';
}
}
after build_active => sub {
my $self = shift;
if ( ( $self->field('email')->is_active || $self->field('username')->is_active ) && $self->field('password')->is_active && $self->field('confirm_password')->is_active )
{
$self->field('submit')->value('Register');
}
elsif ( ( $self->field('email')->is_active || $self->field('username')->is_active ) && $self->field('password')->is_active )
{
$self->field('submit')->value('Login');
}
elsif ( ( $self->field('email')->is_active || $self->field('username')->is_active ) && ! $self->field('password')->is_active && ! $self->field('token')->is_active )
{
$self->field('submit')->value('Forgot Password');
}
elsif ( $self->field('old_password')->is_active && $self->field('password')->is_active && $self->field('confirm_password')->is_active )
{
$self->field('password')->label('New Password');
$self->field('submit')->value('Change Password');
}
elsif ( $self->field('token')->is_active )
{
$self->field('password')->label('New Password');
$self->field('submit')->value('Reset Password');
}
};
around token_expires => sub {
my $orig = shift;
my $self = shift;
if ( my $arg = shift )
{
if ( $arg =~ /(\d+)h/i )
{
$arg = $1 * 3600;
}
elsif ( $arg =~ /(\d+)d/i )
{
$arg = $1 * 86400;
}
elsif ( $arg =~ /(\d+)w/i )
{
$arg = $1 * 604800;
}
elsif ( $arg =~ /(\d+)m/i )
{
$arg = $1 * 2629743;
}
return $self->$orig( $arg );
}
return $self->$orig;
};
sub _build_token
{
my $self = shift;
return '' if $self->token_salt eq ''; # no salt, no token
my $time = time + $self->token_expires;
my @field_value_list = map { $self->field( $_ )->value } $self->token_fields;
my $token = join( ':', @field_value_list, $time, sha512_hex( $self->token_salt . join( '', @field_value_list ) . $time ) );
return $token;
}
sub _munge_params
{
my ( $self, $params ) = @_;
if ( exists $params->{ token } )
{
# the order is drastically important
my @token_parts = split( ':', $params->{ token } );
foreach my $field ( $self->token_fields )
{
$self->field( $field )->inactive(0);
$params->{ $field } = shift @token_parts;
}
}
$self->next::method( $params );
}
=head1 RENDERING
This form does some subtle rendering tricks, renaming buttons and labels based on which fields are active.
=head1 TODO
Look at password type fields, pre-set char-length, etc. and/or import types from HTML::FormHandler directly.
=head1 AUTHOR
Rob Brown, C<< >>
=head1 BUGS
Please report any bugs or feature requests to C, or through
the web interface at L. I will be notified, and then you will
automatically be notified of progress on your bug as I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc HTML::FormHandlerX::Form::Login
You can also look for information at:
=over 4
=item * RT: CPAN's request tracker (report bugs here)
L
=item * AnnoCPAN: Annotated CPAN documentation
L
=item * CPAN Ratings
L
=item * Search CPAN
L
=back
=head1 ACKNOWLEDGEMENTS
gshank: Gerda Shank Egshank@cpan.orgE
t0m: Tomas Doran Ebobtfish@bobtfish.netE
=head1 LICENSE AND COPYRIGHT
Copyright 2012 Rob Brown.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See http://dev.perl.org/licenses/ for more information.
=cut
1; # End of HTML::FormHandlerX::Form::Login