Authentication

Credential based Authentication

Login each time when you need to operate something

Cookie Based Authentication

  • The traditional approach of authentication
  • Stateful. This means that an authentication record or session must be kept both server and client-side. The server needs to keep track of active sessions in a database (memory/local store), while on the front-end a cookie is created that holds a session identifier, thus the name cookie based authentication.

5. Authentication-2019-5-1-17-29-44.png

  1. User enters their login credentials
  2. Server verifies the credentials are correct and creates a session which is then stored in a database
  3. A cookie with the session ID is placed in the users browser
  4. On subsequent requests, the session ID is verified against the database and if valid the request processed
  5. Once a user logs out of the app, the session is destroyed both client and server side

Question: what’s the problem of this method?

Imagine when you need to replace your ID?

  1. Go to government, apply for a temporary ID.
  2. It may take them 1 months to send you a new ID.
  3. During this period, you may use your temporary ID for some purposes.

Token-Based Authentication

  • More and more popular (Yelp API, etc.)
  • Usually JSON Web Tokens (JWTs).

How it works

  • User enters their login credentials (=username + password)
  • Server verifies the credentials are correct and returns an encrypted and signed token with a private key.
    • Text: (JSON = {username: “abcd”, uuid=”1.2.3.4”}, private key) => token
    • Code: abccdeedddee
    • Decode: JSON = {username: “abcd”, uuid=”1.2.3.4”}
    • Symmetric vs Asymmetric encryption.
    • This token is stored client-side, most commonly in local storage - but can be stored in session storage or a cookie as well
  • Subsequent requests to the server include this token as an additional Authorization header or through one of the other methods mentioned above
  • The server decodes the JWT and if the token is valid processes the request
  • Once a user logs out, the token is destroyed client-side, no interaction with the server is necessary

Encryption using asymmetric keys.

5. Authentication-2019-5-1-17-30-10.png

Sign using asymmetric keys:

5. Authentication-2019-5-1-17-30-17.png

More reading (symmetric vs asymmetric encryption):
https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences

Advantages of Token-Based Authentication

Stateless, Scalable and Decoupled

  • Stateless: The back-end does not need to keep a record of tokens.
  • Self-contained, containing all the data required to check its validity..
  • No DB look up is needed.

Store Data in the JWT

  • With a cookie based approach, you simply store the session id in a cookie.
  • JWT’s on the other hand allow you to store any type of metadata, as long as it’s valid JSON. (username in our project, for example)

Mobile Friendly

  • Native mobile platforms and cookies do not mix well.
  • In our project, the same Go backend will serve traffic to both iOS and browser (React JS)

    Disadvantages of Token-Based Authentication

  • Usually the size of token is larger than a session id.

Auth0 and Golang

Source of codes: https://auth0.com/blog/authentication-in-golang/

JWT = JSON Web Toolkit

use jwt to protect post and search endpoints (reject if without auth token)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
// new libs
"github.com/auth0/go-jwt-middleware"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
)

// Other codes

var mySigningKey = []byte("secret")

func main() {
// Create a client
client, err := elastic.NewClient(elastic.SetURL(ES_URL), elastic.SetSniff(false))
….
fmt.Println("Started service successfully")
// Here we are instantiating the gorilla/mux router
r := mux.NewRouter()

var jwtMiddleware = jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
},
SigningMethod: jwt.SigningMethodHS256,
})

r.Handle("/post", jwtMiddleware.Handler(http.HandlerFunc(handlerPost))).Methods("POST")
r.Handle("/search", jwtMiddleware.Handler(http.HandlerFunc(handlerSearch))).Methods("GET")
r.Handle("/login", http.HandlerFunc(loginHandler)).Methods("POST")
r.Handle("/signup", http.HandlerFunc(signupHandler)).Methods("POST")

http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}

Let’s explain these codes line by line

1
r := mux.NewRouter()

Create a new router on top of the existing http router as we need to check auth.

1
2
3
4
5
6
var jwtMiddleware = jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
return mySigningKey, nil
},
SigningMethod: jwt.SigningMethodHS256,
})

Create a new JWT middleware with a Option that uses the key ‘mySigningKey’ such that we know this token is from our server. The signing method is the default HS256 algorithm such that data is encrypted.

1
r.Handle("/post", jwtMiddleware.Handler(http.HandlerFunc(handlerPost))).Methods("POST")

It means we use jwt middleware to manage these endpoints and if they don’t have valid token, we will reject them.

First handle the jwt, then chandle the normal http request

Question: if we reject it, what HttpResponse code we should return?
https://golang.org/src/net/http/status.go

Install new packages

1
2
3
go get "github.com/auth0/go-jwt-middleware"
go get "github.com/dgrijalva/jwt-go"
go get "github.com/gorilla/mux"

Under the same folder, add a new file called user.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
elastic "gopkg.in/olivere/elastic.v3"

"encoding/json"
"fmt"
"net/http"
"reflect"
"regexp"
"time"

"github.com/dgrijalva/jwt-go"
)

const (
TYPE_USER = "user"
)

var (
usernamePattern = regexp.MustCompile(`^[a-z0-9_]+$`).MatchString
)

type User struct {
Username string `json:"username"`
Password string `json:"password"`
Age int `json:"age"`
Gender string `json:"gender"`
}

Implement checkUser method in user.go.

What does this method do?

We need to check whether a pair of username and password is stored in ES.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// checkUser checks whether user is valid
func checkUser(username, password string) bool {
es_client, err := elastic.NewClient(elastic.SetURL(ES_URL), elastic.SetSniff(false))
if err != nil {
fmt.Printf("ES is not setup %v\n", err)
return false
}

// Search with a term query
termQuery := elastic.NewTermQuery("username", username)
queryResult, err := es_client.Search().
Index(INDEX).
Query(termQuery).
Pretty(true).
Do()
if err != nil {
fmt.Printf("ES query failed %v\n", err)
return false
}

var tyu User
for _, item := range queryResult.Each(reflect.TypeOf(tyu)) {
u := item.(User)
return u.Password == password && u.Username == username
}
// If no user exist, return false.
return false
}

Implement addUser method.

Student question

  1. After we send the term query, how do we know whether this user has existed?
  2. How to insert this user into ES?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Add a new user. Return true if successfully.
func addUser(user User) bool {
es_client, err := elastic.NewClient(elastic.SetURL(ES_URL), elastic.SetSniff(false))
if err != nil {
fmt.Printf("ES is not setup %v\n", err)
return false
}

termQuery := elastic.NewTermQuery("username", user.Username)
queryResult, err := es_client.Search().
Index(INDEX).
Query(termQuery).
Pretty(true).
Do()
if err != nil {
fmt.Printf("ES query failed %v\n", err)
return false
}

if queryResult.TotalHits() > 0 {
fmt.Printf("User %s already exists, cannot create duplicate user.\n", user.Username)
return false
}

_, err = es_client.Index().
Index(INDEX).
Type(TYPE_USER).
Id(user.Username).
BodyJson(user).
Refresh(true).
Do()
if err != nil {
fmt.Printf("ES save user failed %v\n", err)
return false
}

return true
}

Implement signupHandler method.

Student question: finish this method to support

  1. Decode a user from request (POST)
  2. Check whether username and password are empty, if any of them is empty, call http.Error(w, "Empty password or username", http.StatusInternalServerError)
  3. Otherwise, call addUser, if true, return a message “User added successfully”
  4. If else, call http.Error(w, “Failed to add a new user”, http.StatusInternalServerError)
  5. Set header to be w.Header().Set(“Content-Type”, “text/plain”) w.Header().Set(“Access-Control-Allow-Origin”, “*”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// If signup is successful, a new session is created.
func signupHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received one signup request")

decoder := json.NewDecoder(r.Body)
var u User
if err := decoder.Decode(&u); err != nil {
panic(err)
return
}

if u.Username != "" && u.Password != "" && usernamePattern(u.Username) {
if addUser(u) {
fmt.Println("User added successfully.")
w.Write([]byte("User added successfully."))
} else {
fmt.Println("Failed to add a new user.")
http.Error(w, "Failed to add a new user", http.StatusInternalServerError)
}
} else {
fmt.Println("Empty password or username.")
http.Error(w, "Empty password or username", http.StatusInternalServerError)
}

w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Access-Control-Allow-Origin", "*")
}

Implement loginHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// If login is successful, a new token is created.
func loginHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received one login request")

decoder := json.NewDecoder(r.Body)
var u User
if err := decoder.Decode(&u); err != nil {
panic(err)
return
}

if checkUser(u.Username, u.Password) {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
/* Set token claims */
claims["username"] = u.Username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

/* Sign the token with our secret */
tokenString, _ := token.SignedString(mySigningKey)

/* Finally, write the token to the browser window */
w.Write([]byte(tokenString))
} else {
fmt.Println("Invalid password or username.")
http.Error(w, "Invalid password or username", http.StatusForbidden)
}

w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Access-Control-Allow-Origin", "*")
}

Let’s explain the codes

1
2
3
4
5
6
decoder := json.NewDecoder(r.Body)
var u User
if err := decoder.Decode(&u); err != nil {
panic(err)
return
}

Decode a user from request’s body

1
if checkUser(u.Username, u.Password) {

Make sure user credential is correct.

1
token := jwt.New(jwt.SigningMethodHS256)

Create a new token object to store.

1
claims := token.Claims.(jwt.MapClaims)

Convert it into a map for lookup

1
2
claims["username"] = u.Username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

Store username and expiration into it.

1
tokenString, _ := token.SignedString(mySigningKey)

Sign (Encrypt) and token such that only server knows it.

1
w.Write([]byte(tokenString))

Write it into response

Update handlerPost in main.go to populate username

Question: why use the username in context? Why not ask user to send it as param?

Answer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func handlerPost(w http.ResponseWriter, r *http.Request) {
// other codes
user := r.Context().Value("user")
claims := user.(*jwt.Token).Claims
username := claims.(jwt.MapClaims)["username"]

// 32 << 20 is the maxMemory param for ParseMultipartForm
// After you call ParseMultipartForm, the file will be saved in the server memory with maxMemory size.
// If the file size is larger than maxMemory, the rest of the data will be saved in a system temporary file.
r.ParseMultipartForm(32 << 20)

// Parse from form data.
fmt.Printf("Received one post request %s\n", r.FormValue("message"))
lat, _ := strconv.ParseFloat(r.FormValue("lat"), 64)
lon, _ := strconv.ParseFloat(r.FormValue("lon"), 64)
p := &Post{
User: username.(string),
Message: r.FormValue("message"),
Location: Location{
Lat: lat,
Lon: lon,
},
}

...
}

Test

Local test

In the same folder where you have main.go and user.go

1
go run *.go

In windows, execute go run main.go users.go

Open Postman and enter ‘http://localhost:8080/signup’ in the url, in the Body add a new json object as

1
2
3
4
5
6
{
"username":"jack",
"password":"ABCD",
"age":16,
"gender":"female"
}

Click Send and you should see a message like ‘User added successfully’.

Change the url to be ‘http://localhost:8080/login’, and in the body use the same json object.

It will return a token as response copy it

Post

change the url to ‘http://localhost:8080/post’ and then in the header add a new key value with a key as ‘Authorization’ and a value as ‘Bearer YOUR_TOKEN’. In the body, still add related input params and an image file.

bearer is also fine

5. Authentication-2019-5-1-15-39-33.png

Click Send and make it works.

在这里elastic search默认只显示10条结果,需要在search handler那里修改一下结果数量。

5. Authentication-2019-5-1-17-21-39.png

change the url to ‘http://localhost:8080/search?lat=37.5&lon=-120.5&range=200’ and the method to be GET. Then in the Headers, similarly, add a new Authorization key value pair if not there.

Click send and you should get the same results as last time.
You can also verify the signed content on:
https://jwt.io/

Remote test (Homework)

Deploy a new instance in GCloud and then test it again

REMEMBER: Install new packages on Google Cloud Shell!
Note: some new package may need to be installed on Google cloud shell:

1
2
3
go get github.com/googleapis/gax-go
go get google.golang.org/api/googleapi
go get go.opencensus.io/trace

Homework

  1. Try to send some random string and see what’s the response
  2. Why we don’t protect the two endpoints of ‘login’ and ‘signup’?
  3. How to protect credentials from man-in-the-middle attack?