-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add client implementation for doing validation
- Loading branch information
1 parent
30363f9
commit 387077e
Showing
1 changed file
with
172 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
validateURI = "/api/v3/internal/validate" | ||
authHeader = "Authorization" | ||
UserDataHeader = "X-User-Data" | ||
fingerprintHeader = "X-Fingerprint" | ||
featureFlagsHeader = "X-Feature-Flags" | ||
serviceNameHeader = "X-Service-Name" | ||
|
||
modeQueryParam = "mode" | ||
fingerprintQueryParam = "fingerprint" | ||
checksumQueryParam = "x-upstream-name" | ||
subrequestQueryParam = "subrequest" | ||
) | ||
|
||
type Client struct { | ||
baseURL string | ||
client *http.Client | ||
timeout time.Duration | ||
isOptional bool | ||
} | ||
|
||
type Payload struct { | ||
UserData UserData | ||
} | ||
|
||
type UserData struct { | ||
IAT int `json:"iat"` | ||
Aud string `json:"aud"` | ||
Iss int `json:"iss"` | ||
Sub string `json:"sub"` | ||
UserID int `json:"user_id"` | ||
Email string `json:"email"` | ||
Exp int `json:"exp"` | ||
Locale string `json:"locale"` | ||
} | ||
|
||
// New creates a new Client with default attributes. | ||
func New(url string, timeout time.Duration) Client { | ||
return Client{ | ||
baseURL: url, | ||
client: new(http.Client), | ||
timeout: timeout, | ||
isOptional: false, | ||
} | ||
} | ||
|
||
// WithOptionalValidate enables you to bypass the signature validation. | ||
// If the validation of the JWT signature is optional for you, and you just want to extract | ||
// the payload from the token, you can use the client in `WithOptionalValidate` mode. | ||
func (c *Client) WithOptionalValidate() { | ||
c.isOptional = true | ||
} | ||
|
||
// Validate gets the parent context, headers, and JWT token and calls the validate API of the JWT validator service. | ||
// The parent context is helpful in canceling the process in the upper hand (a function that used the SDK) and in case | ||
// you have something like tracing spans in your context and want to extend these things in your custom HTTP handler. | ||
// Otherwise, you can use `context.Background()`. | ||
// The headers argument is used when you want to pass some headers like user-agent, | ||
// X-Service-Name, X-App-Name, X-App-Version and | ||
// X-App-Version-Code to the validator. It is extremely recommended to pass these headers (if you have them) because | ||
// it increases the visibility in the logs and metrics of the JWT Validator service. | ||
// You must place your Authorization header content in the bearerToken argument. | ||
// Consider that the bearerToken must contain Bearer keyword and JWT. | ||
// For `X-Service-Name` you should put your project/service name in this header. | ||
func (c *Client) Validate(parentCtx context.Context, headers http.Header, bearerToken string) (*Payload, error) { | ||
if headers.Get(serviceNameHeader) == "" { | ||
return nil, errors.New("x-service-name can not be empty") | ||
} | ||
|
||
segments := strings.Split(bearerToken, " ") | ||
if len(segments) < 2 || strings.ToLower(segments[0]) != "bearer" { | ||
return nil, errors.New("invalid jwt") | ||
} | ||
|
||
ctx, cancel := context.WithTimeout(parentCtx, c.timeout) | ||
defer cancel() | ||
|
||
url := c.baseURL + validateURI | ||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
request.Header = headers | ||
request.Header.Set(authHeader, bearerToken) | ||
|
||
query := request.URL.Query() | ||
if c.isOptional { | ||
query.Add(modeQueryParam, "optional") | ||
} | ||
|
||
request.URL.RawQuery = query.Encode() | ||
|
||
response, err := c.client.Do(request) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
closeBody(response) | ||
|
||
if response.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("invalid token: %s", response.Status) | ||
} | ||
|
||
userDataHeader := response.Header.Get(UserDataHeader) | ||
if userDataHeader == "" { | ||
return nil, fmt.Errorf("invalid X-User-Data header") | ||
} | ||
|
||
userData := map[string]interface{}{} | ||
|
||
if err := json.Unmarshal([]byte(userDataHeader), &userData); err != nil { | ||
return nil, fmt.Errorf("X-User-Data header unmarshal failed: %s", err) | ||
} | ||
|
||
payload := &Payload{UserData: UserData{}} | ||
if iat, ok := userData["iat"].(float64); ok { | ||
payload.UserData.IAT = int(iat) | ||
} | ||
|
||
if aud, ok := userData["aud"].(string); ok { | ||
payload.UserData.Aud = aud | ||
} | ||
|
||
if iss, ok := userData["iss"].(float64); ok { | ||
payload.UserData.Iss = int(iss) | ||
} | ||
|
||
if sub, ok := userData["sub"].(string); ok { | ||
payload.UserData.Sub = sub | ||
} | ||
|
||
if userID, ok := userData["user_id"].(float64); ok { | ||
payload.UserData.UserID = int(userID) | ||
} | ||
|
||
if email, ok := userData["email"].(string); ok { | ||
payload.UserData.Email = email | ||
} | ||
|
||
if exp, ok := userData["exp"].(float64); ok { | ||
payload.UserData.Exp = int(exp) | ||
} | ||
|
||
if locale, ok := userData["locale"].(string); ok { | ||
payload.UserData.Locale = locale | ||
} | ||
|
||
return payload, nil | ||
} | ||
|
||
// closeBody to avoid memory leak when reusing http connection. | ||
func closeBody(response *http.Response) { | ||
if response != nil { | ||
_, _ = io.Copy(io.Discard, response.Body) | ||
_ = response.Body.Close() | ||
} | ||
} |