Phase-4 at FlatIron school brought-forth several new topics including rails, validations, deployment, and authorization. One of the topics I was having trouble with in this phase was password authentication. As such I chose to write my phase-4 blog about the topic. While I had a hard time wrapping my head around this topic, I am writing this to further push my understanding and to give a different point of view, perhaps to someone who is also struggling with the concept. For this example we'll be using rails and react to set up password authentication for users.
Cookies
Before we can begin adding our routes and creating our methods, we need to make sure cookies are enabled in the application configuration file.
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
#This is for security
config.action_dispatch.cookies_same_site_protection = :strict
We will also need to make sure to add include ActionController::Cookies in our application controller in order for all controllers to have access to cookies. Cookies are important because it is what will allow us to store the user in a session.
Login
We'll start with the login route. For a user to log in we will need a route and a post method in the SessionsController, as we are creating a session for the user that will be logged in.
The route we'll use will also be a post request and go in the routes.rb file.
post "/login", to: "sessions#create"
In the Sessionscontroller we'll add
class SessionsController < ApplicationController
def create
user = User.find_by(username: params[:username])
if user&.authenticate(params[:password])
session[:user_id] = user.id
render json: user, status: :created
else
render json: { error: "Invalid username or password" }, status: :unauthorized
end
end
end
In the code above we are creating a session, where if the password passed in from the params matches the password we have stored in the database, we set the session [:user_id] to the users id.
The front end will look something like this:
function Login({ onLogin }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(e) {
e.preventDefault();
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
})
.then((r) => r.json())
.then((user) => onLogin(user));
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}
Above we can see that we are making our post request passing in the username and password.
Staying Logged In
One thing to note is when we refresh the page we lose the value of state which above we have set to password and username. Refreshing the page would log the user out and thus would have to log in again. We can handle this by retrieving the user data from the database once the user logs in. For this we would have to create a separate fetch route, that comes from the UsersController rather than the SessionsController.
get "/me", to: "users#show"
and in the "UsersController" we add a find method to find the user associated with said account.
class UsersController < ApplicationController
def show
user = User.find_by(id: session[:user_id])
if user
render json: user
else
render json: { error: "Not authorized" }, status: :unauthorized
end
end
end
On the front end we can do a fetch request to keep the user logged in.
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/me").then((response) => {
if (response.ok) {
response.json().then((user) => setUser(user));
}
});
}, []);
Creating A User
To keep things short B-crypt is a gem that allows us to store our passwords in the database in a secure way. Passwords stored as a hash rather than the real value of the password which would be a security risk. When we create a password in a data-table it isn't stored in a password column, but instead in a column named password_digest. It is important that this column be named password_digest if we want to use B-crypt. In the user model we'll make sure to add has_secure_password in order to hash and salt all passwords.
class User < ApplicationRecord
has_secure_password
end
The has_secure_password we added also provides two instances for our "User" model which are password and password_confirmation This will allow us to create a new user and add password and password-confirmation fields in a signup form.
*Another bonus is it allows us to raise exceptions to handle any errors.
Just like before we'll add a create method for a signup form in our front end.
class UsersController < ApplicationController
def create
user = User.create(user_params)
if user.valid?
render json: user, status: :created
else
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
end
end
private
def user_params
params.permit(:username, :password, :password_confirmation)
end
end
The params we are permitting for our create method are the username, password and password-confirmation fields which can be seen below.
function SignUp({ onLogin }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
function handleSubmit(e) {
e.preventDefault();
fetch("/signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
password_confirmation: passwordConfirmation,
}),
})
.then((r) => r.json())
.then(onLogin);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<label htmlFor="password_confirmation">Confirm Password:</label>
<input
type="password"
id="password_confirmation"
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
Deleting a Session
We can now sign in and sign up users, but what if we want to delete the current session or logout? It is very similar, where we'll need a route in our route.rb file, a delete method in our "SessionsController" and a request made from our front end, that will be triggered when we click on a Logout button.
#route
delete "/logout", to: "sessions#destroy"
#method in SessionsController
def destroy
session.delete :user_id
head :no_content
end
#react/frontEnd request
function Navbar({ onLogout }) {
function handleLogout() {
fetch("/logout", {
method: "DELETE",
}).then(() => onLogout());
}
return (
<header>
<button onClick={handleLogout}>Logout</button>
</header>
);
}
User authentication for phase-4 was by far one of the most challenging topics for me to comprehend at first, but was still very engaging to learn. As I finish phase-4, I am now able to create full-stack applications. However, there is still much to learn and I am eager to see what else there is out there.
Sources:
Accessing A Session
B-Crypt
Has-Secure Password
FlatIronSchool
Top comments (0)