Edit 2009-11-30
This in from Jeremy Evans
------------------------------
Looks better, but I recommend a few more changes:
1) You need to salt your password hashes. Unsalted hashes are better
than storing the password in plaintext, but most common hash
algorithms probably already have large rainbow tables that will allow
an easy lookup of most passwords given an unsalted hash.
2) I would recommend at least including random data when generating
the random key for password resets. Your use of the username and
Time.now makes it guessable if you know roughly when the user
requested/will request a password change. encrypt_password is a
poorly named method, since you are hashing, not encrypting (encrypting
implies the possibility of decrypting, while hashing is one way).
3) I generally put a time limit on password resets. That way if
someone requests one, but then remembers their password and doesn't
change it, they are not vulnerable to someone else changing it next
year.
------------------------------------------------
So, add the salt for the password, modify the generate_rand_key() to add a random component (possibly just add a #{rand(1000000)} to the end of the string to add in a 6 digit random component, rename encrypt_password to say hash_password, and finally add a time limit. Here, you'll need to add a date to the user table, add a date when you generate_rand_key(), and check this in change_password() when you also test the existence of a user with the key.
Thanks for the improvements Jeremy.
------------------------------------------------
After my
last post, I received, as I noted, some rather stern comments on a) saving passwords in plaintext in the database and b) sending them in plaintext via email. So, I decided to fix the example up to make it a bit better. We'll be saving the password as a hash in the database and also when the user forgets their password, we'll send a link to let them change it. One other suggestion was to add a salt for the password hash which is easy enough to do, but I've left it as an exercise for the reader. I should note that some of the ideas in this post were stolen (and I mean that in the good sense) from
JustKez. This post is definitely worth reading over as are others on the site.
OK, let's get started. We'll start out with the database migration.
Class.new(Sequel::Migration) do
def up
create_table(:users) do
primary_key :id
String :user_name
String :password
String :email
String :rand_key
end
end
def down
drop_table(:users)
end
end
This is a bit simpler than in the previous example. We're only going to have a users table with the user_name, password (which will be encrypted), an email address, and a random key (used for sending the user their password). The down method will just drop the users table. To generate the password, simply type
sequel -M dbMigration/ sqlite://forgot.db. This will create the database we'll use for the project.
The model for this is pretty simple too.
require 'digest/sha1'
class User < Sequel::Model
def self.encrypt_password(password)
Digest::SHA1.hexdigest(password)
end
def generate_rand_key
self.rand_key = Digest::SHA1.hexdigest("#{@username} -- #{Time.now}")
save
end
end
The only model we have is for the User. We have a couple of methods. The first is a Class level method for encrypting the password. We create a hash of the password using SHA1 and return the value (it will be used for creation of the user and changing the password). The second method is for generating a random key which will be passed as part of a link to the user in case they forget or lose their password.
Here's our start code
require 'rubygems'
require 'ramaze'
require 'sequel'
require 'net/smtp'
require 'mailit'
MAILER = Mailit::Mailer.new(:server => 'MailServer.MyCompany.COM', :port => 25, :username => 'ApplicationName',
:password => 'ApplicationPassword')
DB = Sequel.sqlite("forgot.db")
require 'models/models'
require 'controllers/main_controller'
Ramaze.start
This is pretty much the same as most of our start files. We add in the code for mailit, open the database, and then require our models and controllers. Finally, we start ramaze.
Next up our only controller.
class MainController < Ramaze::Controller
map '/'
layout(:page) { !request.xhr? }
helper :aspect
before(:account_settings, :main) {
unless logged_in?
flash[:message] = "You must log in before accessing the requested page."
redirect rs(:index)
end
}
def index
@title = "SteamCode - User"
end
def main
"<h2>Main</h2>"
end
def about
"<h2>About</h2>"
end
def help
"<h2>User Help</h2>"
end
def register
@title = "Register with SteamCode"
if request.post?
unless User.find(:user_name => request[:user_name])
user_name = request[:user_name]
password = request[:password]
email = request[:email]
user = User.create(:user_name => user_name, :password => User.encrypt_password(password), :email => email)
Ramaze::Log.debug "New User Added: user_name = #{user.user_name} password = #{user.password} email = #{user.email}"
redirect rs(:login)
else
flash[:message] = "Login #{request[:user_name]} already used. Please select another."
redirect rs(:register)
end
end
end
def login
@title = "Login to SteamCode"
if request.post?
if user = User.find(:user_name => request[:user_name], :password => User.encrypt_password(request[:password]))
session[:user_id] = user.id
session[:user_name] = user.user_name
redirect rs(:main)
else
flash[:message] = "Incorrect user name or password, please try again!!!"
session[:user_id] = nil
session[:user_name] = nil
redirect rs(:login)
end
end
end
def account_settings
@title = "Account Settings for SteamCode"
user = User[session[:user_id]]
if request.post?
user = User[session[:user_id]]
user.email = request[:email]
user.password = User.encrypt_password(request[:password])
user.save
flash[:message] = "New email and/or password saved."
redirect rs(:main)
end
@current_email = user.email
end
def logout
@title = "Logout from SteamCode"
flash[:message] = "#{session[:user_name]} Logged out"
session[:user_id] = nil
session[:user_name] = nil
redirect rs(:index)
end
def forgot_password
if request.post?
if user = User.find(:user_name => request[:user_name])
user.generate_rand_key
mail = Mailit::Mail.new
mail.to = user.email
mail.from = "Steamcode@MyCompany.com"
mail.subject = "Steamcode Password"
mail.text = "To change your password: http://localhost:7000/change_password/#{user.rand_key}"
MAILER.send(mail)
redirect rs(:login)
else
flash[:message] = "Could not find user: #{request[:user_name]}"
redirect rs(:forgot_password)
end
end
end
def change_password(key)
user = User.find(:rand_key => key)
if user
if request.post?
user.password = User.encrypt_password(request[:password])
user.rand_key = nil
user.save
flash[:message] = "Your new password saved."
redirect rs(:login)
end
else
flash[:message] = "This does not appear to be a valid request."
redirect rs(:login)
end
end
private
def logged_in?
session[:user_id] != nil
end
end
This is very similar to many of our other controllers from our past posts. We map to '/', Add the layout (which won't be used for AJAX calls (not used here anyway)), and then the aspect which will prevent the user from going to certain pages unless they're logged in to the system. The next four methods, index, main, about, and help are really just place holders. Index is where the user will end up initially and main is where they will end up after logging in. Then we have the register page. This will check if it's a post and if it is, try to find the user. If it can't find the user (note the use of "unless" here. To a certain extend, I'm trying to decide if it helps or hinder readability. Let me know what you think) it will create a new one with the parameters from the request hash. If the user already exists, we'll just flash a message back and redirect them back to the register page. Next we have the login method. We check to make sure this is from a post, then check to see if we can find the user based on their user name and password (using the model's encrypt_password method). If we find them, we go ahead and log them in (set the session variables) and redirect them to the main page. If not, we flash a message, reset the session variables, and redirect them back to the login page. The account_settings page let's the user reset their email and password. The logout page resets the session variables and then redirects back to the index page. Looking at it now, I'd recommend refactoring the session resets here and in login to their own private method (once again we'll leave that as an exercise). The forgot_password method is the reason for all of this. We check that it's a post and contains a valid user. If so, we generate a random key for the user (using the model method) and then create an email with the link containing this key. If we can't find the user, we'll flash a message and redirect back to the same page. Finally (for the pages anyway), we have the change_password page. This checks to see if we have user with this rand_key (from the link generated above) and if we do, we check if this is a post and change their password appropriately, reset the random key (so it's used only once), flash a message and redirect them back to the login page. If there wasn't a user with that random key, we simply flash a message and redirect them back to the login page. Finally, we have the private method that's used to check if a user is logged in to the system.
Here's our layout page.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="stylesheet" type="text/css" href="/page.css"/>
<title>#@title</title>
</head>
<body>
<div id="whole_page">
<div id="header">
<h1>SteamCode</h1>
</div>
<div id="nav">
<span style="float: right">
<?r if !logged_in? ?>
<a href="#{r(:login)}">Login</a>
<a href="#{r(:register)}">Register</a>
<?r else ?>
<a href="#{r(:account_settings)}">Account</a>
<a href="#{r(:logout)}">Logout</a>
<?r end ?>
</span>
<a href="#{r(:index)}">Home</a> |
<a href="#{r(:about)}">About Us</a> |
<a href="#{r(:help)}">Help</a>
</div>
<div id="content">
#@content
</div>
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</div>
</body>
</html>
It's stripped down a bit from the last post and has all of the administration stuff removed. Since you've seen this in past posts, I won't go through it. It's mostly just code for the navigation with some content and a header and footer tossed in for good measure.
Let's take a look at the views for the project. They're really simple this time. No JavaScript and no AJAX. Here's the index.
#{flashbox}
<h2>Welcome</h2>
Here's the login page
#{flashbox}
<a href="#{r(:forgot_password)}">Forgot your password?</a>
<br/>
<form id="login" method="post">
<fieldset>
<legend> Login </legend>
<div>
<label for="user_name">User Name:</label>
<input id="user_name" name="user_name" type="text" />
<br/>
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>
<input type="submit" value="Login" />
</div>
</fieldset>
</form>
There's just input boxes for the user name and password and a button for submitting.
Here's the registration page.
<form id="register" method="post">
<div>
<label for="user_name">Login:</label>
<input id="user_name" name="user_name" type="text" />
<br/>
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>
<label for="email">Email:</label>
<input id="email" name="email" type="text" />
<br/>
<input type="submit" value="Register" />
</div>
</form>
As above boxes for user name, password, and email plus the submit button.
Here's the page for changing the account settings.
<form id="change_password" method="post">
<fieldset>
<legend> Change Settings </legend>
<div>
<label for="email">Email:</label>
<input id="email" name="email" type="text" value=#{@current_email} />
<br/>
<label for="password">Password:</label>
<input id="password" name="password" type="password"/>
<br/>
<input type="submit" value="Submit" />
</div>
</fieldset>
</form>
This contains boxes for password and email (the user can't change their user_name) and the submit button.
Next is the forgot_password.xhtml.
<h2>Forgot Password</h2>
#{flashbox}
<form id="forgot_screen" method="post">
<fieldset>
<legend> Forgot Password </legend>
<div id='forgot_password'>
<label for="user_name">User Name:</label>
<input id="user_name" name="user_name" type="text" value="User Name" />
<br/>
</div>
<input type="submit" value="Send Password" />
</fieldset>
</form>
<br/>
This has a box for the user_name and the submit button. When the user puts in their user_name and submits, an email is generated (see above) that provides a link to change their password. This page is the change_password.xhtml.
<form id="change_password" method="post">
<div>
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>
<input type="submit" value="Change Password." />
</div>
</form>
Once again, dead simple with the password box and a submit (you'd probably want a confirmation password that would be checked using JavaScript, but we'll just assume that the user won't make any mistakes.
That's pretty much everything except for the CSS which is in public/page.css
#whole_page {
width: 50em;
margin: auto;
padding: 0;
text-align: left;
border-width: 0 1px 1px 1px;
border-color: black;
border-style: solid;
}
#content {
height: 100%;
background: white;
padding: 1em 1em 1em 1em;
}
/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 0.25em;
}
#nav {
background:#9DA9EE;
color: black;
padding: 0.5em;
}
/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}
So, I hope this works out for everyone a bit better than the last version and gets at least a bit closer to what you might use in a "real" project. Let me know if you have questions or comments.