Authentication

Authentication is used to verify if the users have access to that particular part of your web application. For understanding how to implement authentication we need to understand what happens behind the scenes of a browser. Suppose we run a bank, we want only our legitimate users to access our application. We set up a login page and provide our users with their username and password which they can use to validate their claim to our webapp.

When the users submit the login form, the browser takes the username, password and sends a POST request to the webserver. The server checks this with the database and if the username and password is valid then it logs the user in.

The HTTP protocol is stateless, which means every request is unique. There is no way for identifying automatically if a request is related to another request. This brings about the problem of authentication, how then can we validate if the users have access to our webapp?

We can send the username along with each HTTP request, either in the URL via a GET request or in the POST request. But this is inefficient since for each request, the webserver would need to hit the database to validate the username, also this would mean weak security since if I know your username, I can impersonate you pretty easily and the webserver is helpless to identify this impersonation.

To solve this problems Sessions were invented, sessions need to use cookies on the browser to function. The basic idea is that the server generates a sessionID and store it in a cookie on the browser. With subsequent requests, the browser will send the sessionID along with the request, the webserver will then come to know from that sessionID if the request is a fake one or not. Also we get to know who the user is from that.

Cookies

Cookies, as we saw in a previous chapter can be used to store a key, value pair on the user's browser. We used a cookie to store the CSRF token, the cookie had the name as CSRF and value as the token.

Please don't confuse sessions with cookies. Sessions are a way of working with cookies on the server side. There is a gap of the entire Internet between sessions and cookies.

Cookies are stored in our browsers, for security reasons we need to enable the "HTTPOnly" field of our cookies, so only our webapplication can read the cookie. Otherwise the user can access the cookies using Javascript.

From the go documentation

type Cookie struct {
    Name  string
    Value string

    Path       string    // optional
    Domain     string    // optional
    Expires    time.Time // optional
    RawExpires string    // for reading cookies only

    // MaxAge=0 means no 'Max-Age' attribute specified.
    // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
    // MaxAge>0 means Max-Age attribute present and given in seconds
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // Raw text of unparsed attribute-value pairs
}

The domain of our cookie enables a restricted access to our cookie. A visitor goes to our fictional bank website, sbank.com and enters a username and password, a cookie is stored in the browser which only the sbank.com domain can access since we are security aware and we have set the HttpOnly field to true while setting the cookie. This means some malicious website which is set up by an attacker to intentionally target our bank isn't able to access the cookie using javascript.

One has to remember that cookie is nothing but a file stored in a user's browser, if can be accessed over HTTP by our webserver, a client browser does allow access to it through browser settings or custom javascript. The browser, after all is a platform, and we have APIs to that platform.

Sessions

A session is a series of actions performed between you and the webapp you are acting, enabled by the browser and the Internet you are using.

While generating a new session, we need to check if a sessions is already active, if so we return the same session ID rather than create a new one, if not, we generate a new session ID. Session IDs need to be sufficiently random. Of course we can't generate something totally random, but we have to ensure to generate something that nobody else can replicate, unless they have access to our private key which we use to generate our random number.

Session handling using gorilla/sessions

Till now we never used any third party library or a framework in this book, this is for the first time that we are doing so, we better use libraries for handling sessions, since security is the primary aspect of any web application it is wiser to use existing (and good) packages to manage security.

Path: ~/Tasks/sessions/sessions.go

package sessions

import (
    "net/http"
    "github.com/gorilla/sessions"
)

//Store the cookie store which is going to store session data in the cookie
var Store = sessions.NewCookieStore([]byte("secret-password"))

//IsLoggedIn will check if the user has an active session and return True
func IsLoggedIn(r *http.Request) bool {
    session, _ := Store.Get(r, "session")
    if session.Values["loggedin"] == "true" {
        return true
    }
    return false
}

This is the sessions package which we will use in our application.

We create a CookieStore which stores the sessions information under the "sessions" in the browser. We get the session ID stored under the session cookie and store it the session variable. When it comes to using this function, we have the AddCommentFunc view below which is going to handle the commenting feature of our application, it'll first check if the user has an active session and if so, it'll handle the POST request to add a comment, if not, it'll redirect the user to the login page.

Path: ~/Tasks/Views/addViews.go

//AddCommentFunc will be used
func AddCommentFunc(w http.ResponseWriter, r *http.Request) {
    if sessions.IsLoggedIn(r) {
    if r.Method == "POST" {
        r.ParseForm()
        text := r.Form.Get("commentText")
        id := r.Form.Get("taskID")

        idInt, err := strconv.Atoi(id)

        if (err != nil) || (text == "") {
        log.Println("unable to convert into integer")
        message = "Error adding comment"
        } else {
        err = db.AddComments(idInt, text)

        if err != nil {
            log.Println("unable to insert into db")
            message = "Comment not added"
        } else {
            message = "Comment added"
        }
        }

        http.Redirect(w, r, "/", http.StatusFound)

        }
    } else {
        http.Redirect(w, r, "/login", 302)
    }
}

The below file contains the code to login and logout of our application, we basically are going to set the "loggedin" property of our cookiestore.

Path: ~/Tasks/Views/sessionViews.go

package views

import (
    "net/http"

    "github.com/thewhitetulip/Tasks/sessions"
)

//LogoutFunc Implements the logout functionality. 
//WIll delete the session information from the cookie store
func LogoutFunc(w http.ResponseWriter, r *http.Request) {
    session, err := sessions.Store.Get(r, "session")
    if err == nil { //If there is no error, then remove session
    if session.Values["loggedin"] != "false" {
        session.Values["loggedin"] = "false"
        session.Save(r, w)
    }
    }
    http.Redirect(w, r, "/login", 302) 
    //redirect to login irrespective of error or not
}

//LoginFunc implements the login functionality, will 
//add a cookie to the cookie store for managing authentication
func LoginFunc(w http.ResponseWriter, r *http.Request) {
    session, err := sessions.Store.Get(r, "session")

    if err != nil {
    loginTemplate.Execute(w, nil) 
    // in case of error during 
    // fetching session info, execute login template
    } else {
    isLoggedIn := session.Values["loggedin"]
    if isLoggedIn != "true" {
        if r.Method == "POST" {
        if r.FormValue("password") == "secret" 
           && r.FormValue("username") == "user" {
            session.Values["loggedin"] = "true"
            session.Save(r, w)
            http.Redirect(w, r, "/", 302)
            return
        }
        } else if r.Method == "GET" {
        loginTemplate.Execute(w, nil)
        }
    } else {
        http.Redirect(w, r, "/", 302)
    }
    }
}

There is a better way of handling sessions using middleware, we'll introduce that concept in the next chapter.

Users

Signing users up

The above example just hardcodes the username and password, of course we'd want people to sign up to our service. We create a user table.

CREATE TABLE user (
    id integer primary key autoincrement,
    username varchar(100),
    password varchar(1000),
    email varchar(100)
);

For the sake of simplicity, it'll only contain ID, username, password and email.

There are two parts here, first one where we allow users to sign up, and another part where we replace the hard coded username and password in our login logic.

file: ~/Tasks/main.go

http.HandleFunc("/signup/", views.SignUpFunc)

file: ~/Tasks/views/sessionViews.go

//SignUpFunc will enable new users to sign up to our service
func SignUpFunc(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        r.ParseForm()

        username := r.Form.Get("username")
        password := r.Form.Get("password")
        email := r.Form.Get("email")

        log.Println(username, password, email)

        err := db.CreateUser(username, password, email)
        if err != nil {
        http.Error(w, "Unable to sign user up", http.StatusInternalServerError)
        } else {
        http.Redirect(w, r, "/login/", 302)
        }
    }
}

file: ~/Tasks/db/user.go

//CreateUser will create a new user, take as input the parameters and
//insert it into database
func CreateUser(username, password, email string) error {
    err := taskQuery("insert into user(username, password, email) values(?,?,?)", username, password, email)
    return err
}

We saw TaskQuery in our chapter on DB, it is a simple wrapper around db.Exec().

Note: In a real web app, you'd want to encrypt the password and not store it in plain text, this is a dummy app which'll never see the light of the day so I am keeping it plaintext.

Login

file ~/Tasks/views/sessionViews.go

//LoginFunc implements the login functionality, will add a cookie to the cookie store for managing authentication
func LoginFunc(w http.ResponseWriter, r *http.Request) {
    session, err := sessions.Store.Get(r, "session")

    if err != nil {
        log.Println("error identifying session")
        loginTemplate.Execute(w, nil)
        return
    }

    switch r.Method {
    case "GET":
        loginTemplate.Execute(w, nil)
    case "POST":
        log.Print("Inside POST")
        r.ParseForm()
        username := r.Form.Get("username")
        password := r.Form.Get("password")

        if (username != "" && password != "") && db.ValidUser(username, password) {
            session.Values["loggedin"] = "true"
            session.Values["username"] = username
            session.Save(r, w)
            log.Print("user ", username, " is authenticated")
            http.Redirect(w, r, "/", 302)
            return
        }
        log.Print("Invalid user " + username)
        loginTemplate.Execute(w, nil)
    }
}

file ~/Tasks/db/user.go

//ValidUser will check if the user exists in db and if exists if the username password
//combination is valid
func ValidUser(username, password string) bool {
    var passwordFromDB string
    userSQL := "select password from user where username=?"
    log.Print("validating user ", username)
    rows := database.query(userSQL, username)

    defer rows.Close()
    if rows.Next() {
        err := rows.Scan(&passwordFromDB)
        if err != nil {
        return false
        }
    }
    //If the password matches, return true
    if password == passwordFromDB {
        return true
    }
    //by default return false
    return false
}

Note: Since we are using gorilla/sessions, we just have to plugin the functionality that the package provides and don't have to bother about the actual implementation of sessions handling, if you are interested then by all means go ahead and checkout the source code of gorilla/sessions!

We keep resetting the password as an exercise, formulate a mechanism to resetting the password, requires us to create a user table and store some profile information, either email-ID from where we'll send a security code or by doing something else! Brainstorm!

-Previous section -Next section

results matching ""

    No results matching ""