Currently, a large part of authentication flows is based on generating a token, which can, for example, use the JWT standard. The frontend then makes requests informing the backend who the user is that is actually making the call. You can see this when the frontend sends the Authorization header in its requests.
It’s common for this token to contain user information, like their ID, for example. So when it receives the request, the backend decodes the token to extract this information and then link it to a user in the database. With the user in hand, we execute the desired action. Below, I’ll give you an example of a Go service that does exactly this.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| package service
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
http.HandleFunc("/", getEmail)
fmt.Println("Listening on :8081")
err := http.ListenAndServe(":8081", nil)
if err != nil {
fmt.Println(err)
}
}
func getEmail(w http.ResponseWriter, r *http.Request) {
// example from https://pkg.go.dev/github.com/golang-jwt/jwt/v4@v4.4.2
authHeader := r.Header.Get("Authorization")
tokenStr, err := getToken(authHeader)
if err != nil {
fmt.Println(err)
}
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte("my_secret_key"), nil
})
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
fmt.Println(err)
}
// get the user from the ID
// user := users.GetUserById(claims["user_id"])
userID := claims["user_id"].(string)
user := User{ID: userID, Name: "Matheus Mina", Email: "mfbmina@gmail.com"}
userJSON, _ := json.Marshal(user)
w.Write(userJSON)
}
func getToken(tokenStr string) (string, error) {
authHeaderParts := strings.Fields(tokenStr)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("Not valid")
}
return authHeaderParts[1], nil
}
|
This flow works great for monoliths, but not so much for micro-services. The problem is that when you move to a micro-services environment, this logic for opening the token has to be replicated for each new service. If the token format changes, all the micro-services will have to be updated to follow the new token standard.
Enriching requests
To avoid this problem, we can do something called request enrichment. This involves adding more information to the original request, giving more context and information to the backend. A service that does this, for example, is Cloudflare, which adds some headers to your request. To perform this enrichment, we can place an intermediary application to open the token and put the request into the headers of the other responses.
A very simple way to do this is to use the middleware mechanisms of Traefik. It’s a reverse proxy and load balancer that allows us to easily route between our micro-services. Plus, it’s open-source and written in Go. Using the middleware idea with Traefik, our architecture would look like this:
Pretty cool, right? To sum it all up, the request cycle will work like this:
- The user makes a request to the backend.
- Traefik receives the request and holds the original request.
- Traefik makes a new request to the middleware.
- Traefik gets the response from the middleware and adds the configured header to the original request.
- Traefik forwards the original request to the backend service.
- The service responds to the user.
Building the solution
Getting our hands dirty, let’s configure our service in Traefik to receive and forward calls to our service.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| entryPoints:
web:
# Listen on port 8081 for incoming requests
address: :8081
providers:
# Enable the file provider to define routers / middlewares / services in file
file:
directory: /path/to/dynamic/conf
# dynamic config below
http:
routers:
# Define a connection between requests and services
user-service:
rule: "Path(`/users`)"
service: user-service
services:
# Define how to reach an existing service on our infrastructure
user-service:
loadBalancer:
servers:
- url: http://private/user-service
|
With this configuration, every request to /users will go to our UserService. The idea here is to add a middleware in between, so that it’s transparent to the user that the token is being opened. To do this, we’ll create another micro-service whose sole responsibility is to open this token and enrich this request.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| package middleware
import (
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/golang-jwt/jwt"
)
func main() {
http.HandleFunc("/", setHeaderExample)
fmt.Println("Listening on :8082")
err := http.ListenAndServe(":8082", nil)
if err != nil {
log.Fatal(err)
}
}
func setHeaderExample(w http.ResponseWriter, r *http.Request) {
// example from https://pkg.go.dev/github.com/golang-jwt/jwt/v4@v4.4.2
authHeader := r.Header.Get("Authorization")
tokenStr, err := getToken(authHeader)
if err != nil {
fmt.Println(err)
}
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte("my_secret_key"), nil
})
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
fmt.Println(err)
}
// Setting the header X-User-Id"
userID := claims["user_id"].(string)
w.Header().Add("X-User-Id", userID)
w.Write([]byte("This response has the X-User-Id header"))
}
func getToken(tokenStr string) (string, error) {
authHeaderParts := strings.Fields(tokenStr)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("Not valid")
}
return authHeaderParts[1], nil
}
|
Now, we need to add it as a middleware in Traefik:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| http:
services:
# Define how to reach an existing service on our infrastructure
user-middleware:
loadBalancer:
servers:
- url: http://private/user-middleware
middlewares:
user-middleware:
forwardAuth:
address: "http://private/user-middleware"
authResponseHeaders:
- "X-User-ID"
|
We also need to tell the UserService to use the middleware:
1
2
3
4
5
6
7
8
| http:
routers:
# Define a connection between requests and services
user-service:
rule: "Path(`/users`)"
service: user-service
middlewares:
- user-middleware
|
The full configuration looks like this:
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
| entryPoints:
web:
# Listen on port 8081 for incoming requests
address: :8081
providers:
# Enable the file provider to define routers / middlewares / services in file
file:
directory: /path/to/dynamic/conf
# dynamic config below
http:
routers:
# Define a connection between requests and services
user-service:
rule: "Path(`/users`)"
service: user-service
middlewares:
- user-middleware
middlewares:
user-middleware:
forwardAuth:
address: "http://private/user-middleware"
authResponseHeaders:
- "X-User-ID"
services:
# Define how to reach an existing service on our infrastructure
user-service:
loadBalancer:
servers:
- url: http://private/user-service
user-middleware:
loadBalancer:
servers:
- url: http://private/user-middleware
|
And that’s it! The magic is working! All requests to the UserService will now have the X-User-Id header. To finish up, we just need to remove the code that “opens” the token and start reading the information from the header instead. Our handler would look like this:
1
2
3
4
5
6
7
8
| func getEmailFinal(w http.ResponseWriter, r *http.Request) {
// get the user from the ID
// user := users.GetUserById(claims["user_id"])
userID := r.Header.Get("X-User-Id")
user := User{ID: userID, Name: "Matheus Mina", Email: "mfbmina@gmail.com"}
userJSON, _ := json.Marshal(user)
w.Write(userJSON)
}
|
Conclusion
By enriching the request, we can simplify our service code, transparently passing useful information to the backend. As you can see, our service’s handler is much cleaner, focusing only on what it should actually be doing.
If you liked this post, you can also find me on Twitter, Github, or LinkedIn.