coupergateway / couper Goto Github PK
View Code? Open in Web Editor NEWCouper is a lightweight API gateway designed to support developers in building and operating API-driven Web projects
Home Page: https://couper.io
License: MIT License
Couper is a lightweight API gateway designed to support developers in building and operating API-driven Web projects
Home Page: https://couper.io
License: MIT License
If this block is protected and a sub-match for api base_path
/... have no matching endpoint Couper returns with a 404 Not Found. This should be changed to access denied to prevent exposing available endpoints.
Describe the bug
The variable req.json_body
does not return the expected outcome.
To Reproduce
Steps to reproduce the behavior:
couper version
or docker run --entrypoint=/couper avenga/couper version
master 2021-03-29 acfce46
*.hcl
. Remove sensitive data.server "api" {
api {
base_path = "/api"
endpoint "/req" {
response {
# body = json_encode(req.json_body)
body = json_encode([1,2,3])
}
}
}
}
curl
call for reproductioncurl -s -H "Content-Type: application/json" -d "[1,2,3]" localhost:8080/api/req|jq
Expected behavior
json_encode([1,2,3])
: got expected [
1,
2,
3
]
json_encode(req.json_body)
got empty object, expected array with 1, 2, 3
{}
Additional context
This happens with boolean
, number
, string
or array
values.
The OAuth 2.0 Authorization Framework: Bearer Token Usage
(This is the RFC defining Bearer Authorization.)
Section "3. The WWW-Authenticate Response Header Field":
If the protected resource request does not include authentication credentials or does not contain an access token that enables access to the protected resource, the resource server MUST include the HTTP "
WWW-Authenticate
" response header field; it MAY include it in response to other conditions as well.
If the protected resource request included an access token and failed authentication, the resource server SHOULD include the "
error
" attribute to provide the client with the reason why the access request was declined. The parameter value is described in Section 3.1. In addition, the resource server MAY include the "error_description
" attribute to provide developers a human-readable explanation that is not meant to be displayed to end-users. It also MAY include the "error_uri
" attribute with an absolute URI identifying a human-readable web page explaining the error. The "error
", "error_description
", and "error_uri
" attributes MUST NOT appear more than once.
Example:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
Section "3.1. Error Codes":
invalid_token
The access token provided is expired, revoked, malformed, or invalid for other reasons. The resource SHOULD respond with the HTTP401
(Unauthorized) status code. The client MAY request a new access token and retry the protected resource request.
Currently, the status code for a rejected request (lacking authrorization or with an invalid token) is 403
(Forbidden), which SHOULD be used for insufficient privileges:
insufficient_scope
The request requires higher privileges than provided by the access token. The resource server SHOULD respond with the HTTP403
(Forbidden) status code and MAY include the "scope
" attribute with the scope necessary to access the protected resource.
A basic requirement is to provide a configurable health check status for our own server.
We have defined some health specific requirements:
/healthz
host
or xfh
path
match only which allows queries tooCache-Control
with no-store
500
while the server is closingcurrently, the couper
binary is just the server. starting the binary ./couper
starts the server.
in the future, we will most likely need more commands to be incorporated into the binary, e.g. couper validate
.
for now, we should start the server with the sub command run
:
$ couper run -f couper.hcl
My upstream systems needs an authorization token to be passed as a query parameter. Let's call it token=…
.
I configure an /upstream/**
endpoint and route all requests to a backend
configuration. As of now, all client requests hitting /usptream
will be passed unchanged to the backend
(except for the path being changed by /**
).
The token is an authorization mechanism that I want to implement in couper. Maybe I will extract it from a cookie, an arbitrary header or a JWT token. Now I am looking for a way to add it as a query parameter on every upstream request.
json-doc() expects a path to a json file. If we use json-doc()
in an xpath inside a file which has been called from another file, the json-doc path must not be relative to the current file but to the original caller, i.e.
conf/flow.xml:
<xslt in="$preResult" src="./transform/transform.xsl"/>
conf/transform/fields.json
contains data we want to parse.
conf/transform/transform.xsl:
<xsl:variable name="fieldList" select="json-doc('./conf/transform/fields.json')"/>
As this behaviour might be misleading, it would be good to add an example or a hint in the documentation.
We would like to support this kind of access control too.
A simple hcl block described in definitions
and can be referenced via label.
basic_auth "my-ba" {
# simple inline definition for demonstration
user = "john.do"
password = env.BASIC_AUTH_PASS
# production setup: multiple users with hashed passwords:
htpasswd_file = "/etc/htpasswd"
}
user
and password
are allowed to exist next to a htpasswd file reference
user
& password
configuration is optional and gets evaluated before htpasswd file.user
could be empty :password
crypto/subtle.ConstantTimeCompare()
should be used for the equality check401
statussplit(':')
bcrypt
md5
by apache (APR1)We want to prevent invalid upstream request and responses which does not match the requirements from a given openAPI yaml file.
Currently every merge to master releases this state to dockerhub latest. We will change this behaviour that the latest tag points to the latest release tag.
The github actions needs some configuration to tag and release as github release (tag).
We could provide an edge
tag which points to the latest master revision.
We want to introduce a new option for all kinds of access control to react to all or specific context errors. A common use-case for example is to redirect to a login route if a jwt token is expired. Currently Couper just throws this error and some frontend application must handle the redirect.
Specified as follows:
definitions {
jwt "myJ" {
error_handler "jwt_token_expired" "jwt_another_specific_one" {
# redirect { ... } or
response {
status = 403
}
}
# or a more generic fallback for all non listed jwt related cases
error_handler {
redirect {
location = "/login"
}
}
}
#saml "sso" { ... }
}
Implementation details:
Since the access_control
validates before the client-request will hit the final endpoint handler we must move the response object creation from there to the server level to intercept writing any client bytes before entering the error handling.
The error handling must be slightly refactored to fulfill this requirements in combination with the current "classic" error-file handling.
Specific error types could be a composition for grouping several cases and the access_control context.
The OpenAPI file may have defined servers with urls which could be absolute or relative. This leads to invalid requests for relative ones. We must add the current backend origin host to the relative OpenAPI server url.
Implementation: Since the backend origin gets evaluated during runtime the openAPI configuration must move to a runtime factory.
Accessing a bodies json payload is crucial for at least header enrichment in both directions. This will be a useful option for some new features later on.
The request (req
) and response (beresp
) evaluation needs to be extended with json_body
.
Since we have to buffer and may rewind the body content for further processing a limit is a requirement:
request/response_body_limit
or body_limit
for both directions.
Buffering those bodies should be known in general on startup/config load. Detecting the json_body
usage within a backend block. This enables some kind of lazy loading for json_body
if used in config and with the http method matches POST
PUT
or PATCH
and content-type application/json
.
Couper generates a unique request id with the help of https://github.com/rs/xid .
Some projects have a requirement of a real uuid which should be sent along with the request.
To provide a good log correlation, couper should be able to generate those via configuration.
The correct place for this would be the settings
block within our couper configuration file.
settings {
request_id_format = "uuid4"
}
Default should be common
which would be the current behaviour with the xid
package.
If the user configure the value uuid4
another generation method should be mapped to the uidFn
Field of the HTTPServer
struct.
Since all settings
are mapped to config/runtime/HTTPConfig
this one must be configurable via COUPER_REQUEST_ID_FORMAT
and command flag -request-id-format
.
Depends on PR #29 due to the settings
block.
Pick a stable uuid package.
https://github.com/avenga/couper/tree/master/docs#the-api-block-
You can add more than one api block to a server block
This is wrong.
The label for server
is documented as optional, but is required.
time="2020-10-08T07:05:18Z" level=fatal msg="Failed to load configuration bytes: loadBytes.hcl:1,8-9: Missing name for server; All server blocks must have 1 labels (name)." type=couper_daemon
The label for api
, files
and spa
is documented as optional, but if it is set there is an error. This may be the case for other blocks too. I only tested these three.
time="2020-10-08T07:00:27Z" level=fatal msg="Failed to load configuration bytes: loadBytes.hcl:6,7-15: Extraneous label for spa; No labels are expected for spa blocks." type=couper_daemon
access control is currently displayed in front of endpoints, that's not correct as it can be set at various locations in the config. not sure if it's possible to update the image in order to be 100% correct. It might be ok to stick with the simplified version. TBD
couper -h
Usage of couper:
-f string
-f ./couper.conf (default "couper.hcl")
-log-format string
-log-format json (default "default")
-p int
-p 8080 (default 8080)
-xfh
-xfh
…
As in #46 mentioned the documentation differs from our implementation. Currently we support just one api
block but there is no reason why we should not have multiple of them and is was planned anyways.
Add support for more than one api
block within a server
block. Configure the muxer accordingly and watch for clashes / conflicts and handle them.
If the upstream backend answers with a gzip body the reverseproxy component throws a panic panic: net/http: abort Handler
which is related to an error during body copy (client went away or writer closed).
Couper Version: 0.6
Configuration:
server "zipzip" {
endpoint "/**" {
proxy {
backend {
path = "/en/"
set_request_headers = {
Accept-Encoding: "gzip"
}
origin = "https://wao.io/"
}
}
}
}
Describe the bug
The array may evaluate to null
and therefore encodes respectively to null
.
To Reproduce
Steps to reproduce the behavior:
couper version
or docker run --entrypoint=/couper avenga/couper version
master 2021-03-29 acfce46
*.hcl
. Remove sensitive data.server "custom-response" {
endpoint "/**" {
request {
url = "https://httpbin.org/anything?a=${req.id}"
body = json_encode({
foo = []
bar = ["bar"]
})
}
response {
body = json_encode(beresp.json_body.json)
}
}
}
curl
call for reproductioncurl -v http://localhost:8080/
Result:
$ http localhost
HTTP/1.1 200 OK
Connection: close
Content-Encoding: gzip
Content-Length: 47
Date: Thu, 18 Mar 2021 18:02:26 GMT
Server: couper.io
Vary: Accept-Encoding
{"bar": ["bar"], "foo": null }
Expected behavior
{"bar": ["bar"], "foo": []}
Supporting user defined path parameter in couper is crucial. An example would look like this:
endpoint "/my/{category}/{sub}/list" {
backend {
path = "/cms/${req.path_param.category}/order/${req.path_param.sub}/list"
# or
request_headers {
x-cat: req.path_param.category
x-sub-cat: req.path_param.sub
}
}
}
Since some other packages has already solved some related details we would like to use this pathpattern module. This could be handy in combination with our openapi validation poc #21 later on.
So the main task is to replace our current http multiplexer and routing which additionally will simplify some implementation details.
Setting some header values like req.id
or simple strings should be send to the client, even with api connect errors, where applicable.
In the documentation the label
is optional for the server
, api
, spa
and files
block.
But the implementation differs. The label
is required for server
, and not allowed for api
, spa
and files
. Other blocks may be also differ between implementation and documentation.
The implementation should make the label
optional for all blocks, as it is in the documentation.
Couper should serve a simple htdocs
directory with a default index.html
(welcome page) if the user provides no other configuration.
This enables at least some simple static serving with our docker image:
docker run -p 80:8080 -v `pwd`/public:/htdocs avenga/couper
For this task we should use and cleanup /public
in our repository. /couper.hcl
should be discarded.
Create a couper welcome page and basic couper hcl configuration and add /public
to our Dockerfile
.
At least, JWT signing needs
optionally
exp
) and may either be part of the provided claims or a separate parameter (e.g. as time-to-live ttl
)kid
) of the JWT (necessary for JWK); this may not be useful, if Couper is both JWT signer and signature validatortyp
) header (e.g. at+jwt
, see https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-11#section-2.1)The JWT signing function expects a reference to a JWT signing profile (with invariable parameters; see jwt_signing_profile "MyToken" {}
below) and (optionally?) non-default claim:
jwt_sign(jwt_signing_profile_label, claims)
The first parameter jwt_signing_profile_label
references a jwt_signing_profile {}
block by its label (e.g. "MyToken"
).
The second parameter claims
is an object with (non-default) claims (e.g. { sub: "12345" }
).
A JWT signing profile specifies several parameters for JWT signing:
jwt_signing_profile "MyToken" {
# the algorithm to use for signing, REQUIRED
signature_algorithm = "RS256"
# the private key or secret to use for signing, REQUIRED; one of
key_file = "/keys/user/priv.pem"
# or
key = "$3cRe4"
# the time-to-live; translated into exp claim, REQUIRED
ttl = "1d" # 0 meaning no expiration
# the "default" claims to include in the JWT, optional
# they are merged with claims provided as function parameter, which take precedence
# they are evaluated at function call time
#claims = req.ctx.get_from_somewhere.claims
claims = {
iss = "the_issuer"
aud = req.query.youchoose
iat = unixtime()
}
# later:
# key id: id of the corresponding public key published in JWK, optional
# kid = "foo12345bar" # or key_id, ...
# ...
}
This may have a corresponding JWT access control block (already implemented), e.g.:
jwt "MyToken" {
signature_algorithm = "RS256"
key_file = "/keys/user/pub.pem"
cookie = "UserToken"
claims = {
aud = "user"
}
}
In order to make the configuration more user friendly we should allow single string values for list attributes with only one element.
example (all three configurations should be valid):
hosts = "example.com"
hosts = ["example.com"]
hosts = ["example.com", "foo.bar"]
This should be implemented for:
hosts
access_control
paths
allowed_origins
endpoint
s are a means to configure a proxy connection to "mount" upstream services ("backend") to a local path. Currently, an endpoint
can only be configured in the api
section.
The idea of the api
is to group endpoints or (client) requests that are meant to be consumed by applications (and probably follow strict schemas). Whereas the rest of the server (files
…) is consumed by "browsers" - that is navigational requests and usual Web semantics. Put simply, it's general purpose Web traffic vs API calls.
There are use cases for general purpose traffic proxy configurations apart from api
. For example, we could deploy a Docker container with the swagger UI to render our API docs. These endpoint should not be part of the api
. Here, a "free endpoint" would make sense:
server "foo" {
endpoint "/doc/**" {
access_control = [ "JWTToken" ]
backend {
origin = "http://swaggerui:8080"
path = "/**"
}
}
// …
}
Spec:
api
routes, but before files
and spa
paths.files
) instead of JSON error documents (TODO: should we lift files.error_document
to server
?)api.endpoint
(but handler:endpoint
instead of handler:api
)backend
should work as in api
: anonymous "inline" backend
with no label or refine a named backend or fallback to fallback backend
that is defined in server
backend
can be defined in server
as the fallback backend. This applies to all free endpoints, and to api.endpoint
(if no fallback backend is defined in api
). The server.backend
can be named (labelled) to allow refining in endpoints.Example with fallback backend:
server "foo" {
// fallback backend for free endpoints and API (may be overwritten by api.backend)
// label is optional
backend {
// …
}
// uses default backend
endpoint "/foo/**" {
}
}
My couper.hcl:
server "my-api" {
api {
endpoint "/**" {
backend = "foo"
}
}
}
definitions {
backend "foo" {
origin = "https://self-signed.badssl.com"
disable_certificate_validation = true
}
}
Although disable_certificate_validation
is active, I get a request error:
$ http :8080/
…
{
"error": {
"code": 4002,
"id": "c02mu9h8d3b4hja4foi0",
"message": "API upstream connection error",
"path": "/",
"status": 502
}
}
Couper's backend log says:
x509: certificate signed by unknown authority
However, the settings works, when defined in the endpoint
:
server "my-api" {
api {
endpoint "/**" {
backend {
origin = "https://self-signed.badssl.com"
disable_certificate_validation = true
}
}
}
}
This config yields a 200, as expected.
Except the default_port
option all other possible attributes from the settings
block needs to be merged with the default http configuration (default -> flag -> env).
Currently there are only two visible reasons for a request rejected by JWT access control:
401
403
Internally, there are several reasons for the rejection of a request, leading to "Authorization failed" with status 403
:
Authorization
header is included, but its value lacks "bearer" (case-insensitively) (accesscontrol.ErrorBearerRequired
)jwt.MalformedTokenError
), e.g.
jwt.InvalidSignatureError
)iss
claim) has an unexpected value (jwt.InvalidIssuerError
)aud
claim (if present) (jwt.InvalidAudienceError
)jwt.InvalidClaimsError
)It would be handy to have the more specific reason in the error response.
An access control to be used in a SAML2 assertion consumer service endpoint.
https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html mentions some security issues and how to address them.
https://tools.ietf.org/html/rfc7522#section-3 (which describes a similar case) has some requirements for assertion format and processing requirements.
The following is a mix of requirements:
RelayState
param (= RelayState
from AuthN request query param)samlp:Response/ds:Signature
))samlp:Response/@Destination
(= ACS endpoint URL of SP)samlp:Response/@InResponseTo
(= samlp:AuthnRequest/@ID
of the AuthN request)samlp:Response/saml:Issuer
(= entity_id
of IdP, from IdP metadata file)samlp:Response/samlp:Status/samlp:StatusCode/@Value
saml:Assertion/ds:Signature
))saml:Assertion/saml:Issuer
(= entity_id
of IdP, from IdP metadata file)saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience
(= entity_id
of SP, from Couper config)saml:Assertion/saml:Subject/saml:Subject
saml:Assertion/saml:Conditions/@NotOnOrAfter
or saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@NotOnOrAfter
saml:Assertion/saml:Conditions/@NotOnOrAfter
: if time passed, reject Assertionsaml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@NotOnOrAfter
: if time passed, reject SubjectConfirmationsaml:Assertion/saml:Subject/saml:SubjectConfirmation/@Method
(= 'urn:oasis:names:tc:SAML:2.0:cm:bearer')saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@Recipient
(= ACS endpoint URL of SP)saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@InResponseTo
(= samlp:AuthnRequest/@ID
of the AuthN request)saml:Assertion/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef
saml2 "SAML_AC" {
# the XML metadata about the identity provider
idp_metadata_file = "idp-metadata.xml"
# the entity id of the service provider (couper)
entity_id = "my-couper-api"
# single sign on service (idp)
sso_binding = "HTTP-Redirect" # irrelevant for access control?
# assertion consumer service (couper)
acs_binding = "HTTP-Post"
# alternative: binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
# whether to generate JSON claims
generate_json_claims = true
# which `saml:Attribute` elements are expected to possibly have multiple `saml:AttributeValue` element children
array_attributes = ["memberOf"]
# generates:
#claims = {
# iss = "https://the-id-provider.com…" # saml:Assertion/saml:Issuer
# sub = "username" # saml:Assertion/saml:Subject/saml:NameID
# exp = 1612371650 # saml:Assertion/saml:Conditions/@NotOnOrAfter -> unixtime
# nbf = 1612300000 # saml:Assertion/saml:Conditions/@NotBefore -> unixtime
# iat = 1612300010 # saml:Assertion/@IssueInstant -> unixtime
# aud = "my-couper-api" # saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience
# # loop saml:Assertion/saml:AttributeStatement/saml:Attribute -> @Name = ./saml:AttributeValue
# displayName = "Doe, John (External)"
# objectSid = "S-1-5-21-123456789-0123456789-123456789-000001"
# mail = "[email protected]"
# # Attribute with multiple AttributeValue's become arrays?
# memberOf = [
# "admin",
# "users",
# "CN=Special AD Group,OU=foo,DC=example,DC=com"
# ]
#}
}
Resources which have an access_control
type basic_auth
are not reachable via browser login-form triggered via WWW-Authenticate
header.
The current result:
curl -i http://localhost:8080
HTTP/1.1 403 Forbidden
Content-Type: text/html
Couper-Error: 5001 - "Authorization failed"
Server: couper.io
expected:
curl -i http://localhost:8080
HTTP/1.1 401 Unauthorized
Content-Type: text/html
Couper-Error: 5002 - "Unauthorized"
Server: couper.io
Www-Authenticate: Basic
When used in kubernetes with a config file mounted from a config map,
it would be best to have cooper autodetect changes in a/the config file and auto reload it.
Also: Make a boot param to enable/disable this feature.
The unixtime()
function retrieves the current UNIX timestamp in seconds
We would like to enhance our flow and retry the resource request if the response status is 401
with a new token request and replay the resource request.
Log enrichment to mark this as retry.
Since we know the length of the given body bytes for each direction we should ensure sending the Content-Length
header instead of sending those bytes with Transfer-Encoding: chunked
.
Depends on the io.Reader
. StringReader for example triggers an implicit set...
If the OpenAPI3 description of an upstream API mentions security requirements (security
) for routes, Couper should perform security checks when validating requests to this upstream API (according to the referenced security scheme objects in components.securitySchemes
):
http
basic
: header "Authorization: Basic ...
" set?bearer
: header "Authorization: Bearer ...
" set?apiKey
query
; name: <query_param>
: query parameter <query_param>
set?header
; name: <header_name>
: header <header_name>
set?cookie
; name: <cookie_name>
: cookie <cookie_name>
set?oauth2
/openIdConnect
: header "Authorization: Bearer ...
" set?If security
references several security scheme objects, the requirements must be AND
ed or OR
ed accordingly.
This additional check should be configurable (default is on
true
; performed only if upstream request validation is ).on
We want to change some label requirements for a cleaner configuration and useful log enrichment.
As follows:
The server
label is optional. If set: must be unique across the configuration file.
The api
block could have an optional label. If set: must be unique per server
.
Configured labels must be logged.
Currently, if I define my origin like origin = "http://${req.path_param.origin}"
and the {origin}
is not defined in the path
of my endpoint
, couper panics.
We should convert missing params to null
. The same goes for JsonBody
, Query
, Post
, etc.
Currently we are able to map environment variables to configuration structs via tag.
An example:
type Config struct {
DefaultPort int `env:"default_port"`
}
This gets internally prefixed with COUPER_
and uppercased. The value of COUPER_DEFAULT_PORT
will be assigned to Config.DefaultPort
.
The following example is not possible at this moment but should:
type Config struct {
DefaultPort int `env:"default_port"`
Timings Timings
}
Type Timings struct {
Timeout time.Duration `env:"timeout"`
}
Env COUPER_TIMEOUT
must be assigned to Config.Timings.Timeout
.
Related files are config/env/env.go
and runtime/http.go
makes use of it.
Due to some features we have wrapped the http.ResponseWriter
which is missing a http.Hijacker
interface implementation now. This interface must be implemented and requires a test to verify the UpgradeResponse (Status 101) is working again.
We have to document our already implemented and registered variables for our request/response evaluation.
req
is the client request.
bereq
is the modified backend request.
beresp
is the original backend response.
resp
would be the modified client response. Not implemented.
Most fields are self explanatory:
req
:
id
- unique request idmethod
http methodpath
url pathendpoint
matched endpoint patternheaders.<name>
http header value for requested keycookies.<name>
http cookie value for requested key (last wins)query.<name>
query parameter values (last wins)post.<name>
params - not implementedbereq
:
req
except endpoint
does not existurl
backend origin urlberesp
:
status
http status codeheaders|cookies<name>
value from set-cookie-header
(last wins)A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.