1. Mojolicious Startup
  2. & # 8250;
  3. here

How to create user authentication using login function --Implementation of member registration function (planned to be deleted)

(This page is about to be written. Eventually, the content will be split into multiple pages. The content is inaccurate. 2020/05/05)

I will write about how to create user authentication using the login function. Do you find it difficult to authenticate users using the membership registration function in a web application? I will write an article that can solve the question of those who do not know how to create a member registration function and a login function.

After writing the article, I will write a sample code for user authentication using the membership registration function and login function in Mojolicious and MySQL, which is necessary for Web service startup.

After reading this article, you will be able to implement a security-friendly membership registration and login function that can be said to be okay for security experts if you learn the procedure.

(This article is a trial-and-error writing of the best way to do it in 2019, so it needs to be verified.)

(In the middle of writing the article. There are additions and corrections. The actual overall verification has not been done. Last updated November 14, 2019)

What is user authentication using the login function?

I will explain what user authentication is using the login function.

What is user authentication?

User authentication is the ability to identify a user. It is a function to identify which user is currently operating, such as Mr. Kimoto's operation or Mr. Tanaka's operation.

What is the login function

Generally, in a web application, a user ID and password, or an email address and password are specified, and the login function is used to implement user authentication. In this article, this feature is referred to as the login feature.

Since there are other authentication methods such as basic authentication and OAuth for user authentication, we will call the user authentication function implemented in the application by the name of login function.

It is a function to log in from the login screen that is often seen widely.

User ID

The user ID may be alphanumerical, such as "kimoto_yuki01", or it may be just a number, such as "0012345". Both cases can be handled with the same logic.

In the case of alphanumericals, I think it should be limited to the ASCII code "a-zA-Z0-9_".

The user ID can be determined by the user or by the service provider.

The user ID or email address must be unique. Set a unique constraint.

When user authentication is performed using a user ID, it is natural that the user's email address can be changed, but even if the user authentication is created using the email address, the email address can be changed. So don't worry. When you create a user table, create a column of IDs that uniquely identifies the row and set the primary key constraint and auto-increment.

Password security

The password is represented by the visible characters of the ASCII code. "A-zA-Z0-9" and ASCII symbol "& @" etc. Think of it as limiting the characters you can type on the keyboard.

To increase security, it is safe to write a constraint that the number of characters is 8 or more. The longer the password, the more secure it is, but it is also convenient, so it would be nice if the user could know about the security on the password decision screen.

The fact that it contains all of English, numbers and symbols will also make it a little safer.

The password should never be stored in the user table as it is. If user data is leaked, there is a risk that you can log in to other services by entering the password as it is. You might take it for granted, but I've actually seen a table with raw passwords.

The password uses a hash function to store its value. If you use a hash function, you won't know what the original password was. To be more precise, it's almost impossible to know. Adding a value called salt makes it even more secure. We'll talk more about this later.

HTTP communication and user authentication

HTTP communication, which is a standard protocol for the Web, is assumed to be stateless. Stateless means that there is no state, there is an HTTP request, and an HTTP response is returned, which is one process, and each HTTP request is independent.

HTTP itself does not have the ability to sustain a connection. (HTTP 2.0 is not mentioned in this article).

Basic authentication and digest authentication are defined, but they are authentication over HTTP communication and cannot be integrated into the application.

After all, user authentication to service a user must be done on the application side.

HTTP has a function called a cookie that stores data on the client side. You will make good use of this cookie feature to implement user authentication.

At the time of login, the application issues a session ID and asks it to be saved in a cookie. Then, the session ID stored in the cookie is sent, and the application side identifies the user.

How to save password

When creating user authentication using the login function, the user enters the password. You must save this password so that it is safe to be stolen.

If you save the password as-is, anyone working on the server can run the SQL in the database and see the user ID and password as-is. This is not good for security.

Therefore, instead of saving the password as it is, the value obtained by executing the hash function is saved in the password.

What is a hash function?

A hash function is a function that outputs one value when a certain value is given.

#Hash function
my $hash_value = hash_func ($value);

When people say, "Well, this is a hash function," it's just that it's a hash function. Those who believed it was very difficult would be surprised.

However, there are good hash functions and bad hash functions, and you have to choose a good hash function.

The conditions for a hash function that is said to be good are as follows.

  • Don't guess the original value from the hash value
  • For different inputs, the output hash values ​​should be as unique as possible

Guessing the original value means that the password will be known. It would be a problem if the password was easily known. So a good hash function needs to be hard to guess the original value.

In the case of password, it is not a necessary requirement that the hash value is duplicated, but if you want to treat the hash value as a unique ID, the hash value output is not duplicated as much as possible for different inputs. Is important. Keep this in mind as you will need it later to create the session ID.

By the way, hash functions have nothing to do with Perl hashes.

Hash password with bcrypt

As a result of the 2019 survey, bcrypt seems to be a good way to hash passwords. PHP 7 uses the bcrypt algorithm by default.

bcrypt seems to be a hash function algorithm that takes some computational time to guess the original value, even if the input value is short (password is short).

The implementation of bcrypt in Perl is Crypt::Eksblowfish::Bcrypt .

In Mojolicious, as a plugin Mojolicious::Plugin::There is Bcrypt.

#Create a hashed password when registering
sub signup {
    my $c = shift;
    my $encrypted_pass = $c->bcrypt($self->param('password'));
    ...
}

#Check password when logging in
sub login {
    my $self = shift;
    my $entered_pass = $self->param('password');
    my $encrypted_pass = $self->get_password_from_db();
    if ($c->bcrypt_validate($entered_pass, $encrypted_pass)) {
 
        #Authenticated
        ...;
    }
    else {
 
        # Wrong password
        ...;
    }
}

Save the hashed password generated by bcrypt in your user table password.

In case a stronger algorithm comes out in the future, bcrypt is a self-made password_hash function, bcrypt_validIf you wrap ate with your own password_validate function, maintainability may be improved.

Other than Mojolicious, the source code of Mojolicious::Plugin::Bcrypt does not depend on Mojolicious and is very easy, so it seems that you can copy and use it.

How to generate a session ID

A little difficult in user authentication is to use the session ID to identify the user, except for the first login.

When logging in from the login screen, the user is authenticated using the user ID and password, but after authentication, the session ID is used to identify the user.

What is a session ID?

A session ID is a sequence of characters that identifies a user. Let's write a sample session ID.

#Session ID sample
aabc73ce3
deab33cea
a567c73c1

The real session ID should be longer, but here it's just the atmosphere. Hexadecimal characters are arranged, but random characters such as "a-z", "A-Z", and "0-9" can be used.

Session ID must be unique and long enough

The session ID is just a string sequence, but it must meet some requirements in order to be used for user identification.

User identification first requires the ability to identify the user.

For example, if you have 10,000 users, you're in trouble if you only have 5,000 session IDs.

Also, when you issue the session ID, it's also a problem that it overlaps with other users.

Also, the shorter the session ID, the more likely it is to be guessed. In terms of security, it is better to have many types of characters and long ones.

The session ID can be just a random string sequence, but if you want to ensure uniqueness, it's easy to use a hash function that has very few collisions.

When generating with a hash function, the character type is 16 characters of "0-9a-z", so it is safe to make the character length as long as possible.

SHA-1 has 40 characters, SHA-256 has 64 characters, and SHA-512 has 128 characters.

SHA-1
356a192b7913b04c54574d18c28d46e6395428ab

SHA-256
6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b

SHA-512
4dff4ea340f0a823f15d3f4f01ab62eae0e5da579ccb851f8db9dfe84c58b2b37b89903a740e1ee172da793a6e79d560e5f7f9bd058a12a280433ed6fa46510a

The session ID can be long as it does not need to be remembered by the user.

Session ID should identify the user, be non-persistent, and difficult to guess

I use a hash function when creating a session ID, but I have to think about what the input should be.

If you use a user name, it's easy to guess. In this case, if you know the user name and the type of hash function, you can see the output.

It also means that the session ID will not change permanently. The session ID needs to be invalidated when the deadline is reached.

Add the time information to the end of your username. This will make it non-persistent. It is not unique to a user name.

Is this enough? Well, don't worry? But let's make the session ID a little stronger.

The user name and time are not random information, aren't they? An attacker can break through if he knows the username, time, and hash function.

Therefore, I will add a random number to the end. Shall I keep it around 1 million?

Add a random number of 1 million or less.

So, based on the above, let's generate a session ID. The user name, time and random number are given to the hash function of the SHA-512 algorithm.

use strict;
use warnings;

use Digest::SHA'sha512_hex';

#Create session ID
my $user_id ='kimoto';
my $time = time;
my $rand = int rand 1_000_000;
my $session_id = sha512_hex ($user_id. $time. $rand);
print "$session_id\n";

Example of output session ID

e182aa93605c82a472c4986f2749b111f35042f2bb01a3ba9eba3603653cf5ca4d9eb540f1ae32eb4c8d280f18ccb4d5f58f614652c067c7c3175e9b58d29d1a

By the way, please note that Mojolicious's session function has nothing to do with how to generate a session ID. Mojolicious sessions can be used to store session IDs.

$c->session(session_id => $session_id);

Where to store the session ID

The session ID will be issued at the time of login, but where should I save it?

The session ID should be stored in two places. It stores the client cookie and the location associated with the server-side user's ID, for example in the user table.

Save session ID in client cookie

The client authenticates the user by sending the session ID stored in the cookie to the server. If the session ID has not expired, the server will know that you are.

Mojolicious allows you to save your session ID using a feature called Sessions. Please note that Mojolicious sessions have nothing to do with the session ID described in this article.

It's a good idea to think of Mojolicious sessions as a feature that makes cookies more secure. Since it has an electronic signature, a function has been added to the cookie that it can detect if it has been tampered with.

It is not encrypted and is not suitable for storing user-identifiable information, such as user IDs and passwords. It's easy to crack, and if you save your raw password, it will be known when your cookie is stolen.

Store the session ID in a Mojolicious session (signed cookie). This is done only once when the user logs in.

Store session ID in Mojolicious session (signed cookie)
$c->session(session_id => $session_id);

Even if it's not Mojolicious, it's the same if you take advantage of the framework's ability to store cookies.

Save the session ID in the location associated with the user ID on the server side

The session ID can be saved anywhere on the server side as long as it can be linked with the user ID. It can be a memory, a file, a relational database (MySQL, PostgreSQL), or a volatile key-value store (Redis, memcached).

If it is memory, it cannot be handled if the server is forking. If it is a file, it cannot be handled when the application servers are lined up in parallel. Relational databases have access to the database. Volatile key-value stores are faster than relational databases, but at the cost of installing and using a server.

Mojolicious Startup is a site that enables new development of the Web with Perl, so I will show you how to use a relational database to avoid complexity and make it easy to apply.

If you understand the basic idea, you can use other methods.

In this article, I will introduce you using MySQL and Perl DBI as samples.

User table definition for session management

The user table looks like this: Suppose the database is InnoDB and the character code is utf8mb4 in the MySQL settings. Be sure to set InnoDB as the default table engine. InnoDB has a row lock feature and does not lock the entire table.

create table user (
  id int primary_key auto_increment,
  code varchar (150) not null default'',
  name varchar (150) not null default'',
  mail varchar (150) not null default'',
  session_id varchar (150),
  session_expiration bigint not null default 0,
  temp_regist_id varchar (150),
  temp_regist_expiration bigint not null default 0,
  authenticated tiny int default 0,
  unique (code),
  unique (mail),
  unique (session_id),
  unique (temp_regist_id)
);

The main point is about the session, but I'll briefly describe what this user table is based on.

Database table and field name naming conventions are based on database table and field name naming conventions.

The user ID is int type and auto_increment because the user is the registered data. It has an ID that can uniquely identify the column. CODE is what is called a user ID, such as kimoto. Since the ID is used as information to identify the row, the user ID is stored in CODE.

MAIL is an e-mail address, and if you want to register as a member, you will need to send an e-mail to verify your identity. E-mail addresses have unique restrictions so that they are not duplicated.

In mysql, varchar (150) is set to 150 as large as possible because the storage size does not change regardless of the length of varchar up to 255.

mysql has an old MIn the case of ySQL, when utf8mb4 is specified, there is a specification that the index works only up to the length of 191.

Not null constraints should be in place unless there is an absolute need to allow nulls. This is because it is easier to judge if NULL is excluded in the application logic.

Now, here is the story of the session. The session ID must be unique, so put a unique constraint on it. However, it does not have a not null constraint. This is because the session ID does not exist at the stage of membership registration. Knowing the session ID stored in the cookie allows you to search the user table to determine which user is accessing it.

The session has an expiration date, but if you save it as a 64-bit integer type with bigint, you can clear the 2019 problem. The reason it is not datetime type is that you don't have to write the logic for date conversion every time you check the session ID. Sessions need to be checked no matter which page the user visits, so it's best to have the best possible performance.

It seems that the time when the number of users is increasing and the cost of the application server is found by checking the session is the time to switch the storage destination from the relational database to the volatile key-value store.

Save session ID

The session ID will be issued when you log in, but let's save it in the session of the user table at this time. The session is valid for 2 weeks. The session ID is the session ID explained in "How to generate a session ID". In the sample, DBI exception handling is not written, but in fact, please write it.

#Save session. The deadline is two weeks later.
my $two_week_seconds = (60 * 60 * 24 * 14);
my $session_expiration = time + $two_week_seconds;
my $sth = $dbi->prepare('update user set session_id =?, session_expiration =?');
$sth->execute($session_id, $session_expiration);

If the session ID has a unique constraint, if it overlaps, this process will fail, but since the session ID is created so that the probability of duplication is very small, the probability of duplication is astronomical. Will be smaller.

If you want to prevent it from happening, issue a session ID and repeat the process if it fails.

Session confirmation

If the user is logged in, the session ID is stored in the cookie. Compare the session ID stored in the cookie with the user ID stored in the database.

Make sure the session IDs match and that they haven't expired.

A sample code from Mojolicious and DBI.

# Get session ID from cookie
my $cookie_session_id = $c->session('session_id');

# Get the current time
my $cur_time = time;

#Get user information with matching sessions and not expired
my $sth = $dbi->prepare('select id, session_expiration from user where session_id =?, session_expiration <?');
$sth->execute($cookie_session_id, $cur_time);
my $user = $sth->fetchrow_hashref;
$sth->finish;

#If not, authentication failed
unless ($user) {
  # Write the processing when authentication fails
}

#Get user ID
my $user_id = $user->{ID};

# Get the expiration date of the current session
my $user_session_expiration = $user->{SESSION_EXPIRATION};

If you can see the session, you know who is logged in. All you need is your user ID, but you also have a session expiration date to use for updating your session expiration date.

Update session expiration date

Let's update the session while the user is continuously logged in. Let's implement the session to expire if there is no login for 2 weeks from the date and time of the last login.

One thing to keep in mind here is that the update process by update is more expensive than select. Select is easy to scale out, but update is hard to scale out because it always has to update the master database. Also, when the update process is performed, the target row is locked in InnoDB of MySQL.

Keep in mind that update is different from select in that it's hard to scale out and it locks rows.

Therefore, I don't want to write the process of updating the session expiration date every time there is access. Here, let's use the logic of updating the session expiration date only once a day.

This is a sample using DBI. Some variable names are derived from the sample above.

#New expiration date set 2 weeks after now
my $new_user_session_expiration = $cur_time + $two_week_seconds;

# Seconds of the day
my $one_day_seconds = 60 * 60 * 24;

#Renew if the expiration date has not been renewed within 1 day
if ($new_user_session_expiration> $user_session_expiration + $one_day_seconds) {
  my $sth = $dbi->prepare('update user set session_expiration =? from user where id =?');
  $sth->execute($new_user_session_expiration, $user_id);
}

Implementation of membership registration function

The member registration function is required for the login function by user authentication. First, I will explain what you need to know to implement the membership registration function.

Identity verification by email address

In this article, we will register as a new member using the common method of personal authentication using an email address.

Identity verification by email address is identity verification based on the fact that the email address is owned by that person and only that person can see the email.

If the password of the e-mail is leaked, that's all, so recently, double authentication such as phone numbers has been adopted by major sites. Start up your website by verifying your identity with your email address.

User information required for membership registration screen

The minimum user information required for the member registration screen is as follows.

  • User ID (not required if email address is user ID)
  • Email address
  • password

If you create a user ID, it is your user ID, email address, and password. If your email address is your user ID, it's your email address and password. Even if you use your e-mail address as your user ID, you can change your e-mail address if you wish, so don't worry.

In the following articles, I will write articles on the assumption that I will create a user ID.

Depending on the type of business, it may be advisable to enter "name", "address", "phone number", etc.

Create user table

The correspondence between the user ID and the email address must be one-to-one, the user ID must be unique, and the email address must be unique. It is necessary to check in the application, but it is important that the data is completely consistent, so create the user table as follows.

This is the same as described in "User Table Definition for Session Management" above. The ID uniquely defines the row and contains an integer, not the user ID. A user ID such as kimoto is entered in CODE.

In addition, personal authentication by e-mail address is performed by sending the e-mail address to the person and having them access from the link using the sent temporary registration ID. Therefore, the user table has a temporary registration ID "TEMP_REGIST_ID" and a temporary registration expiration date "TEMP_REGIST_EXPIRATION".

This is an example of creating a user table with mysql.

create table user (
  id int primary_key auto_increment,
  code varchar (150) not null default'',
  password_hash varchar (150) not null default'',
  name varchar (150) not null default'',
  mail varchar (150) not null default'',
  session_id varchar (150),
  session_expiration bigint not null default 0,
  temp_regist_id varchar (150),
  temp_regist_expiration bigint not null default 0,
  authenticated tiny int default 0,
  unique (code),
  unique (mail),
  unique (session_id),
  unique (temp_regist_id)
);

Creating a member registration screen

On the member registration screen, you need to specify your user ID, email address, and password.

This is an example of a member registration screen using a Mojolicious sample. Use the POST method to submit the form. action specifies its own URL, the method is POST, and "temp-regist" is specified as the operation so that it can branch.

hidden_field is a Mojolicious function that creates a hidden field. The same applies to text_field and password_field. Mojolicious's form helper will take the value itself if you have to return to the page, such as if the value was incorrect in validation.There is a function to restore it by motion.

<div class = "login-form">
  <form action = "<%url_for%>" method = "POST">
    <div>
      <%= hidden_field op =>'temp-regist'%>
    </div>
    <div>
      User ID
      <%= text_field'user-id'%>
    </div>
    <div>
      email address
      <%= text_field'mail'%>
    </div>
    <div>
      password
      <%= password_field'password'%>
    </div>
    <div>
      <input type = "submit" value = "submit">
    </div>
  </form>
</div>

Check if the user ID is correct

Make sure your user ID is correct. The correct user ID means the following.

  • Consists of at least one of the characters that make up the user ID (alphanumers and underscores in this example)
  • The user ID does not overlap with an existing user ID
  • The email address does not overlap with an already authenticated email address
  • The length of the user ID fits in the length of the CODE column in the database

Note that the user ID must be unique, so you need to access the database to check this.

Let's write a sample with Mojolicious and DBI. If it is not correct, I try to add it to the error information.

#Error information
my $errors = {};

my $user_id = param ('user-id');

#Check the characters that make up the user ID
if ($user_id = ~ / ^ [a-zA-Z0-9_] + $/) {

  #Check the length of the user ID
  my $max_user_id_length = 100;
  if (length $user_id> $max_user_id_length) {
    $errors->{user_id} ='Create the user ID within 100 characters. ';
  }
  else {

    #Check that the user ID is not duplicated
    my $sth = $dbi->prepare('select code from user where code =?');
    $sth->execute($user_id);
    my $user = $sth->fetchrow_hashref;
    $sth->finish;
    if ($user) {
      $errors->{user_id} ='Already exists user. ';
    }
    else {
      #Check that the email address is not duplicated
      my $sth = $dbi->prepare('select mail from user where mail =? and authenticated = 1');
      $sth->execute($mail);
      my $user = $sth->fetchrow_hashref;
      $sth->finish;
      if ($user) {
        $errors->{user_id} ='Email address already registered. ';
      }
    }
  }
}
else {
  $errors->{user_id} ='Create the user ID with at least one character of "a-zA-Z0-9_". ';
}

The user ID consists of one or more characters of "a-zA-Z0-9_", but this check is done using Perl regular expressions.

The length of the user ID is checked because the length of CODE is 150 characters, so I set it to 100 in a clear place. 100 characters should be enough.

I'm accessing the database to make sure the users are unique.

Error information is stored in the hash reference. This allows you to retrieve individual error messages or just the error messages as an array.

Check if the password is valid

Next is the password check. Define the following as a valid password:

  • Password consists of 8 or more ASCII code graphic characters

ASCII code graphic characters are ASCII code printed characters without spaces. Passwords do not allow blanks, but allow alphanumericals and symbols that you can type on the keyboard.

ASCII code graphic characters are appropriate for this group.

# List of printed characters. Graphic characters are the ones without spaces.
  @`
! A a
"B b
#C c
$D d
%E e
& F f
'G g
(H h
) I i
* J j
+ K k
, L l
--M m
. N n
/ O o
0 P p
1 Q q
2 R r
3 S s
4 T t
5 U u
6 V v
7 W w
8 X x
9 Y y
: Z z
; [{{
<\ |
=]}
> ^ ~
? _

If the password is too short, there is a high risk that you will be logged in in a round-robin manner, so use at least 8 characters.

It would be more kind if the UI can be secured by the length of the password.

The password is not stored in clear text in PASSWORD_HASH of the user table, but the hashed one is stored by bcrypt, so you do not need to check the maximum length.

Let's write a sample with Mojolicious.

my $password = param ('password');

# Make sure the password is at least 8 ASCII graphic characters
unless ($password = ~ / ^ \ p {PosixGraph} {8,} $/) {
  $errors->{password} ='Specify the password with at least 8 alphanumerical characters and symbols. ';
}

"\ P {PosixGraph}" is a regular expression character class that represents ASCII code graphic characters. By combining quantifiers, we have confirmed that the number of graphic characters is 8 or more.

Password hashing

Passwords are hashed using bcrypt.

# Password hashing
my $password_hash = $c->bcrypt($password);

Check your email address

I don't think it makes much sense to check your email address strictly. That's because you don't know if your email address is correct until you actually reach the user.

Here, I would like to keep it to the extent that it confirms for the user that "@" is included as a check of the email address.

my $mail = param ('mail');

# Make sure your email address contains @
unless ($mail = ~ / \ @/) {
  $errors->{mail} ='Please enter your email address correctly. ';
}

Saving user data and issuing temporary registration ID

Save the checked user data in the database. At the same time, a temporary temporary registration ID used for temporary registration is issued and saved in the database.

The temporary registration ID can be generated in the same way as the session ID generation method, but there is a concern that the link cannot be clicked correctly due to the wrapping of the body of the email, so generate it within the default wrapping character number of 76 characters in Outlook. I will decide. Generated with SHA1, it is 40 characters long.

#Create a temporary registration ID
my $time = time;
my $rand = int rand 1_000_000;
my $temp_regist_id = sha1_hex ($user_id. $Time. $rand);

Example of generated temporary registration ID

e182aa93605c82a472c4986f2749b111f35042f2

Next, create a valid period for the temporary registration ID. The validity period is 24 hours.

#The temporary registration ID is valid after 24 hours.
my $one_day_seconds = (60 * 60 * 24);
my $temp_regist_expiration = time + $one_day_seconds;
Now, let's save the user information, temporary registration ID, and temporary registration expiration date in the user table. One thing to note at this time is that it is necessary to write down the processing when the temporary registration user performs the temporary registration again.

#Save user information, temporary registration ID, temporary registration expiration date in the user table
my $sth = $dbi->prepare('insert into user (set cdoe =?, password_hash =?, mail =?, temp_regist_id =?, temp_regist_expiration =?');
$sth->execute($user_id, $password_hash, $temp_regist_id, $temp_regist_expirasession_id, $temp_regist_expiration);

->

Notes

A note about the sample. DBI should be read as DBIx::Connector or DBIx::Handler when actually using it. This is because when using DBI for web development, you need a module that manages connections.

For the sake of simplicity, please note that exception handling is not performed for database processing. When actually writing, add exception handling or use the O / R mapper that handles exception handling automatically.

Consideration

It may be better to divide it into a user table, a temporary registration user table, and a user authentication table.

I would like to ask a security expert.

Can password authentication be bcrypt?

If the session ID is SHA-512, it will be 128 characters, but is there any problem?

Associated Information