Go Back

user authentication without cookies or javascript

user authentication is a complicated subject, and we can make things even harder if we don't want to use cookies or javascript. authentication is a necessity for almost all web services we see, and there are many tools and protocols to aid developers on the subject. using the www-authenticate header i was able to handle user sessions without any code on the client side, just some tweaks on my backend. the documentation for the standard is available in [1].

the http www-authenticate header is a powerful tool and part of the basic http authentication standard, but i only became aware of it recently, in this post i'll tell how i used it to handle user sessions on my project tasker.

before we start, let me give a quick overview of the solution.

pros

cons

implementation

basically all we have to do is add the http header: WWW-Authenticate on a response with status 401. this will make browsers to show a dialog for the user to prompt for the credentials. doing this the browser will automatically send the Authorization: ... header on requests.

this can be done when the user visits a login page or tries to access private content. in my case i added it to a login page. the code goes like this:

func login(w http.ResponseWriter, r *http.Request) {
	user, pass, ok := r.BasicAuth()
	if !ok {
		// the user is not logged in (didn't send credentials)
		w.Header().Set("WWW-Authenticate", "Basic")
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// check credentials
	...

	// if ok redirect the user to its content
	http.Redirect(w, r, "/user.html", http.StatusFound)
}

i am using the Basic authorization scheme, but many others are supported, e.g. digest using MD5, SHA256 and so on, the RFC 7616 [2] has all info needed.

logging in

the way i designed the login flux is the following:

  1. user requests the /login page without authorization header
  2. the server responds with 401 and includes the WWW-Authenticate header
  3. the user fills username and password and request the same path but now the browser includes the authorization header
  4. the server checks the credentials and if OK sends a redirect status, e.g. 301, with the location of the new page. if not OK the server an error page, so the user can retry after a refresh

when browsers receive this header in the response they open a dialog for the user, some aspects can be set, for example, if realm is set on the header it will be displayed for the user, but be careful, this option serves a purpose, check [1] for more info. The dialog looks like this on chromium:

Chromium login dialog

clicking sign in makes the browser to repeat the request but with the Authorization: ... header on, as i set it to Basic scheme the browser will send the credentials base64 encoded.

the nice thing about this is that the browser will keep sending the credentials to subsequent requests of the same domain until it receives a 401 status as response.

server code for logged in users

now the users can log in, every time a private page is requested we must check the credentials, in this project i used the Basic scheme so i check it using go's proper http functions:

user, pass, ok := r.BasicAuth()
if !ok {
	w.Header().Set("WWW-Authenticate", "Basic")
	w.WriteHeader(http.StatusUnauthorized)
	// send an error page
	return
}

// check user and pass
...

// serve your content

this way if a request comes unauthenticated for some reason the server will ask again for credentials. another option here would be to redirect the user to the login page.

logging out

logging out is done by simply returning a 401 without www-authenticate header:

func logout(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusUnauthorized)
	// serve a logout html page
	...
}

final remarks

this is the method i'm using right now and i find it pretty good: it uses only standard features that are there for years, nothing new; there is no client side javascript or cookies, which makes it easy to maintain and satisfy even the most demanding users.