Today the web is full of people using various platforms. Every platform has its own authentication mechanism to identify users specific to their platform. Authentication helps your application to know that the person who sent a request to your application is actually who they say they are. Today we will learn how to implement one of the most simple and easiest authentication mechanisms, Basic Authentication. This tutorial will walk you through how can you implement a Go server which implements basic authentication.
Basic Access Authentication
Basic authentication is a simple authentication scheme built into the HTTP protocol. It doesn't require cookies, session identifiers, or login pages. The client sends HTTP requests with the standard Authorization header that contains the word Basic followed by space and a base64-encoded string username:password
.
For example, the header for username test
and password secret
will look something shown below.
Authorisation: Basic dGVzdDpzZWNyZXQ=
This diagram outlines the flow of basic auth between client and server.
- If the client doesn't provide the
Authorisation
header the server returns401 Unauthorised
status code and provides information on how to authorize with a WWW-Authenticate response header containing at least one challenge. - If the client provides the
Authorisation
the server simply responds with200 OK
and the resource(In this case it's a simple welcome message but it can be any information.)
After authenticating the server can perform various other checks like whether the client has access to the resource requested etc and respond to the client accordingly. In the above example, we are simply returning a welcome message without these checks.
Creating the HTTP server
Let's create a simple HTTP server in Go.
In case you want to learn about it you can refer to my other blog Create Golang HTTP Server in 15 Lines
package main
import (
"fmt"
"log"
"net/http"
)
func greeting(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"meesage": "welcome to golang world!"}`))
return
}
func main() {
http.HandleFunc("/", greeting)
fmt.Println("Starting Server at port :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Now we will add a check for basic auth in the greeting handler.
func greeting(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message": "No basic auth present"}`))
return
}
if !isAuthorised(username, password) {
w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message": "Invalid username or password"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "welcome to golang world!"}`))
return
}
If you look at the above carefully, you will see we are using the r.BasicAuth()
method to extract the username password. Here is the method signature func (r *Request) BasicAuth() (username, password string, ok bool)
.
So the method simply checks if the authorization header is not present it returns an empty username and password with ok as false else it decodes the base64 string and splits them by: and returns the username and password along with ok as true.
After that, we check if the username and password provided by the client are present in our system or not. I have created a simple map of username and password. Here is the code for it.
var users = map[string]string{
"test": "secret",
}
func isAuthorised(username, password string) bool {
pass, ok := users[username]
if !ok {
return false
}
return password == pass
}
Now let's take a look at the entire example.
package main
import (
"fmt"
"log"
"net/http"
)
var users = map[string]string{
"test": "secret",
}
func isAuthorised(username, password string) bool {
pass, ok := users[username]
if !ok {
return false
}
return password == pass
}
func greeting(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message": "No basic auth present"}`))
return
}
if !isAuthorised(username, password) {
w.Header().Add("WWW-Authenticate", `Basic realm="Give username and password"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message": "Invalid username or password"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "welcome to golang world!"}`))
return
}
func main() {
http.HandleFunc("/", greeting)
fmt.Println("Starting Server at port :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Let me make a curl request and show it to you the output.
$curl -v http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json
< Www-Authenticate: Basic realm="Give username and password"
< Date: Sun, 11 Oct 2020 12:20:23 GMT
< Content-Length: 36
<
* Connection #0 to host localhost left intact
{"message": "No basic auth present"}* Closing connection 0
if you see the above message you will realize I made the call without the Authorisation
header is unauthenticated.
$ curl -v -u test:secret http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'test'
> GET / HTTP/1.1
> Host: localhost:8080
> Authorization: Basic dGVzdDpzZWNyZXQ=
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sun, 11 Oct 2020 12:21:15 GMT
< Content-Length: 39
<
* Connection #0 to host localhost left intact
{"message": "welcome to golang world!"}* Closing connection 0
Now after providing the correct username and password it returns success and the body with the message.
If you open the localhost:8080 in the browser it will give you a pop-up window to enter the username and password. This happens because of the header
Www-Authenticate: Basic realm="Give username and password"
.
Create a client
To create the client we use func (r *Request) SetBasicAuth(username, password string)
to set the header. It basically takes the username and password then encodes it using base 64 and then add the header Authorisation: Basic <bas64 encoded string>
. Voila, you have successfully added the basic auth to your client request. Let me show you the full code.
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
if err != nil {
panic(err)
}
req.SetBasicAuth("test", "secret")
req.Header.Add("Content-Type", "application/json")
req.Close = true
client := http.Client{}
response, err := client.Do(req)
if err != nil {
panic(err)
}
if response.StatusCode != http.StatusOK {
panic("Non 2xx response from server, request" + response.Status)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
panic(err)
}
log.Print(string(body))
}
I am using panic here to handle the error but when you are writing this for a production handle it gracefully either by returning it or by logging it.
If you want to check out the full code you can go here.
Conclusion:
Creating a client and server with basic auth support for Go is pretty easy. You don't even need an external library, the inbuilt support is more than sufficient to implement it. Also to better improve this you can move the basic auth logic into middleware and then you can attach that middleware to any handler which requires the basic auth. Just a warning basic auth is not the most secure mechanism for authentication for public-facing API.
If you liked this post please share it with others so that it can help them as well. You can tag me on twitter @imumesh18. Also please follow me here and on twitter for future blogs which I write over here