Skip to content

Commit

Permalink
feat(auth): allow limiting header authentication to list of configure…
Browse files Browse the repository at this point in the history
…d IP addresses (#787) (#793)
  • Loading branch information
piotrp authored and tchiotludo committed Oct 24, 2021
1 parent 44c67c4 commit b579ac5
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 6 deletions.
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

0 comments on commit b579ac5

Please sign in to comment.