S3 Media Server Login 2

Authentication Backend

When we left off last time, we had retrieved a JWT token from Google for a given user, and our front-end was sending it to our backend for authentication. That’s where we will start today.

Authentication Endpoints

The endpoint used to authenticate must be unprotected, meaning it’s available to a user that isn’t logged in. As mentioned last time, our authentication endpoints are not protected to allow this. The relevant mux setup code is as follows:

userDb.HandleFunc("/authenticate", authenticateHandler).Methods("POST")

Google Response

The response from Google looks something like this:

{
    "token_type":"Bearer",
    "access_token":"ya29.Gl2SBg9pTi1_kE3_Yrs3c51rIoO_9tYtsP2gnxK76JzyVOCFc78Xm248ZlatqfxpU32tZQpVou4C5sp2OASGKvXR15PiygO0pBdpTI7y2UiS1t4faSyjCens87qkXXo",
    "scope":"openid email profile https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
    "login_hint":"AJDLj6LA--0a1msOjtiNFtVQfjsLCVocgDhlDL4kDcZN0xXz5nxMetJMiLrWH6-a2FOzeUiQ7l0IogBK5YONEVSBpoutevKMOA",
    "expires_in":3600,
    "id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjhhYWQ2NmJkZWZjMWI0M2Q4ZGIyN2U2NWUyZTJlZjMwMTg3OWQzZTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiOTMyMzA4Mzk3NDAzLW4xdGNyZW42cWhzYWMyN2hzNWU2Z2M5aDJuMXRsZ2diLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiOTMyMzA4Mzk3NDAzLW4xdGNyZW42cWhzYWMyN2hzNWU2Z2M5aDJuMXRsZ2diLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTE3Mjg1NzYxOTg2OTA3MjMwNDgwIiwiZW1haWwiOiJqb25hdGhhbm1hcmtmaXNoZXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJ4ZUZLMmpmbWRPYWNUZ0xUUW5KMm1nIiwibmFtZSI6IkpvbmF0aGFuIEZpc2hlciIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vLWhnRG5mTlRjRzhBL0FBQUFBQUFBQUFJL0FBQUFBQUFBQUY4L19Rbk5JcllaeVdVL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJKb25hdGhhbiIsImZhbWlseV9uYW1lIjoiRmlzaGVyIiwibG9jYWxlIjoiZW4iLCJpYXQiOjE1NDc1MTM5NjUsImV4cCI6MTU0NzUxNzU2NSwianRpIjoiMjM2NjA3M2U3ZTAyMzM4YWEwNTI3YWJjYjJjMzdjODRmNWEwY2EwYiJ9.UJylxpoPV11Uba48_G2RSwUkrBhByMcflKxC72E61rnGYjYDDUKYrEaIsBKfKccaCge49h_Sq-lnMxpDWvc58ECaiAyF1Cd_mAk9XlKTuuWf_AhpwLi30QlsRplXpGm5bGa3f7L3it0Tfz1dg59Uvv5DOW2FXKhxbVw9wmxqmpQAWV9DSL7OC20g8HTXbnWrJcEu5SMDcBr484LgL7fd-q53rUq_qXaQ4K6mTUmf0_viFYOyy9e4ratdHfxdDWq7x0QBZBXcwBGJIlTJS-v__iNgWjIQtsihWg8YDMf45Pt-cGKXTY6y9QoW5rSIb_JnvHPDXfvheZaYMHe7bI36oA",
    "session_state": {
        "extraQueryParams": {
            "authuser":"0"}
        },
    "first_issued_at":1547513967683,
    "expires_at":1547517567683,
    "idpId":"google"
}

Now, what do we do with this? I actually found it fairly tricky to figure out how to deal with this. There are several libraries out there, and they all do similar-but-not-the-same stuff. At a high level, I know that I need to authenticate this JWT (to ensure that it came from Google), and to decode the email address that is contained within it.

Through mostly trial-and-error, I found that I could use the google.golang.org/api/oauth2/v2 package for what I need. Ultimately I ended up using the following function for validating an access token:

func authenticateGoogleAccessToken(accessToken string) bool {
	var httpClient = &http.Client{}
	oauth2Service, err := oauth2.New(httpClient)
	if err != nil {
		log.Fatalf("Failed to create oauth2 client: %v", err)
	}
	tokenInfoCall := oauth2Service.Tokeninfo()

	tokenInfoCall.AccessToken(accessToken)
	if tokenInfo, err := tokenInfoCall.Do(); err != nil {
		log.Printf("Failed to id token: %v", err)
		return false
	} else {
		// The token from Google is valid, now check to see whether this user is one we want to
		// give access to. For now, I am hard-coding my own email address.
		if tokenInfo.Email == "[email protected]" {
			return true
		}
	}

	return false
}

The input to this function (accessToken) is the value of the access_token field in the JWT blob above. What I found confusing is that if I change the tokenInfoCall.AccessToken(accessToken) line to tokenInfoCall.IdToken(accessToken) and use the value of the id_token field, the verification also succeeds. I believe what is happening is that the accessToken value is stored by Google and when I make a call to their API with that value, I am returned the full decoded token associated with that value. If I use the id_token, I believe that that contains the full information of the token, and the code simply checks the signature of it against Google’s public key.

At some point I’d like to look more at this, so I’ll make a note of it and move on.

Session Token

Once we’ve validated the user, it is time to generate a session token for the browser. Instead of simply re-using the Google token, I am choosing to generate my own bearer token, mostly because I want to standardize the bearer token across login methods (e.g. Google and FIDO).

Creating this token is fairly straight forward; I’m not encoding any real information outside of the time the token expires. I somewhat arbitrarily picked 24 hours as the time that a session can last, and wrote this function:

func createBearerToken() (string, error) {
	claims := &jwt.StandardClaims{
		ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
		Issuer: "host",
	}
	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
	if tokenString, err := token.SignedString(JwtPrivateKey); err != nil {
		log.Printf("error creating jwt bearer token: %v", err)
		return "", err
	} else {
		return tokenString, nil
	}
}

Returning the session token to the browser

Ok, now let’s put it together. The following is the endpoint handler for the Google Authentication path. This function reads the body of the HTTP POST (which is expected to be the JWT access token), checks that it is authentic (i.e. from Google), and issues a bearer token for the browser to use for future access.

func authenticateHandler(w http.ResponseWriter, r *http.Request) {
	raw, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w,"Invalid request, missing body", http.StatusBadRequest)
		return
	}

	if !authenticateGoogleAccessToken(string(raw)) {
		http.Error(w, "Unable to authenticate token", http.StatusForbidden)
		return
	}

	// Return a JWT bearer token.
	bearerToken, err := createBearerToken()
	if err != nil {
		http.Error(w, "error generating bearer token", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/jwt")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, bearerToken)
}

Authenticating the Bearer Token

While we’re on the back end of things, let’s take a peek at the code for validating this token when the browser hits an authenticated endpoint using the bearer token:

// Set up the JWT middleware which is responsible for verifying the session bearer token supplied by the user.
jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func (token *jwt.Token) (interface{}, error) {
        if JwtPrivateKey == nil {
            log.Printf("Loading jwt private key...")
            if err := loadJwtSigningKey(); err != nil {
                return nil, err
            }
        }

        return JwtPrivateKey.Public(), nil
    },
    SigningMethod: jwt.SigningMethodES256,
    ErrorHandler: authorizationFailure,
})

This is a surprisingly small bit of code, but basically this is the magic of middleware in Go. Since I have a pretty simple setup, the code is rather small. Essentially I want to check the requests for my authenticated endpoints for the Bearer token, and validate that it was signed by my private key (stored in JwtPrivateKey). If it isn’t, the ErrorHandler function is called, otherwise the Bearer token is decrypted and stored in the request variable for further validation in the endpoint handler:

user := r.Context().Value("user").(*jwt.Token)

if err := user.Claims.Valid(); err != nil {
    http.Error(w, "invalid bearer token", http.StatusForbidden)
    return
}

Summary

This time I added the code for authenticating the Google OAUTH token and creating a session token to be used by the client for authenticating calls to protected endpoints. Next time I’ll probably take a pass at the front-end code to get an end-to-end example. After that, it’s on to the U2F frontier!