Skip to content

Commit 11a71dc

Browse files
Mortega5wistefan
andauthored
fix(tir): change /v4/issuers implementation to match specification (#23)
* add filter to get x-forwarded-for headers * fix(tir): change /v4/issuers implementation to match specification The specification indicates that page[after] represents the page being accessed. The implementation has been updated to follow this behavior. Pages start at 0. Additionally, the links and the self did not reference the full URL, but only the path, without the host, port, or protocol. For its implementation, `micronaut.server.forward-headers` configuration has been added to read the X-Forwarded-* headers. * setup qemu to build multiarch images * feat(server): add support to RFC 7239 Forward header --------- Co-authored-by: Stefan Wiedemann <wistefan@googlemail.com>
1 parent a7b845f commit 11a71dc

File tree

13 files changed

+748
-61
lines changed

13 files changed

+748
-61
lines changed

.github/workflows/pre-release.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,18 @@ jobs:
5555
java-version: '21'
5656
java-package: jdk
5757

58-
- name: Log into quay.io
59-
run: docker login -u "${{ secrets.QUAY_USERNAME }}" -p "${{ secrets.QUAY_PASSWORD }}" ${{ env.REGISTRY }}
58+
- name: Set up QEMU
59+
uses: docker/setup-qemu-action@v3
60+
61+
- name: Set up Docker Buildx
62+
uses: docker/setup-buildx-action@v3
63+
64+
- name: Login to registry
65+
uses: docker/login-action@v3
66+
with:
67+
registry: ${{ env.REGISTRY }}
68+
username: ${{ secrets.QUAY_USERNAME }}
69+
password: ${{ secrets.QUAY_PASSWORD }}
6070

6171
- name: Build&Push image
6272
run: |

.github/workflows/release.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@ jobs:
4747
java-version: '21'
4848
java-package: jdk
4949

50-
- name: Log into quay.io
51-
run: docker login -u "${{ secrets.QUAY_USERNAME }}" -p "${{ secrets.QUAY_PASSWORD }}" ${{ env.REGISTRY }}
50+
- name: Set up QEMU
51+
uses: docker/setup-qemu-action@v3
52+
53+
- name: Set up Docker Buildx
54+
uses: docker/setup-buildx-action@v3
55+
56+
- name: Login to registry
57+
uses: docker/login-action@v3
58+
with:
59+
registry: ${{ env.REGISTRY }}
60+
username: ${{ secrets.QUAY_USERNAME }}
61+
password: ${{ secrets.QUAY_PASSWORD }}
5262

5363
- name: Build&Push image
5464
run: |

api/trusted-issuers-registry.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ components:
8585
in: query
8686
required: false
8787
schema:
88-
type: string
89-
example: did:key:z6MksU6tMfbaDzvaRe5oFE4eZTVTV4HJM4fmQWWGsDGQVsEr
88+
type: integer
89+
minimum: 0
90+
default: 0
91+
example: 0
9092
schemas:
9193
IssuerEntry:
9294
type: object
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.fiware.iam.configuration;
2+
3+
import io.micronaut.context.annotation.ConfigurationProperties;
4+
import lombok.Getter;
5+
6+
/**
7+
* Configuration for processing forwarded request headers.
8+
* This configuration is used to extract information from headers such as
9+
* Forwarded and X-Forwarded-* to determine the original request details.
10+
* `Forwarded` header, as defined in RFC 7239, is preferred over `X-Forwarded-*` headers.
11+
*/
12+
@ConfigurationProperties("micronaut.server.forward-headers")
13+
@Getter
14+
public class ForwardedForConfig {
15+
16+
17+
/**
18+
* The name of the header that carries the protocol.
19+
* Default: "X-Forwarded-Proto".
20+
*/
21+
private String protocolHeader;
22+
23+
/**
24+
* The name of the header that carries the port.
25+
* Default: "X-Forwarded-Port".
26+
*/
27+
private String portHeader;
28+
29+
/**
30+
* The name of the header that carries the host.
31+
* Default: "X-Forwarded-Host".
32+
*/
33+
private String hostHeader;
34+
35+
/**
36+
* The name of the header that carries the context path or prefix of the request.
37+
* Default: "X-Forwarded-Prefix".
38+
*/
39+
private String prefixHeader;
40+
41+
/**
42+
* The name of the header that carries the client’s IP address.
43+
* Default: "X-Forwarded-For".
44+
*/
45+
private String forHeader;
46+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package org.fiware.iam.filter;
2+
3+
import io.micronaut.http.HttpHeaders;
4+
import io.micronaut.http.HttpRequest;
5+
import io.micronaut.http.server.HttpServerConfiguration;
6+
import io.micronaut.http.ssl.ServerSslConfiguration;
7+
import jakarta.inject.Singleton;
8+
import org.fiware.iam.configuration.ForwardedForConfig;
9+
10+
import java.util.function.Consumer;
11+
12+
/**
13+
* Parses HTTP forwarding headers to extract client request information.
14+
*
15+
* <p>
16+
* This parser handles both the standard RFC 7239 `Forwarded` header and
17+
* configurable legacy `X-Forwarded-*` headers. It extracts key information
18+
* such as:
19+
* <ul>
20+
* <li>Original client IP address (`for` / X-Forwarded-For)</li>
21+
* <li>Original protocol (`proto` / X-Forwarded-Proto)</li>
22+
* <li>Host and optional port (`host` / X-Forwarded-Host and X-Forwarded-Port)</li>
23+
* <li>Proxy information (`by`)</li>
24+
* <li>Path prefix added by upstream proxies (`X-Forwarded-Prefix`)</li>
25+
* </ul>
26+
* </p>
27+
*/
28+
@Singleton
29+
public class ForwardHeaderParser {
30+
31+
public static final int DEFAULT_HTTP_PORT = 80;
32+
public static final int DEFAULT_HTTPS_PORT = 443;
33+
34+
private static final String DEFAULT_HOST = "localhost";
35+
private static final String HTTP_PROTOCOL = "http";
36+
private static final String HTTPS_PROTOCOL = "https";
37+
38+
// RFC 7239 directive names
39+
private static final String FOR_DIRECTIVE = "for";
40+
private static final String HOST_DIRECTIVE = "host";
41+
private static final String PROTO_DIRECTIVE = "proto";
42+
private static final String BY_DIRECTIVE = "by";
43+
44+
private final ForwardedForConfig config;
45+
private final int serverPort;
46+
private final String defaultServerProtocol;
47+
private final String defaultHost;
48+
49+
public ForwardHeaderParser(ForwardedForConfig config, HttpServerConfiguration serverConfiguration,
50+
ServerSslConfiguration sslConfig) {
51+
52+
this.config = config;
53+
this.serverPort = serverConfiguration.getPort().orElse(HttpServerConfiguration.DEFAULT_PORT);
54+
this.defaultServerProtocol = sslConfig.isEnabled() ? HTTPS_PROTOCOL : HTTP_PROTOCOL;
55+
this.defaultHost = serverConfiguration.getHost().orElse(DEFAULT_HOST);
56+
}
57+
58+
/**
59+
* Parses the provided HTTP headers to extract forwarding information.
60+
*
61+
* <p>
62+
* This method reads both the standard `Forwarded` header (RFC 7239) and
63+
* any legacy `X-Forwarded-*` headers as configured in {@link ForwardedForConfig}.
64+
* It populates a {@link ForwardedInfo} object with the extracted details:
65+
* client IP (`for`), protocol (`proto`), host, port, proxy (`by`), and path prefix.
66+
* `Forwarded` header values take precedence over legacy headers when both are present.
67+
* </p>
68+
*
69+
* <p>
70+
* The returned {@link ForwardedInfo} object can be used by downstream filters or
71+
* controllers to reconstruct the original request URL or handle redirects correctly
72+
* when behind reverse proxies.
73+
* </p>
74+
*
75+
* @param request the incoming request
76+
* @return a {@link ForwardedInfo} object containing the parsed forwarding details
77+
*/
78+
public ForwardedInfo parse(HttpRequest<?> request) {
79+
80+
HttpHeaders headers = request.getHeaders();
81+
ForwardedInfo forwardedInfo = new ForwardedInfo(null, defaultServerProtocol, defaultHost, serverPort, null, "");
82+
parseLegacyForwardedHeaders(headers, forwardedInfo);
83+
parseForwardedHeader(headers, forwardedInfo);
84+
85+
int port = forwardedInfo.getForwardedPort();
86+
String proto = forwardedInfo.getForwardedProto();
87+
88+
if (port == 0) {
89+
forwardedInfo.setForwardedPort(HTTPS_PROTOCOL.equalsIgnoreCase(proto) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
90+
} else if (port == -1) {
91+
// Use the actual server port if the forwarded port is invalid
92+
forwardedInfo.setForwardedPort(request.getServerAddress().getPort());
93+
}
94+
95+
return forwardedInfo;
96+
}
97+
98+
private void parseForwardedHeader(HttpHeaders headers, ForwardedInfo defaultEntry) {
99+
100+
String forwardedHeader = headers.get(HttpHeaders.FORWARDED);
101+
if (forwardedHeader == null || forwardedHeader.isEmpty()) {
102+
return ;
103+
}
104+
105+
String[] directives = forwardedHeader.split(";");
106+
107+
for (String directive : directives) {
108+
// get only first keyValue if multiple are present
109+
String[] keyValue = directive.trim().split(",")[0].split("=", 2);
110+
if (keyValue.length == 2) {
111+
String key = keyValue[0].trim().toLowerCase();
112+
String value = keyValue[1].trim().replaceAll("^\"|\"$", ""); // remove quotes
113+
switch (key) {
114+
case FOR_DIRECTIVE:
115+
defaultEntry.setForwardedFor(value);
116+
break;
117+
case PROTO_DIRECTIVE:
118+
defaultEntry.setForwardedProto(value);
119+
break;
120+
case HOST_DIRECTIVE:
121+
// Extract host and optional port
122+
if (value.contains(":")) {
123+
String[] hostParts = value.split(":", 2);
124+
defaultEntry.setForwardedHost(hostParts[0]);
125+
try {
126+
defaultEntry.setForwardedPort(Integer.parseInt(hostParts[1]));
127+
} catch (NumberFormatException e) {
128+
defaultEntry.setForwardedPort(0);
129+
}
130+
} else {
131+
defaultEntry.setForwardedHost(value);
132+
defaultEntry.setForwardedPort(0);
133+
}
134+
break;
135+
case BY_DIRECTIVE:
136+
defaultEntry.setForwardedBy(value);
137+
break;
138+
}
139+
}
140+
}
141+
}
142+
143+
private void parseLegacyForwardedHeaders(HttpHeaders headers, ForwardedInfo defaultEntry) {
144+
145+
getHeaderValue(headers, config.getForHeader(), defaultEntry::setForwardedFor);
146+
getHeaderValue(headers, config.getProtocolHeader(), defaultEntry::setForwardedProto);
147+
getHeaderValue(headers, config.getHostHeader(), defaultEntry::setForwardedHost);
148+
getHeaderValue(headers, config.getPortHeader(), defaultEntry::setForwardedPortStr);
149+
getHeaderValue(headers, config.getPrefixHeader(), defaultEntry::setForwardedPrefix);
150+
}
151+
152+
private void getHeaderValue(HttpHeaders headers, String headerName, Consumer<String> setter) {
153+
154+
if (headerName != null) {
155+
String value = headers.get(headerName);
156+
if (value != null) {
157+
setter.accept(value);
158+
}
159+
}
160+
}
161+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.fiware.iam.filter;
2+
3+
import io.micronaut.core.order.Ordered;
4+
import io.micronaut.http.HttpRequest;
5+
import io.micronaut.http.MutableHttpResponse;
6+
import io.micronaut.http.annotation.Filter;
7+
import io.micronaut.http.filter.HttpServerFilter;
8+
import io.micronaut.http.filter.ServerFilterChain;
9+
import io.micronaut.http.uri.UriBuilder;
10+
import org.reactivestreams.Publisher;
11+
12+
import java.net.URI;
13+
14+
/**
15+
* HTTP server filter that normalizes and exposes forwarding information for incoming requests.
16+
*
17+
* <p>
18+
* This filter reads the RFC 7239 Forwarded header as well as legacy X-Forwarded-* headers
19+
* (such as X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Prefix) from
20+
* each request. It parses these headers to determine the original client IP, protocol,
21+
* host, port, and any path prefix applied by upstream proxies or gateways.
22+
* </p>
23+
*
24+
* <p>
25+
* The filter then computes the full original request URL and stores it, along with the parsed
26+
* forwarding details, as attributes in the request:
27+
* <ul>
28+
* <li>{@link #REQ_ATTR}: the reconstructed request URI</li>
29+
* <li>{@link #FORWARD_INFO_ATTR}: the {@link ForwardedInfo} object containing all parsed forwarding information</li>
30+
* </ul>
31+
* </p>
32+
*
33+
* <p>
34+
* Downstream controllers and filters can use these attributes to correctly generate
35+
* absolute URLs, redirects, or logs that reflect the original client-facing request
36+
* even when behind reverse proxies.
37+
* </p>
38+
*/
39+
@Filter(Filter.MATCH_ALL_PATTERN)
40+
public class ForwardedForFilter implements HttpServerFilter, Ordered {
41+
42+
public static final String REQ_ATTR = "server-req";
43+
public static final String FORWARD_INFO_ATTR = "forwarded-info";
44+
45+
private static final String HTTP_PROTOCOL = "http";
46+
private static final String HTTPS_PROTOCOL = "https";
47+
48+
private final ForwardHeaderParser forwardHeaderParser;
49+
50+
public ForwardedForFilter(ForwardHeaderParser forwardHeaderParser) {
51+
52+
this.forwardHeaderParser = forwardHeaderParser;
53+
}
54+
55+
@Override
56+
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
57+
58+
ForwardedInfo forwardedInfo = forwardHeaderParser.parse(request);
59+
60+
URI reqUrl = getReqUrl(forwardedInfo);
61+
62+
HttpRequest<?> modifiedRequest = request
63+
.setAttribute(REQ_ATTR, reqUrl)
64+
.setAttribute(FORWARD_INFO_ATTR, forwardedInfo);
65+
66+
return chain.proceed(modifiedRequest);
67+
}
68+
69+
private URI getReqUrl(ForwardedInfo forwardedInfo) {
70+
71+
72+
String protocol = forwardedInfo.getForwardedProto();
73+
String host = forwardedInfo.getForwardedHost();
74+
int port = forwardedInfo.getForwardedPort();
75+
String prefix = forwardedInfo.getForwardedPrefix();
76+
77+
// Ignore default ports
78+
Integer portToUse = null;
79+
if (!(HTTP_PROTOCOL.equalsIgnoreCase(protocol) && port == ForwardHeaderParser.DEFAULT_HTTP_PORT)
80+
&& !(HTTPS_PROTOCOL.equalsIgnoreCase(protocol) && port == ForwardHeaderParser.DEFAULT_HTTPS_PORT)) {
81+
portToUse = port;
82+
}
83+
84+
UriBuilder builder = UriBuilder.of(protocol + "://" + host).path(prefix);
85+
86+
if (portToUse != null) {
87+
builder.port(portToUse);
88+
}
89+
90+
return builder.build();
91+
}
92+
93+
@Override
94+
public int getOrder() {
95+
96+
return Ordered.HIGHEST_PRECEDENCE;
97+
}
98+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.fiware.iam.filter;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.ToString;
6+
7+
@AllArgsConstructor
8+
@ToString
9+
@Data
10+
public class ForwardedInfo {
11+
12+
private String forwardedFor;
13+
private String forwardedProto;
14+
private String forwardedHost;
15+
private int forwardedPort;
16+
private String forwardedBy;
17+
private String forwardedPrefix;
18+
19+
public void setForwardedPortStr(String portStr) {
20+
if (portStr == null || portStr.isEmpty()) {
21+
return;
22+
}
23+
this.forwardedPort = Integer.parseInt(portStr);
24+
}
25+
26+
public String getForwardedPrefix() {
27+
return forwardedPrefix != null ? forwardedPrefix : "";
28+
}
29+
}

0 commit comments

Comments
 (0)