Skip to content

Commit

Permalink
EPA-102: Error pages are not rendered correctly in the ehealthID Rely…
Browse files Browse the repository at this point in the history
…ing Party (#68)
  • Loading branch information
thomasrichner-oviva authored Apr 18, 2024
1 parent c1ffe77 commit d168f3f
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 54 deletions.
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).
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

0 comments on commit d168f3f

Please sign in to comment.