Skip to content

Commit 387077e

Browse files
committed
feat: add client implementation for doing validation
1 parent 30363f9 commit 387077e

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

pkg/validator/client.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"strings"
11+
"time"
12+
)
13+
14+
const (
15+
validateURI = "/api/v3/internal/validate"
16+
authHeader = "Authorization"
17+
UserDataHeader = "X-User-Data"
18+
fingerprintHeader = "X-Fingerprint"
19+
featureFlagsHeader = "X-Feature-Flags"
20+
serviceNameHeader = "X-Service-Name"
21+
22+
modeQueryParam = "mode"
23+
fingerprintQueryParam = "fingerprint"
24+
checksumQueryParam = "x-upstream-name"
25+
subrequestQueryParam = "subrequest"
26+
)
27+
28+
type Client struct {
29+
baseURL string
30+
client *http.Client
31+
timeout time.Duration
32+
isOptional bool
33+
}
34+
35+
type Payload struct {
36+
UserData UserData
37+
}
38+
39+
type UserData struct {
40+
IAT int `json:"iat"`
41+
Aud string `json:"aud"`
42+
Iss int `json:"iss"`
43+
Sub string `json:"sub"`
44+
UserID int `json:"user_id"`
45+
Email string `json:"email"`
46+
Exp int `json:"exp"`
47+
Locale string `json:"locale"`
48+
}
49+
50+
// New creates a new Client with default attributes.
51+
func New(url string, timeout time.Duration) Client {
52+
return Client{
53+
baseURL: url,
54+
client: new(http.Client),
55+
timeout: timeout,
56+
isOptional: false,
57+
}
58+
}
59+
60+
// WithOptionalValidate enables you to bypass the signature validation.
61+
// If the validation of the JWT signature is optional for you, and you just want to extract
62+
// the payload from the token, you can use the client in `WithOptionalValidate` mode.
63+
func (c *Client) WithOptionalValidate() {
64+
c.isOptional = true
65+
}
66+
67+
// Validate gets the parent context, headers, and JWT token and calls the validate API of the JWT validator service.
68+
// The parent context is helpful in canceling the process in the upper hand (a function that used the SDK) and in case
69+
// you have something like tracing spans in your context and want to extend these things in your custom HTTP handler.
70+
// Otherwise, you can use `context.Background()`.
71+
// The headers argument is used when you want to pass some headers like user-agent,
72+
// X-Service-Name, X-App-Name, X-App-Version and
73+
// X-App-Version-Code to the validator. It is extremely recommended to pass these headers (if you have them) because
74+
// it increases the visibility in the logs and metrics of the JWT Validator service.
75+
// You must place your Authorization header content in the bearerToken argument.
76+
// Consider that the bearerToken must contain Bearer keyword and JWT.
77+
// For `X-Service-Name` you should put your project/service name in this header.
78+
func (c *Client) Validate(parentCtx context.Context, headers http.Header, bearerToken string) (*Payload, error) {
79+
if headers.Get(serviceNameHeader) == "" {
80+
return nil, errors.New("x-service-name can not be empty")
81+
}
82+
83+
segments := strings.Split(bearerToken, " ")
84+
if len(segments) < 2 || strings.ToLower(segments[0]) != "bearer" {
85+
return nil, errors.New("invalid jwt")
86+
}
87+
88+
ctx, cancel := context.WithTimeout(parentCtx, c.timeout)
89+
defer cancel()
90+
91+
url := c.baseURL + validateURI
92+
93+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
request.Header = headers
99+
request.Header.Set(authHeader, bearerToken)
100+
101+
query := request.URL.Query()
102+
if c.isOptional {
103+
query.Add(modeQueryParam, "optional")
104+
}
105+
106+
request.URL.RawQuery = query.Encode()
107+
108+
response, err := c.client.Do(request)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
closeBody(response)
114+
115+
if response.StatusCode != http.StatusOK {
116+
return nil, fmt.Errorf("invalid token: %s", response.Status)
117+
}
118+
119+
userDataHeader := response.Header.Get(UserDataHeader)
120+
if userDataHeader == "" {
121+
return nil, fmt.Errorf("invalid X-User-Data header")
122+
}
123+
124+
userData := map[string]interface{}{}
125+
126+
if err := json.Unmarshal([]byte(userDataHeader), &userData); err != nil {
127+
return nil, fmt.Errorf("X-User-Data header unmarshal failed: %s", err)
128+
}
129+
130+
payload := &Payload{UserData: UserData{}}
131+
if iat, ok := userData["iat"].(float64); ok {
132+
payload.UserData.IAT = int(iat)
133+
}
134+
135+
if aud, ok := userData["aud"].(string); ok {
136+
payload.UserData.Aud = aud
137+
}
138+
139+
if iss, ok := userData["iss"].(float64); ok {
140+
payload.UserData.Iss = int(iss)
141+
}
142+
143+
if sub, ok := userData["sub"].(string); ok {
144+
payload.UserData.Sub = sub
145+
}
146+
147+
if userID, ok := userData["user_id"].(float64); ok {
148+
payload.UserData.UserID = int(userID)
149+
}
150+
151+
if email, ok := userData["email"].(string); ok {
152+
payload.UserData.Email = email
153+
}
154+
155+
if exp, ok := userData["exp"].(float64); ok {
156+
payload.UserData.Exp = int(exp)
157+
}
158+
159+
if locale, ok := userData["locale"].(string); ok {
160+
payload.UserData.Locale = locale
161+
}
162+
163+
return payload, nil
164+
}
165+
166+
// closeBody to avoid memory leak when reusing http connection.
167+
func closeBody(response *http.Response) {
168+
if response != nil {
169+
_, _ = io.Copy(io.Discard, response.Body)
170+
_ = response.Body.Close()
171+
}
172+
}

0 commit comments

Comments
 (0)