Skip to content
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

feat(auth): allow limiting header authentication to list of configured IP addresses #793

Merged
merged 1 commit into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ The username field can be any string field, the roles field has to be a JSON arr

### Header configuration (reverse proxy)

To enable Header authentification in the application, you'll have to configure the header that will resolve users & groups:
To enable Header authentication in the application, you'll have to configure the header that will resolve users & groups:

```yaml
akhq:
Expand All @@ -627,6 +627,7 @@ akhq:
user-header: x-akhq-user # mandatory (the header name that will contain username)
groups-header: x-akhq-group # optional (the header name that will contain groups separated by groups-header-separator)
groups-header-separator: , # optional (separator, defaults to ',')
ip-patterns: [0.0.0.0] # optional (Java regular expressions for matching trusted IP addresses, '0.0.0.0' matches all addresses)
users: # optional, the users list to allow, if empty we only rely on `groups-header`
- username: header-user # username matching the `user-header` value
groups: # list of group for current users
Expand All @@ -636,10 +637,11 @@ akhq:
- admin
```

* The `user-header` is mandatory in order to map the user with `users` list or to display the user on the ui if no `users` is provided.
* The `groups-header` is optional and can be used in order to inject a list of groups for all the users. This list will be merged with `groups` for the current users.
* The `groups-header-separator` is optional and can be used to customize group separator used when parsing `groups-header` header, defaults to `,`.
* The `users` is a list of allowed users.
* `user-header` is mandatory in order to map the user with `users` list or to display the user on the ui if no `users` is provided.
* `groups-header` is optional and can be used in order to inject a list of groups for all the users. This list will be merged with `groups` for the current users.
* `groups-header-separator` is optional and can be used to customize group separator used when parsing `groups-header` header, defaults to `,`.
* `ip-patterns` limits the IP addresses that header authentication will accept, given as a list of Java regular expressions, omit or set to `[0.0.0.0]` to allow all addresses
* `users` is a list of allowed users.

### External roles and attributes mapping

Expand Down
3 changes: 2 additions & 1 deletion application.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ micronaut:
groups:
enabled: true
base: "dc=example,dc=com"
# OIDC authentification configuration
# OIDC authentication configuration
oauth2:
enabled: true
clients:
Expand Down Expand Up @@ -265,6 +265,7 @@ akhq:
user-header: x-akhq-user # mandatory (the header name that will contain username)
groups-header: x-akhq-group # optional (the header name that will contain groups separated by groups-header-separator)
groups-header-separator: , # optional (separator, defaults to ',')
ip-patterns: [127.0.0.*] # optional (Java regular expressions for matching trusted IP addresses, '0.0.0.0' matches all addresses)
users: # optional, the users list to allow, if empty we only rely on `groups-header`
- username: header-user # username matching the `user-header` value
groups: # list of group for current users
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/akhq/configs/HeaderAuth.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.akhq.configs;

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.security.config.SecurityConfigurationProperties;
import lombok.Data;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Data
Expand All @@ -13,6 +15,7 @@ public class HeaderAuth {
String groupsHeader;
String groupsHeaderSeparator = ",";
List<Users> users;
List<String> ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE);

@Data
public static class Users {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.micronaut.security.authentication.AuthenticationUserDetailsAdapter;
import io.micronaut.security.authentication.Authenticator;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.config.SecurityConfigurationProperties;
import io.micronaut.security.filters.AuthenticationFetcher;
import io.micronaut.security.token.config.TokenConfiguration;
import io.reactivex.Flowable;
Expand All @@ -15,11 +16,14 @@
import org.akhq.utils.ClaimProvider;
import org.reactivestreams.Publisher;

import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;

Expand All @@ -39,6 +43,16 @@ public class HeaderAuthenticationFetcher implements AuthenticationFetcher {
@Inject
TokenConfiguration configuration;

private List<Pattern> ipPatternList;

@PostConstruct
public void init() {
this.ipPatternList = headerAuth.getIpPatterns()
.stream()
.map(Pattern::compile)
.collect(Collectors.toList());
}

@Override
public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
Optional<String> userHeaders = headerAuth.getUserHeader() != null ?
Expand All @@ -49,6 +63,29 @@ public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
return Publishers.empty();
}

if (!ipPatternList.isEmpty()) {
InetSocketAddress socketAddress = request.getRemoteAddress();
//noinspection ConstantConditions https://github.com/micronaut-projects/micronaut-security/issues/186
if (socketAddress == null) {
log.debug("Request remote address was not found. Skipping header authentication.");
return Publishers.empty();
}

if (socketAddress.getAddress() == null) {
log.debug("Could not resolve the InetAddress. Skipping header authentication.");
return Publishers.empty();
}

String hostAddress = socketAddress.getAddress().getHostAddress();
if (ipPatternList.stream().noneMatch(pattern ->
pattern.pattern().equals(SecurityConfigurationProperties.ANYWHERE) ||
pattern.matcher(hostAddress).matches())) {
log.warn("None of the IP patterns [{}] matched the host address [{}]. Skipping header authentication.", headerAuth.getIpPatterns(), hostAddress);
return Publishers.empty();
}
log.debug("One or more of the IP patterns matched the host address [{}]. Continuing request processing.", hostAddress);
}

Optional<String> groupHeaders = headerAuth.getGroupsHeader() != null ?
request.getHeaders().get(headerAuth.getGroupsHeader(), String.class) :
Optional.empty();
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javax.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class HeaderAuthControllerTest extends AbstractTest {
@Inject
Expand Down Expand Up @@ -89,4 +90,25 @@ void invalidUser() {
assertEquals(3, result.getRoles().size());
}
}

@MicronautTest(environments = "header-ip-disallow")
public static class UntrustedIp extends AbstractTest {
@Inject
@Client("/")
protected RxHttpClient client;

@Test
void invalidIp() {
AkhqController.AuthUser result = client.toBlocking().retrieve(
HttpRequest
.GET("/api/me")
.header("x-akhq-user", "header-user")
.header("x-akhq-group", "limited,extra"),
AkhqController.AuthUser.class
);

assertNull(result.getUsername());
assertNull(result.getRoles());
}
}
}
7 changes: 7 additions & 0 deletions src/test/resources/application-header-ip-disallow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
akhq:
security:
header-auth:
user-header: x-akhq-user
groups-header: x-akhq-group
ip-patterns: ["none"]
default-group: invalid-group