-
Notifications
You must be signed in to change notification settings - Fork 454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conditional backend request skipping based on JWT type #928
Comments
I managed to solve this issue by writing a custom plugin for KrakenD. But I updated the logic: jti is now passed in the request body instead of the URL path. Also I implemented a mechanism to reject access tokens that were issued before KrakenD was started. Everything works as I expected. Maybe it will be useful to someone. I will leave the code below. main.go package main
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
func main() {}
// ModifierRegisterer registers the plugin
var ModifierRegisterer = registerer("krakend-tokens-validation")
var errUnauthorized = HTTPResponseError{
Code: http.StatusUnauthorized,
Msg: "",
HTTPEncoding: "application/json",
}
var logger Logger = nil
type registerer string
var serverStartupTime int64
func (r registerer) RegisterModifiers(f func(
name string,
modifierFactory func(map[string]interface{}) func(interface{}) (interface{}, error),
appliesToRequest bool,
appliesToResponse bool,
)) {
f(string(r), r.modifierFactory, true, false)
}
func (r registerer) RegisterLogger(in interface{}) {
serverStartupTime = time.Now().Unix()
l, ok := in.(Logger)
if !ok {
return
}
logger = l
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Logger loaded", ModifierRegisterer))
}
func (r registerer) modifierFactory(config map[string]interface{}) func(interface{}) (interface{}, error) {
validatorUrl, ok := config["api-tokens-validator-url"].(string)
if !ok {
if logger != nil {
logger.Error(fmt.Sprintf("[PLUGIN: %s] api-tokens-validator-url not provided in config", ModifierRegisterer))
}
return nil
}
verifyApiTokens, ok := config["verify-api-tokens"].(bool)
if !ok {
if logger != nil {
logger.Error(fmt.Sprintf("[PLUGIN: %s] verify-api-tokens not provided in config", ModifierRegisterer))
}
return nil
}
return func(input interface{}) (interface{}, error) {
req, ok := input.(RequestWrapper)
if !ok {
return nil, errors.New("unknown request type")
}
headers := req.Headers()
// Extract X-Token-Iat header
tokenIat := getHeader(headers, "X-Token-Iat")
if tokenIat == "" {
if logger != nil {
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Token rejected: iat is missing", ModifierRegisterer))
}
return nil, errUnauthorized
}
// Convert X-Token-Iat to Unix timestamp and compare it with server startup time
tokenIatTime, err := strconv.ParseInt(tokenIat, 10, 64)
if err != nil {
if logger != nil {
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Token rejected: invalid iat", ModifierRegisterer))
}
return nil, errUnauthorized
}
// Extract X-Token-Jti header
tokenJti := getHeader(headers, "X-Token-Jti")
if tokenJti == "" {
if logger != nil {
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Token rejected: jti is missing", ModifierRegisterer))
}
return nil, errUnauthorized
}
// Check for X-Token-Type header
tokenType := getHeader(headers, "X-Token-Type")
if tokenType != "API" && tokenType != "" {
// Reject ACCESS tokens issued before server startup time
if tokenType == "ACCESS" && tokenIatTime < serverStartupTime {
if logger != nil {
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Token rejected: issued before server startup", ModifierRegisterer))
}
return nil, errUnauthorized
}
tokenExp := getHeader(headers, "X-Token-Exp")
if tokenExp == "" {
if logger != nil {
logger.Debug(fmt.Sprintf("[PLUGIN: %s] Token rejected: expiration date is missing", ModifierRegisterer))
}
return nil, errUnauthorized
}
return req, nil
}
if !verifyApiTokens {
return req, nil
}
// Validate the API token JTI by sending it to the auth server
if err := validateTokenJti(validatorUrl, tokenJti); err != nil {
return nil, err
}
// If validation succeeds, continue with the request
return req, nil
}
}
// Helper function to get a specific header from the request
func getHeader(headers map[string][]string, key string) string {
if values, ok := headers[key]; ok && len(values) > 0 {
return values[0]
}
return ""
}
// Helper function to validate the token JTI with the auth server via POST
func validateTokenJti(validatorUrl, tokenJti string) error {
// Create the JSON payload with the JTI
jsonBody := fmt.Sprintf(`{"jti": "%s"}`, tokenJti)
bodyReader := strings.NewReader(jsonBody)
// Create a new HTTP POST request
req, err := http.NewRequest("POST", validatorUrl, bodyReader)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
// Initialize the HTTP client and send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// If the auth server returns 200, the token is valid
if resp.StatusCode == http.StatusOK {
return nil
}
// If the status is 403, return a custom error with the body message
if resp.StatusCode == http.StatusForbidden {
// Read the response body
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
bodyString := string(bodyBytes)
return HTTPResponseError{
Code: http.StatusForbidden,
Msg: bodyString,
HTTPEncoding: "application/json",
}
}
// Any other responses
return errUnauthorized
}
// RequestWrapper interface to work with requests
type RequestWrapper interface {
Params() map[string]string
Headers() map[string][]string
Body() io.ReadCloser
Method() string
URL() *url.URL
Query() url.Values
Path() string
}
// Logger interface for logging
type Logger interface {
Debug(v ...interface{})
Info(v ...interface{})
Warning(v ...interface{})
Error(v ...interface{})
Critical(v ...interface{})
Fatal(v ...interface{})
}
type HTTPResponseError struct {
Code int `json:"http_status_code"`
Msg string `json:"http_body,omitempty"`
HTTPEncoding string `json:"http_encoding"`
}
// Error returns the error message
func (r HTTPResponseError) Error() string {
return r.Msg
}
// StatusCode returns the status code returned by the backend
func (r HTTPResponseError) StatusCode() int {
return r.Code
}
// Encoding returns the HTTP output encoding
func (r HTTPResponseError) Encoding() string {
return r.HTTPEncoding
} go.mod module krakend-api-tokens-validation
go 1.22.7 And parts of config.json: {
"$schema": "https://www.krakend.io/schema/v3.json",
"version": 3,
"name": "Test API",
"port": 8080,
"timeout": "5s",
"cache_ttl": "300s",
"plugin": {
"pattern": ".so",
"folder": "/opt/krakend/plugins"
},
... {
"endpoint": "/users/me",
"input_headers": [
"Content-Type",
"Cookie",
"Authorization",
"x-user",
"x-token-limits",
"x-token-type",
"x-token-jti",
"x-token-iat",
"x-token-exp"
],
"method": "GET",
"output_encoding": "no-op",
"backend": [
{
"url_pattern": "/users/me",
"encoding": "no-op",
"method": "GET",
"input_headers": [
"Content-Type",
"x-user",
"x-token-limits",
"x-token-type"
],
"extra_config": {
"qos/ratelimit/proxy": {
"max_rate": 120,
"capacity": 50,
"every": "1m"
}
},
"host": ["http://192.168.0.103:3001"]
}
],
"extra_config": {
"auth/validator": {
"alg": "RS256",
"jwk_url": "http://192.168.0.103:3000/auth/jwks",
"cookie_key": "accessToken",
"cache": true,
"disable_jwk_security": true,
"propagate_claims": [
["userId", "x-user"],
["limits", "x-token-limits"],
["type", "x-token-type"],
["jti", "x-token-jti"],
["iat", "x-token-iat"],
["exp", "x-token-exp"]
]
},
"validation/cel": [
{
"check_expr": "has(JWT.userId) && has(JWT.limits) && has(JWT.iat) && JWT.type in ['ACCESS', 'API']"
},
{
"check_expr": "(JWT.type == 'ACCESS' && has(JWT.exp)) || JWT.limits in ['READ_ONLY', 'DEFAULT']"
}
],
"plugin/req-resp-modifier": {
"name": ["krakend-tokens-validation"],
"api-tokens-validator-url": "http://192.168.0.103:3000/auth/api-tokens/validate",
"verify-api-tokens": true
}
}
} Build plugin command:
|
Environment info:
Describe what are you trying to do:
I have two types of JWT tokens:
ACCESS
andAPI
. The token type is stored as a string in a claim namedtype
. My goal is to apply conditional logic based on this type before forwarding the request to the backend:API
, I want to validate the token by calling an authentication microservice (/auth/api-tokens/validate/{JWT.jti}
) and after that send request to target endpoint.ACCESS
, I want to skip the validation step and send the request directly to the target endpoint.I need a way to skip one backend request (the token validation) based on the JWT type. I'm unsure if this is possible in KrakenD.
I created the following configuration, but it always sends requests to the
/auth/api-tokens/validate/{JWT.jti}
endpoint, even for access tokens. I would like to bypass this validation for access tokens and send the request directly to the second backend if the token type is access.Is there a way to conditionally skip the first backend (the token validation) if the JWT type is
ACCESS
? If so, how can I configure KrakenD to do this?The text was updated successfully, but these errors were encountered: