Gomologin is a Go package that handles all aspects of login process including user authentication, authorization, session management, and securing resources of our application behind login walls. It works with databases/sql package to retrieve user information from SQL databases.
During the login process, the client/browser sends a request containing the credential to server to check if it match the one in the database. If it matches, the user successfully logs in and Gomologin grant him/her the access to web application/website resources corresponding to its roles.
Authentication and Authorization
Authentication is the process of identifying a user. In other words it determines who the users are. In other hand, Authorization checks wheather a user has the right to access a particular web resource or not.
Gomologin is quite easy to use after the initial configuration. It can easily setup and customize login process of the application in couple of lines. It works with roles, so you can specify which roles has right to access which resources. Moreover, it has built-in session storage that allows developer to store data related to a user in a session. In the next sections I talk about all features of the Gomologin and teach you how to use it in your Go web application.
How to setup
Like every other Go package, we need to download it in our project using go get command:
go get github.com/birddevelper/gomologin
After downloading the package, we can import it in our code :
package main
import (
// other packages
"github.com/birddevelper/gomologin"
)
Then, we need to configure the Gomologin engine. However, most of parameters has default value and no need for explicit setting.
Gomologin parameters are :
Parameter | Function | Description | Example |
---|---|---|---|
LoginPage | SetLoginPage(path string) | Login page html template file path. Default path is ./template/login.html | gomologin.Configure().SetLoginPage(“./templates/myloginpage.html”) |
LoginPath | SetLoginPath(path string) | Login url relative path. Default path is /login | gomologin.Configure().SetLoginPath(“/”) |
SessionTimeout | SetSessionTimeout(seconds int) | Number of seconds before the session expires. Default value is 120 seconds. | gomologin.Configure().SetLoginPath(300) |
PasswordEnceyption | SetPasswordEncryption() | A custom encryption function that accepts one argument in string and returns a string. Default function is EncNoEncrypt which do nothing on the given password.** | gomologin.Configure(). SetPasswordEncryption(gomologin.EncMD5) |
Sql query for authentication and retrieving roles | AuthenticateBySqlQuery() | SQL queries to check user credential and retrieve its roles by username and password sent from client. The authentication query must return only single arbitary column, it must have a where clause with two placeholder ::username and ::password. And the query for retrieving user’s roles must return only the text column containing the role name. | AuthenticateBySqlQuery( db, “select id from users where username = ::username and password = ::password”, “select role from user_roles where userid = (select id from users where username = ::username)”) |
** You can set any function which accept single string and returns single string as encryption function. See the following example to understand the concept :
func MyHashFuncion(s string) string {
return ("fakeHash"+s)
}
func main(){
// use MyHashFunction as password encryption method.
gomologin.Configure().SetPasswordEncryption(MyHashFunction)
}
Note that all of above listed functions return Config object which means we can call setting function sequentially one after another. See the example :
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"time"
"github.com/birddevelper/gomologin"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// create connection to database
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:6666)/mydb")
if err != nil {
log.Fatal(err)
}
// Gomologin configuration
gomologin.Configure().
SetLoginPage("./template/login.html"). // set login page html template path
SetSessionTimeout(90). // set session expiration time in seconds
SetLoginPath("/login"). // set login http path // set database connection and sql query
AuthenticateBySqlQuery( db, "select id from users where username = ::username and password = ::password", // authentication query
"select role from user_roles where userid = (select id from users where username = ::username)") // fetch user's roles
}
As you can see, the configuration took only 4 lines of code. After configuration done, we can wrap any handler function that we want to prevent from unauthenticated/unauthorized access. See the example :
// instantiate http server
mux := http.NewServeMux() mux.Handle("/static/", public())
// use Gomologin login handler for /login endpoint
mux.Handle("/login", gomologin.LoginHandler())
// the pages/endpoints that we need to protect should be wrapped with gomologin.LoginRequired
mux.Handle("/mySecuredPage", gomologin.LoginRequired(securedPage()))
// the pages/endpoints those we want to be available for two roles => ADMIN and MODERATOR
mux.Handle("/mySecuredPage2", gomologin.RolesRequired(securedPage2()),"ADMIN","MODERATOR")
You can see from the above example that I didn’t protect /login path and also /public path in the web application which contains static files such as css, javascript and images.
The complete main.go file :
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"time"
"github.com/birddevelper/gomologin"
_ "github.com/go-sql-driver/mysql"
)
// static assets like CSS and JavaScript
func public() http.Handler {
return http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
}
// a page in our application, it needs user only be authenticated
func securedPage() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi! Welcome to secured page.")
})
}
// another page in our application, it needs user be authenticated and have ADMIN role
func securedPage2() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi! Welcome to very secured page.")
})
}
func main() {
// create connection to database
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:6666)/mydb")
if err != nil {
log.Fatal(err)
}
// Gomologin configuration
gomologin.Configure(). SetLoginPage("./template/login.html"). // set login page html template path
SetSessionTimeout(90). // set session expiration time in seconds
SetLoginPath("/login"). // set login http path // set database connection and sql query
AuthenticateBySqlQuery( db, "select id from users where username = ::username and password = ::password", // authentication query
"select role from user_roles where userid = (select id from users where username = ::username)") // fetch user's roles
// instantiate http server
mux := http.NewServeMux() mux.Handle("/static/", public())
// use Gomologin login handler for /login endpoint
mux.Handle("/login", gomologin.LoginHandler())
// the pages/endpoints that we need to protect should be wrapped with gomologin.LoginRequired
mux.Handle("/mySecuredPage", gomologin.LoginRequired(securedPage()))
mux.Handle("/mySecuredPage2", gomologin.RolesRequired(securedPage2()),"ADMIN")
// server configuration
addr := ":8080"
server := http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 15 * time.Second,
}
// start listening to network
if err := server.ListenAndServe(); err != nil {
log.Fatalf("main: couldn't start simple server: %v\n", err)
}
}
In the whole application we can access the gomologin session storage as well as gomologin functions.
Function | Returns | Description | |
gomologin.SetSession() | – | This function stores a session key-value entry in session storage. | |
gomologin.GetSession() | interface{}, bool | It retrieves session value by its key from session storage. | |
gomologin.RemoveSession() | – | It removes a session entry by its key. | |
gomologin.GetCurrentUsername() | string | It returns current authenticated username. | |
gomologin.GetDataReturnedByAuthQuery() | interface{} | It returns the column which is specified in authentication select query. | |
gomologin.GetCurrentUserRoles() | []string | Returns current authenticated user roles in string slice. | |
gomologin.HasRole(role) | bool | It checks wheather the user poses the given role or not. | |
gomologin.LoginRequired() | http.Handler | It’s a wrapper function, it accepts Handler and procced the request only if the client is already authenticated. | |
gomologin.RolesRequired() | http.Handler | It’s a wrapper function, it accepts Handler and procced the request only if the client is already authenticated and it poses the specified roles. | |
gomologin.LoginHandler() | http.Handler | It returns Handler of login page. It should be routed to your login url path. | mux.Handle(“/login”, gologin.LoginHandler()) |
Code examples showing how to use gomologin functions in project :
// a page in our application
func securedPage() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := gomologin.GetDataReturnedByAuthQuery(r)
// get current authenticated username
username := gomologin.GetCurrentUsername(r)
// create a new session data
gomologin.SetSession("test", "hello", r)
// get the already stored data back.
txt, _ := gomologin.GetSession("test", r)
// As the GetSession return type is interface{}, we should specify the exact type of the session data
fmt.Fprintf(w, txt.(string)+" Hi "+username+"! Welcome to secured page. Your Id is "+strconv.FormatInt(id.(int64), 10)+
" <a href='/login?logout=yes' > Logout </a>")
// GetCurrentUserRoles return slice of string containing current user roles
for _, role := range gomologin.GetCurrentUserRoles(r) {
fmt.Fprintf(w, " Role 1 : "+role+"\n")
}
})
}
At last but not the least, you will be in need of logout action. Gomologin already has solution for it. Just send user to /yourloginpath?logout=yes. It clears all session data and log out the user.
Awesome package. Thanks a lot.
How to use it with Fiber?