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

EPA-102: Error pages are not rendered correctly in the ehealthID Relying Party #68

Merged
merged 1 commit into from
Apr 18, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import jakarta.ws.rs.core.Response.StatusType;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -86,7 +87,18 @@ private Response buildContentNegotiatedErrorResponse(Message message, StatusType

if (MediaType.TEXT_HTML_TYPE.equals(mediaType)) {
var body = pages.error(message, locale);
return Response.status(status).entity(body).type(MediaType.TEXT_HTML_TYPE).build();

// FIXES oviva-ag/ehealthid-relying-party #58 / EPA-102
// resteasy has a built-in `MessageSanitizerContainerResponseFilter` escaping all non status
// 200
// 'text/html' responses
// if the entity is a string.
// The corresponding "resteasy.disable.html.sanitizer" config does not work with SeBootstrap
// currently (resteasy 6.2).
Comment on lines +91 to +97
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting from spotless/intellij 🤷

return Response.status(status)
.entity(body.getBytes(StandardCharsets.UTF_8))
.type(MediaType.TEXT_HTML_TYPE)
.build();
}

if (MediaType.APPLICATION_JSON_TYPE.equals(mediaType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider;
import com.oviva.ehealthid.relyingparty.test.EmbeddedRelyingParty;
import io.restassured.http.ContentType;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
Expand All @@ -36,35 +31,15 @@ class MainTest {
private static final String IDP_PATH = "auth/select-idp";
private static final String CALLBACK_PATH = "auth/callback";

private static EmbeddedRelyingParty application;

@RegisterExtension
static WireMockExtension wm =
WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();

private static Main application;

@BeforeAll
static void beforeAll() throws ExecutionException, InterruptedException {

var discoveryUri = URI.create(wm.baseUrl()).resolve(DISCOVERY_PATH);

var redirectUri = URI.create("https://myapp.example.com");

var config =
configFromProperties(
"""
federation_enc_jwks_path=src/test/resources/fixtures/example_enc_jwks.json
federation_sig_jwks_path=src/test/resources/fixtures/example_sig_jwks.json
base_uri=%s
idp_discovery_uri=%s
redirect_uris=%s
app_name=Awesome DiGA
port=0
"""
.formatted(wm.baseUrl(), discoveryUri, redirectUri));

application = new Main(config);

// when
application = new EmbeddedRelyingParty();
application.start();
}

Expand All @@ -73,16 +48,6 @@ static void afterAll() throws Exception {
application.close();
}

private static ConfigProvider configFromProperties(String s) {
var props = new Properties();
try {
props.load(new StringReader(s));
} catch (IOException e) {
throw new RuntimeException(e);
}
return new StaticConfig(props);
}

@Test
void run_smokeTest() {

Expand Down Expand Up @@ -333,12 +298,4 @@ void run_selectIdp() {
private void assertGetOk(URI uri) {
get(uri).then().statusCode(200);
}

record StaticConfig(Map<Object, Object> values) implements ConfigProvider {

@Override
public Optional<String> get(String name) {
return Optional.ofNullable(values.get(name)).map(Object::toString);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.oviva.ehealthid.relyingparty.test;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.Stubbing;
import com.oviva.ehealthid.relyingparty.Main;
import com.oviva.ehealthid.relyingparty.cfg.ConfigProvider;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class EmbeddedRelyingParty implements AutoCloseable {

private static final String DISCOVERY_PATH = "/.well-known/openid-configuration";
private Main application;
private WireMockServer wireMockServer;

public URI start() throws ExecutionException, InterruptedException {

var options = WireMockConfiguration.options().dynamicPort();
this.wireMockServer = new WireMockServer(options);
wireMockServer.start();

var discoveryUri = URI.create(wireMockServer.baseUrl()).resolve(DISCOVERY_PATH);

var redirectUri = URI.create("https://myapp.example.com");

var config =
StaticConfig.fromRawProperties(
"""
federation_enc_jwks_path=src/test/resources/fixtures/example_enc_jwks.json
federation_sig_jwks_path=src/test/resources/fixtures/example_sig_jwks.json
base_uri=%s
idp_discovery_uri=%s
redirect_uris=%s
app_name=Awesome DiGA
port=0
"""
.formatted(wireMockServer.baseUrl(), discoveryUri, redirectUri));

this.application = new Main(config);

application.start();

return application.baseUri();
}

public URI baseUri() {
return application.baseUri();
}

public Stubbing wireMockStubbing() {
return wireMockServer;
}

@Override
public void close() throws Exception {

Exception cause = null;

try {
this.application.close();
} catch (Exception e) {
cause = e;
}

this.wireMockServer.stop();
if (cause != null) {
throw cause;
}
}

record StaticConfig(Map<Object, Object> values) implements ConfigProvider {

@Override
public Optional<String> get(String name) {
return Optional.ofNullable(values.get(name)).map(Object::toString);
}

public static ConfigProvider fromRawProperties(String s) {
var props = new Properties();
try {
props.load(new StringReader(s));
} catch (IOException e) {
throw new RuntimeException(e);
}
return new StaticConfig(props);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.oviva.ehealthid.relyingparty.ws;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.startsWith;

import com.oviva.ehealthid.relyingparty.test.EmbeddedRelyingParty;
import io.restassured.RestAssured;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class ThrowableExceptionMapperComponentTest {

private static EmbeddedRelyingParty app;

@BeforeAll
static void beforeAll() throws ExecutionException, InterruptedException {
app = new EmbeddedRelyingParty();
app.start();
RestAssured.baseURI = app.baseUri().toString();
}

@AfterAll
static void afterAll() throws Exception {
app.close();
}

/** Regression test for: oviva-ag/ehealthid-relying-party #58 / EPA-102 */
@Test
void toResponse_htmlUnescaped() {

given()
.accept("text/html,*/*")
.get("/auth")
.then()
.header("content-type", startsWith("text/html"))
.body(startsWith("<!DOCTYPE html>"));
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.oviva.ehealthid.relyingparty.ws;

import static com.oviva.ehealthid.relyingparty.svc.ValidationException.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import com.github.jknack.handlebars.internal.text.StringEscapeUtils;
import com.github.mustachejava.util.HtmlEscaper;
import com.oviva.ehealthid.relyingparty.svc.AuthenticationException;
import com.oviva.ehealthid.relyingparty.svc.ValidationException;
import com.oviva.ehealthid.relyingparty.ws.ThrowableExceptionMapper.Problem;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.ServerErrorException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.core.*;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -230,11 +231,31 @@ void toResponse_withBody_withValidationExceptionAndDynamicContent(

// then
assertEquals(400, res.getStatus());
assertTrue(StringEscapeUtils.unescapeHtml4(res.getEntity().toString()).contains(message));
assertBodyContains(res, message);
assertEquals(MediaType.TEXT_HTML_TYPE, res.getMediaType());
assertNotNull(res.getEntity());
}

private static void assertBodyContains(Response res, String message) {
if (!(res.getEntity() instanceof byte[] bytes)) {
fail("unsupported entity type for tests: %s".formatted(res.getEntity().getClass()));
return;
}

var htmlBody = new String(bytes, StandardCharsets.UTF_8);

// the rendered response is already HTML escaped, let's do the same to the raw message
var escapedMessage = htmlEscape(message);

assertThat(htmlBody, containsString(escapedMessage));
}

private static String htmlEscape(String s) {
var w = new StringWriter();
HtmlEscaper.escape(s, w);
return w.toString();
}

private void mockHeaders(String locales) {
doReturn(locales).when(headers).getHeaderString("Accept-Language");
doReturn("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
Expand Down
Loading