Implementing Authentication and Authorization at API Gateway Level
Moving authentication and authorization to an API Gateway offloads microservices from repetitive token validation logic. Services receive only verified requests with already-extracted claims.
Architecture
Client → [API Gateway: validate JWT, extract claims] → Service
↑
X-User-ID, X-Role, X-Tenant headers
Services trust headers from Gateway and don't perform their own JWT validation. Direct access to services is blocked via network policies.
JWT Validation in Kong
# Enable JWT plugin
curl -X POST http://localhost:8001/plugins \
-d "name=jwt"
# Create consumer and credentials
curl -X POST http://localhost:8001/consumers \
-d "username=frontend-app"
curl -X POST http://localhost:8001/consumers/frontend-app/jwt \
-d "algorithm=RS256" \
-d "rsa_public_key=$(cat /keys/public.pem)"
Client sends: Authorization: Bearer <JWT>. Kong validates signature and expiration, returns 401 on error.
OAuth 2.0 via Kong
curl -X POST http://localhost:8001/services/api/plugins \
-d "name=oauth2" \
-d "config.scopes[]=read" \
-d "config.scopes[]=write" \
-d "config.mandatory_scope=true" \
-d "config.token_expiration=3600" \
-d "config.enable_client_credentials=true" \
-d "config.enable_authorization_code=true"
OIDC in APISIX
{
"plugins": {
"openid-connect": {
"client_id": "api-gateway",
"client_secret": "secret",
"discovery": "https://keycloak.company.com/realms/myapp/.well-known/openid-configuration",
"scope": "openid profile",
"set_access_token_header": true,
"set_userinfo_header": true,
"token_signing_alg_values_expected": ["RS256"],
"introspection_endpoint_auth_method": "client_secret_post",
"set_id_token_header": false
}
}
}
Custom Lambda Authorizer (AWS API Gateway)
# authorizer.py
import json
import jwt
import boto3
def handler(event, context):
token = event['authorizationToken'].replace('Bearer ', '')
try:
# Get public key from AWS Secrets Manager
secret = boto3.client('secretsmanager').get_secret_value(
SecretId='jwt-public-key'
)
public_key = secret['SecretString']
payload = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='api.company.com'
)
# Check additional permissions
if not check_permissions(payload, event['methodArn']):
return generate_policy(payload['sub'], 'Deny', event['methodArn'])
return generate_policy(
payload['sub'],
'Allow',
event['methodArn'],
context={
'user_id': payload['sub'],
'tenant_id': payload.get('tenant_id'),
'role': payload.get('role', 'user')
}
)
except jwt.ExpiredSignatureError:
raise Exception('Token expired')
except jwt.InvalidTokenError:
raise Exception('Unauthorized')
def generate_policy(principal, effect, resource, context=None):
policy = {
'principalId': principal,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}]
}
}
if context:
policy['context'] = context
return policy
def check_permissions(payload, method_arn):
role = payload.get('role', 'user')
# Extract HTTP method from ARN
parts = method_arn.split(':')[-1].split('/')
http_method = parts[2] if len(parts) > 2 else 'GET'
if http_method in ['POST', 'PUT', 'DELETE'] and role == 'readonly':
return False
return True
Passing Claims to Upstream Services
After validation, Gateway adds headers:
# Traefik ForwardAuth response headers propagation
http:
middlewares:
jwt-auth:
forwardAuth:
address: "http://auth-service:4000/validate"
authResponseHeaders:
- X-User-ID
- X-User-Role
- X-Tenant-ID
- X-Subscription-Plan
-- Kong plugin: extract claims from JWT and add to upstream headers
local jwt_decoder = require "kong.plugins.jwt.jwt_parser"
local function execute(conf)
local token = kong.request.get_header("authorization")
if token then
token = token:gsub("Bearer ", "")
local jwt_obj = jwt_decoder:new(token)
local claims = jwt_obj.claims
kong.service.request.set_header("X-User-ID", claims.sub)
kong.service.request.set_header("X-Tenant-ID", claims.tenant_id)
kong.service.request.set_header("X-User-Role", claims.role)
end
end
RBAC (Role-Based Access Control) at Gateway Level
# Kong: different consumer groups with different rights
# Group "admins" — full access
# Group "readonly" — only GET requests
# ACL plugin
curl -X POST http://localhost:8001/services/admin-api/plugins \
-d "name=acl" \
-d "config.allow[]=admin-group" \
-d "config.hide_groups_header=true"
# Assign consumer to group
curl -X POST http://localhost:8001/consumers/alice/acl \
-d "group=admin-group"
Mutual TLS (mTLS) for Service-to-Service
# Kong: require client certificate
curl -X POST http://localhost:8001/services/internal-api/plugins \
-d "name=mtls-auth" \
-d "config.ca_certificates[]=$(cat ca-cert.pem)" \
-d "config.skip_consumer_lookup=true"
Handling Refresh Token
// Middleware before Gateway: automatically refresh expiring token
async function refreshMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (isExpiringSoon(token)) {
const newToken = await refreshAccessToken(req.cookies.refresh_token)
res.setHeader('X-New-Token', newToken)
req.headers.authorization = `Bearer ${newToken}`
}
next()
}
Timeline
Setting up JWT/OIDC authentication with RBAC and claims passing to services — 2–3 working days. With custom Lambda Authorizer and mTLS — 4–5 days.







