Skip to content

Commit

Permalink
preauth - being able to receive base64-encoded headers
Browse files Browse the repository at this point in the history
As Netty will always consider received headers as US-ASCII encoded,
this can lead to issues when the values contain accented characters.

With this new feature, it is possible to consider the values as
base64-encoded, which will be decoded before use.

Tests: IT added.
  • Loading branch information
pmauduit committed Dec 12, 2023
1 parent 009a9fa commit f016c40
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 11 deletions.
12 changes: 12 additions & 0 deletions docs/pre-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ The following headers are expected to be received by the Gateway:
* `preauth-lastname`: the surname of the user (e.g. "Mauduit")
* `preauth-org`: the organisation identifier (e.g. "geOrchestra")

== Charset considerations & encoded headers

As the framework will consider the received headers as pure `us-ascii`, this can lead to issues when values contain
accented characters. To circumvent this, it is possible to
configure the gateway to receive base64-encoded http headers.

If the headers values are prefixed with `{base64}`, then the `gateway` will
base64-decode the HTTP headers' values - apart from the `sec-georchestra-preauthenticated` which should still be set to `true` as plaintext - before using them.

== Account creation

In order to be able to administer users who are using the pre-authentication mechanism,
Expand Down Expand Up @@ -168,6 +177,9 @@ The following Apache configuration has been used in a setup to interact with the
RequestHeader set preauth-firstname %{MELLON_GIVEN_NAME}e "expr=-n env('MELLON_GIVEN_NAME')"
RequestHeader set preauth-lastname %{MELLON_SN}e "expr=-n env('MELLON_SN')"
RequestHeader set preauth-org %{MELLON_O}e "expr=-n env('MELLON_O')"
# If needed to base64-encode the headers because of them containing accented characters, you can
# use the following syntax and adapt the other headers above:
# RequestHeader set preauth-lastname "expr={base64}%{base64:%{env:MELLON_SN}}" "expr=-n env('MELLON_SN')"

ProxyPass "http://georchestra-gateway-svc:8080/"
ProxyPassReverse "http://georchestra-gateway-svc:8080/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,4 @@ public class HeaderPreauthConfigProperties {
* preauth-lastname, preauth-org, preauth-email, preauth-roles
*/
private boolean enabled = false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.RequiredArgsConstructor;
import org.georchestra.commons.security.SecurityHeaders;
import org.georchestra.security.model.GeorchestraUser;
import org.ldaptive.io.Base64;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -83,12 +86,12 @@ public static boolean isPreAuthenticated(ServerWebExchange exchange) {
}

public static GeorchestraUser map(Map<String, String> requestHeaders) {
String username = requestHeaders.get(PREAUTH_USERNAME);
String email = requestHeaders.get(PREAUTH_EMAIL);
String firstName = requestHeaders.get(PREAUTH_FIRSTNAME);
String lastName = requestHeaders.get(PREAUTH_LASTNAME);
String org = requestHeaders.get(PREAUTH_ORG);
String rolesValue = requestHeaders.get(PREAUTH_ROLES);
String username = SecurityHeaders.decode(requestHeaders.get(PREAUTH_USERNAME));
String email = SecurityHeaders.decode(requestHeaders.get(PREAUTH_EMAIL));
String firstName = SecurityHeaders.decode(requestHeaders.get(PREAUTH_FIRSTNAME));
String lastName = SecurityHeaders.decode(requestHeaders.get(PREAUTH_LASTNAME));
String org = SecurityHeaders.decode(requestHeaders.get(PREAUTH_ORG));
String rolesValue = SecurityHeaders.decode(requestHeaders.get(PREAUTH_ROLES));
List<String> roleNames = Optional.ofNullable(rolesValue)
.map(roles -> Stream
.concat(Stream.of("ROLE_USER"), Stream.of(roles.split(";")).filter(StringUtils::hasText))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Map;
import java.util.Optional;

import lombok.RequiredArgsConstructor;
import org.georchestra.gateway.security.GeorchestraUserMapperExtension;
import org.georchestra.security.model.GeorchestraUser;
import org.springframework.security.core.Authentication;
Expand Down
2 changes: 2 additions & 0 deletions gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ georchestra:
gateway:
security:
create-non-existing-users-in-l-d-a-p: false
header-authentication:
enabled: false
events:
rabbitmq:
# Note usually enableRabbitmqEvents, rabbitmqHost, etc. come from georchestra's default.properties
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.georchestra.gateway.accounts.admin;

import org.georchestra.ds.orgs.OrgsDao;
import org.georchestra.ds.users.Account;
import org.georchestra.ds.users.AccountDao;
import org.georchestra.gateway.app.GeorchestraGatewayApplication;
import org.georchestra.gateway.security.preauth.HeaderPreauthConfigProperties;
import org.georchestra.testcontainers.ldap.GeorchestraLdapContainer;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
Expand All @@ -18,8 +20,7 @@

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;

/**
* Integration tests for {@link CreateAccountUserCustomizer}.
Expand All @@ -31,7 +32,6 @@ public class CreateAccountUserCustomizerIT {
private @Autowired WebTestClient testClient;

private @Autowired ApplicationContext context;

private @Autowired AccountDao accountDao;

private @Autowired OrgsDao orgsDao;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.georchestra.gateway.accounts.admin;

import org.georchestra.ds.orgs.OrgsDao;
import org.georchestra.ds.users.Account;
import org.georchestra.ds.users.AccountDao;
import org.georchestra.gateway.app.GeorchestraGatewayApplication;
import org.georchestra.gateway.security.preauth.HeaderPreauthConfigProperties;
import org.georchestra.testcontainers.ldap.GeorchestraLdapContainer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(classes = GeorchestraGatewayApplication.class)
@AutoConfigureWebTestClient(timeout = "PT20S")
@ActiveProfiles({ "createaccount", "preauthbase64encoded" })
public class PreauthHttpHeadersBase64EncodedCreateAccountIT {

private @Autowired WebTestClient testClient;

private @Autowired ApplicationContext context;

private @Autowired AccountDao accountDao;

private @Autowired OrgsDao orgsDao;

public static GeorchestraLdapContainer ldap = new GeorchestraLdapContainer();

public static @BeforeAll void startUpContainers() {
ldap.start();
}

public static @AfterAll void shutDownContainers() {
ldap.stop();
}

public @Test void testPreauthenticatedHeaders_AccentedChars() throws Exception {
testClient.get().uri("/whoami")//
.header("sec-georchestra-preauthenticated", "true")//
.header("preauth-username", "{base64}ZnZhbmRlcmJsYWg=")//
.header("preauth-email", "{base64}ZnZhbmRlcmJsYWhAZ2VvcmNoZXN0cmEub3Jn")//
.header("preauth-firstname", "{base64}RnJhbsOnb2lz")//
.header("preauth-lastname", "{base64}VmFuIERlciBBY2NlbnTDqWQgQ2jDoHJhY3TDqHJz")//
.header("preauth-org", "{base64}R0VPUkNIRVNUUkE=")//
.exchange()//
.expectStatus()//
.is2xxSuccessful()//
.expectBody()//
.jsonPath("$.GeorchestraUser").isNotEmpty();

// Make sure the account has been created and the strings have been correctly
// evaluated at creation
Account toTest = accountDao.findByUID("fvanderblah");

assertTrue(
toTest.getSurname().equals("Van Der Accentéd Chàractèrs") && toTest.getGivenName().equals("François"));
}
}
1 change: 0 additions & 1 deletion gateway/src/test/resources/application-preauth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,3 @@ spring:
- AddSecHeaders
httpclient.wiretap: true
httpserver.wiretap: false

0 comments on commit f016c40

Please sign in to comment.