SQL-based authentication

From Rosetta Code
Jump to: navigation, search
Task
SQL-based authentication
You are encouraged to solve this task according to the task description, using any language you may know.

This task has three parts:

  • Connect to a MySQL database (connect_db)
  • Create user/password records in the following table (create_user)
  • Authenticate login requests against the table (authenticate_user)

This is the table definition:

CREATE TABLE users (
userid INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32) UNIQUE KEY NOT NULL,
pass_salt tinyblob NOT NULL,
-- a string of 16 random bytes
pass_md5 tinyblob NOT NULL
-- binary MD5 hash of pass_salt concatenated with the password
);

(pass_salt and pass_md5 would be binary(16) values, but MySQL versions before 5.0.15 strip trailing spaces when selecting them.)

Contents

[edit] C

Library: mysqlclient
(MySQL client library)
Library: OpenSSL
(for MD5)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <time.h>
 
#include <mysql.h>
#include <openssl/md5.h>
 
void end_with_db(void);
 
MYSQL *mysql = NULL; // global...
 
bool connect_db(const char *host, const char *user, const char *pwd,
const char *db, unsigned int port)
{
if ( mysql == NULL )
{
if (mysql_library_init(0, NULL, NULL)) return false;
mysql = mysql_init(NULL); if ( mysql == NULL ) return false;
MYSQL *myp = mysql_real_connect(mysql, host, user, pwd, db, port, NULL, 0);
if (myp == NULL) {
fprintf(stderr, "connection error: %s\n", mysql_error(mysql));
end_with_db();
return false;
}
}
return true; // already connected... ?
}
 
#define USERNAMELIMIT 32
// no part of the spec, but it is reasonable!!
#define PASSWORDLIMIT 32
#define SALTBYTE 16
bool create_user(const char *username, const char *password)
{
int i;
char binarysalt[SALTBYTE];
char salt[SALTBYTE*2+1];
char md5hash[MD5_DIGEST_LENGTH];
char saltpass[SALTBYTE+PASSWORDLIMIT+1];
char pass_md5[MD5_DIGEST_LENGTH*2 + 1];
char user[USERNAMELIMIT*2 + 1];
char *q = NULL;
static const char query[] =
"INSERT INTO users "
"(username,pass_salt,pass_md5) "
"VALUES ('%s', X'%s', X'%s')";
static const size_t qlen = sizeof query;
 
 
for(i=0; username[i] != '\0' && i < USERNAMELIMIT; i++) ;
if ( username[i] != '\0' ) return false;
for(i=0; password[i] != '\0' && i < PASSWORDLIMIT; i++) ;
if ( password[i] != '\0' ) return false;
 
srand(time(NULL));
 
for(i=0; i < SALTBYTE; i++)
{
// this skews the distribution but it is lazyness-compliant;)
binarysalt[i] = rand()%256;
}
 
(void)mysql_hex_string(salt, binarysalt, SALTBYTE);
 
for(i=0; i < SALTBYTE; i++) saltpass[i] = binarysalt[i];
strcpy(saltpass+SALTBYTE, password);
(void)MD5(saltpass, SALTBYTE + strlen(password), md5hash);
(void)mysql_hex_string(pass_md5, md5hash, MD5_DIGEST_LENGTH);
 
(void)mysql_real_escape_string(mysql, user, username, strlen(username));
 
// salt, pass_md5, user are db-query-ready
q = malloc(qlen + USERNAMELIMIT*2 + MD5_DIGEST_LENGTH*2 + SALTBYTE*2 + 1);
if ( q == NULL ) return false;
sprintf(q, query, user, salt, pass_md5);
#if defined(DEBUG)
fprintf(stderr, "QUERY:\n%s\n\n", q);
#endif
int res = mysql_query(mysql, q);
free(q);
if ( res != 0 )
{
fprintf(stderr, "create_user query error: %s\n", mysql_error(mysql));
return false;
}
return true;
}
 
 
bool authenticate_user(const char *username, const char *password)
{
char user[USERNAMELIMIT*2 + 1];
char md5hash[MD5_DIGEST_LENGTH];
char saltpass[SALTBYTE+PASSWORDLIMIT+1];
bool authok = false;
char *q = NULL;
int i;
static const char query[] =
"SELECT * FROM users WHERE username='%s'";
static const size_t qlen = sizeof query;
 
// can't be authenticated with invalid username or password
for(i=0; username[i] != '\0' && i < USERNAMELIMIT; i++) ;
if ( username[i] != '\0' ) return false;
for(i=0; password[i] != '\0' && i < PASSWORDLIMIT; i++) ;
if ( password[i] != '\0' ) return false;
 
(void)mysql_real_escape_string(mysql, user, username, strlen(username));
 
q = malloc(qlen + strlen(user) + 1);
if (q == NULL) return false;
sprintf(q, query, username);
 
int res = mysql_query(mysql, q);
free(q);
if ( res != 0 )
{
fprintf(stderr, "authenticate_user query error: %s\n", mysql_error(mysql));
return false;
}
 
MYSQL_RES *qr = mysql_store_result(mysql);
if ( qr == NULL ) return false;
 
// should be only a result, or none
if ( mysql_num_rows(qr) != 1 ) {
mysql_free_result(qr);
return false;
}
 
MYSQL_ROW row = mysql_fetch_row(qr); // 1 row must exist
unsigned long *len = mysql_fetch_lengths(qr); // and should have 4 cols...
 
memcpy(saltpass, row[2], len[2]); // len[2] should be SALTBYTE
memcpy(saltpass + len[2], password, strlen(password));
(void)MD5(saltpass, SALTBYTE + strlen(password), md5hash);
 
authok = memcmp(md5hash, row[3], len[3]) == 0;
mysql_free_result(qr);
 
return authok;
}
 
void end_with_db(void)
{
mysql_close(mysql); mysql = NULL;
mysql_library_end();
}
 
int main(int argc, char **argv)
{
 
if ( argc < 4 ) return EXIT_FAILURE;
 
if ( connect_db("localhost", "devel", "", "test", 0 ) )
{
if ( strcmp(argv[1], "add") == 0 )
{
if (create_user(argv[2], argv[3]))
printf("created\n");
} else if ( strcmp(argv[1], "auth") == 0 ) {
if (authenticate_user(argv[2], argv[3]))
printf("authorized\n");
else
printf("access denied\n");
} else {
printf("unknown command\n");
}
end_with_db();
}
return EXIT_SUCCESS;
}

From the command line, program add user password to add users, and program auth user password to see if the user with that password is authorized or not.

[edit] Java

Works with: Java version 1.7
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.math.BigInteger;
 
 
class UserManager {
private Connection dbConnection;
 
public UserManager() {
}
 
private String md5(String aString) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest md;
String hex;
StringBuffer hexString;
byte[] bytesOfMessage;
byte[] theDigest;
 
hexString = new StringBuffer();
bytesOfMessage = aString.getBytes("UTF-8");
md = MessageDigest.getInstance("MD5");
theDigest = md.digest(bytesOfMessage);
 
for (int i = 0; i < theDigest.length; i++) {
hex = Integer.toHexString(0xff & theDigest[i]);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
 
return hexString.toString();
}
 
public void connectDB(String host, int port, String db, String user, String password)
throws ClassNotFoundException, SQLException {
 
Class.forName("com.mysql.jdbc.Driver");
 
this.dbConnection = DriverManager.getConnection("jdbc:mysql://"
+ host
+ ":"
+ port
+ "/"
+ db, user, password);
}
 
public boolean createUser(String user, String password) {
SecureRandom random;
String insert;
String salt;
 
random = new SecureRandom();
salt = new BigInteger(130, random).toString(16);
 
insert = "INSERT INTO users "
+ "(username, pass_salt, pass_md5) "
+ "VALUES (?, ?, ?)";
 
try (PreparedStatement pstmt = this.dbConnection.prepareStatement(insert)) {
pstmt.setString(1, user);
pstmt.setString(2, salt);
pstmt.setString(3, this.md5(salt + password));
pstmt.executeUpdate();
 
return true;
} catch(NoSuchAlgorithmException | SQLException | UnsupportedEncodingException ex) {
return false;
}
}
 
public boolean authenticateUser(String user, String password) {
String pass_md5;
String pass_salt;
String select;
ResultSet res;
 
select = "SELECT pass_salt, pass_md5 FROM users WHERE username = ?";
res = null;
 
try(PreparedStatement pstmt = this.dbConnection.prepareStatement(select)) {
pstmt.setString(1, user);
res = pstmt.executeQuery();
 
res.next(); // We assume that username is unique
 
pass_salt = res.getString(1);
pass_md5 = res.getString(2);
 
if (pass_md5.equals(this.md5(pass_salt + password))) {
return true;
} else {
return false;
}
 
} catch(NoSuchAlgorithmException | SQLException | UnsupportedEncodingException ex) {
return false;
} finally {
try {
if (res instanceof ResultSet && !res.isClosed()) {
res.close();
}
} catch(SQLException ex) {
}
}
}
 
public void closeConnection() {
try {
this.dbConnection.close();
} catch(NullPointerException | SQLException ex) {
}
}
 
public static void main(String[] args) {
UserManager um;
 
um = new UserManager();
try {
um.connectDB("localhost", 3306, "test", "root", "admin");
 
if (um.createUser("johndoe", "test")) {
System.out.println("User created");
}
 
if (um.authenticateUser("johndoe", "test")) {
System.out.println("User authenticated");
}
} catch(ClassNotFoundException | SQLException ex) {
ex.printStackTrace();
} finally {
um.closeConnection();
}
}
}

[edit] Perl

Works with: Perl version 5.8.8
Library: DBI

use DBI;
 
# returns a database handle configured to throw an exception on query errors
sub connect_db {
my ($dbname, $host, $user, $pass) = @_;
my $db = DBI->connect("dbi:mysql:$dbname:$host", $user, $pass)
or die $DBI::errstr;
$db->{RaiseError} = 1;
$db
}
 
# if the user was successfully created, returns its user id.
# if the name was already in use, returns undef.
sub create_user {
my ($db, $user, $pass) = @_;
my $salt = pack "C*", map {int rand 256} 1..16;
$db->do("INSERT IGNORE INTO users (username, pass_salt, pass_md5)
VALUES (?, ?, unhex(md5(concat(pass_salt, ?))))"
,
undef, $user, $salt, $pass)
and $db->{mysql_insertid} or undef
}
 
# if the user is authentic, returns its user id. otherwise returns undef.
sub authenticate_user {
my ($db, $user, $pass) = @_;
my $userid = $db->selectrow_array("SELECT userid FROM users WHERE
username=? AND pass_md5=unhex(md5(concat(pass_salt, ?)))"
,
undef, $user, $pass);
$userid
}

[edit] PHP

To use MySQL in PHP you need the php_mysql module installed

 
function connect_db($database, $db_user, $db_password, $host = 'localhost', $port = NULL, $die = false) {
// Returns a MySQL link identifier (handle) on success
// Returns false or dies() on error depending on the setting of parameter $die
// Parameter $die configures error handling, setting it any non-false value will die() on error
// Parameters $host, $port and $die have sensible defaults and are not usually required
 
if(!$db_handle = @mysql_connect($host.($port ? ':'.$port : ''), $db_user, $db_password)) {
if($die)
die("Can't connect to MySQL server:\r\n".mysql_error());
else
return false;
}
if(!@mysql_select_db($database, $db_handle)) {
if($die)
die("Can't select database '$database':\r\n".mysql_error());
else
return false;
}
return $db_handle;
}
 
function create_user($username, $password, $db_handle) {
// Returns the record ID on success or false on failure
// Username limit is 32 characters (part of spec)
if(strlen($username) > 32)
return false;
 
// Salt limited to ASCII 32 thru 254 (not part of spec)
$salt = '';
do {
$salt .= chr(mt_rand(32, 254));
} while(strlen($salt) < 16);
 
// Create pass_md5
$pass_md5 = md5($salt.$password);
 
// Make it all binary safe
$username = mysql_real_escape_string($username);
$salt = mysql_real_escape_string($salt);
 
// Try to insert it into the table - Return false on failure
if(!@mysql_query("INSERT INTO users (username,pass_salt,pass_md5) VALUES('$username','$salt','$pass_md5')", $db_handle))
return false;
 
// Return the record ID
return mysql_insert_id($db_handle);
}
 
function authenticate_user($username, $password, $db_handle) {
// Checks a username/password combination against the database
// Returns false on failure or the record ID on success
 
// Make the username parmeter binary-safe
$safe_username = mysql_real_escape_string($username);
 
// Grab the record (if it exists) - Return false on failure
if(!$result = @mysql_query("SELECT * FROM users WHERE username='$safe_username'", $db_handle))
return false;
 
// Grab the row
$row = @mysql_fetch_assoc($result);
 
// Check the password and return false if incorrect
if(md5($row['pass_salt'].$password) != $row['pass_md5'])
return false;
 
// Return the record ID
return $row['userid'];
}
 

[edit] Python

Works with: Python version 2.7

Uses the official Python MySQL connector

import mysql.connector 
import hashlib
 
import sys
import random
 
DB_HOST = "localhost"
DB_USER = "devel"
DB_PASS = "devel"
DB_NAME = "test"
 
def connect_db():
''' Try to connect DB and return DB instance, if not, return False '''
try:
return mysql.connector.connect(host=DB_HOST, user=DB_USER, passwd=DB_PASS, db=DB_NAME)
except:
return False
 
def create_user(username, passwd):
''' if user was successfully created, returns its ID; returns None on error '''
db = connect_db()
if not db:
print "Can't connect MySQL!"
return None
 
cursor = db.cursor()
 
salt = randomValue(16)
passwd_md5 = hashlib.md5(salt+passwd).hexdigest()
 
# If username already taken, inform it
try:
cursor.execute("INSERT INTO users (`username`, `pass_salt`, `pass_md5`) VALUES (%s, %s, %s)", (username, salt, passwd_md5))
cursor.execute("SELECT userid FROM users WHERE username=%s", (username,) )
id = cursor.fetchone()
db.commit()
cursor.close()
db.close()
return id[0]
except:
print 'Username was already taken. Please select another'
return None
 
def authenticate_user(username, passwd):
db = connect_db()
if not db:
print "Can't connect MySQL!"
return False
 
cursor = db.cursor()
 
cursor.execute("SELECT pass_salt, pass_md5 FROM users WHERE username=%s", (username,))
 
row = cursor.fetchone()
cursor.close()
db.close()
if row is None: # username not found
return False
salt = row[0]
correct_md5 = row[1]
tried_md5 = hashlib.md5(salt+passwd).hexdigest()
return correct_md5 == tried_md5
 
def randomValue(length):
''' Creates random value with given length'''
salt_chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
 
return ''.join(random.choice(salt_chars) for x in range(length))
 
if __name__ == '__main__':
user = randomValue(10)
passwd = randomValue(16)
 
new_user_id = create_user(user, passwd)
if new_user_id is None:
print 'Failed to create user %s' % user
sys.exit(1)
auth = authenticate_user(user, passwd)
if auth:
print 'User %s authenticated successfully' % user
else:
print 'User %s failed' % user
 

[edit] Raven

MySQL connectivity is available out of the box. Below, we do not ever send the plain text password over the wire.

 'mysql://root@localhost/test' open as mysql
'abcdefghijklmnopqrstuvwxyz0123456789' as $salt_chars
 
# return userid for success and FALSE for failure.
define create_user use $user, $pass
group 16 each as i
$salt_chars choose chr
join as $pass_salt
"%($pass_salt)s%($pass)s" md5 as $pass_md5
$user copy mysql escape as $user_name
group 'INSERT IGNORE into users (username, pass_md5, pass_salt)'
" VALUES ('%($user_name)s', unhex('%($pass_md5)s'), '%($pass_salt)s')"
join mysql query inserted
 
# return userid for success and FALSE for failure.
define authenticate_user use $user, $pass
FALSE as $userid
$user copy mysql escape as $user_name
group 'SELECT userid, pass_salt, hex(pass_md5)'
" FROM users WHERE username = '%($user_name)s'"
join mysql query as rs
rs selected
if rs fetch values into $possible_userid, $pass_salt, $pass_md5
"%($pass_salt)s%($pass)s" md5 $pass_md5 lower =
if $possible_userid as $userid
$userid
 
'foo' 'bar' create_user  !if "could not create user\n" print bye
'foo' 'bar' authenticate_user !if "could not authenticate user\n" print bye
 
"user successfully created and authenticated!\n" print

[edit] Tcl

Works with: Tcl version 8.6

Also requires the TDBC driver for MySQL.

package require tdbc
 
proc connect_db {handleName dbname host user pass} {
package require tdbc::mysql
tdbc::mysql::connection create $handleName -user $user -passwd $pass \
-host $host -database $dbname
return $handleName
}
 
# A simple helper to keep code shorter
proc r64k {} {
expr int(65536*rand())
}
 
proc create_user {handle user pass} {
set salt [binary format ssssssss \
[r64k] [r64k] [r64k] [r64k] [r64k] [r64k] [r64k] [r64k]]
# Note that we are using named parameters below, :user :salt :pass
# They are bound automatically to local variables with the same name
$handle allrows {
INSERT IGNORE INTO users (username, pass_salt, pass_md5)
VALUES (:user, :salt, unhex(md5(concat(:salt, :pass))))
}
return ;# Ignore the result of the allrows method
}
 
proc authenticate_user {handle user pass} {
$handle foreach row {
SELECT userid FROM users WHERE
username=:user AND pass_md5=unhex(md5(concat(pass_salt, :pass)))
} {
return [dict get $row userid]
}
# Only get here if no rows selected
error "authentication failed for user \"$user\""
}
Template:Omit from PostScript
Personal tools
Namespaces

Variants
Actions
Community
Explore
Misc
Toolbox