Skip to content

Commit

Permalink
EPA-171: Remove JSON option, improve error messages (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasrichner-oviva authored Dec 9, 2024
1 parent e46698f commit 9e2929f
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.oviva.ehealthid.relyingparty.svc.AuthService.CallbackRequest;
import com.oviva.ehealthid.relyingparty.svc.AuthService.SelectedIdpRequest;
import com.oviva.ehealthid.relyingparty.svc.ValidationException;
import com.oviva.ehealthid.relyingparty.ws.AuthEndpoint.AuthResponse.IdpEntry;
import com.oviva.ehealthid.relyingparty.ws.ui.Pages;
import com.oviva.ehealthid.relyingparty.ws.ui.TemplateRenderer;
import edu.umd.cs.findbugs.annotations.NonNull;
Expand Down Expand Up @@ -69,34 +68,6 @@ public Response auth(
.build();
}

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response authJson(
@QueryParam("scope") String scope,
@QueryParam("state") String state,
@QueryParam("response_type") String responseType,
@QueryParam("client_id") String clientId,
@QueryParam("redirect_uri") String redirectUri,
@QueryParam("nonce") String nonce) {

var uri = mustParse(redirectUri);

var res =
authService.auth(
new AuthorizationRequest(scope, state, responseType, clientId, uri, nonce));

var availableIdentityProviders =
res.identityProviders().stream()
.map(idp -> new IdpEntry(idp.iss(), idp.name(), idp.logoUrl()))
.toList();

var body = new AuthResponse(availableIdentityProviders);

return Response.ok(body, MediaType.APPLICATION_JSON_TYPE)
.cookie(createSessionCookie(res.sessionId()))
.build();
}

@NonNull
private URI mustParse(@Nullable String uri) {
if (uri == null || uri.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import static com.oviva.ehealthid.relyingparty.svc.ValidationException.*;
import static com.oviva.ehealthid.relyingparty.util.LocaleUtils.getNegotiatedLocale;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.oviva.ehealthid.auth.AuthException;
import com.oviva.ehealthid.fedclient.FederationException;
import com.oviva.ehealthid.relyingparty.svc.AuthenticationException;
import com.oviva.ehealthid.relyingparty.svc.ValidationException;
Expand All @@ -20,11 +20,8 @@
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;
import java.util.Map;
import org.jboss.resteasy.util.MediaTypeHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LoggingEventBuilder;
Expand All @@ -33,15 +30,13 @@ public class ThrowableExceptionMapper implements ExceptionMapper<Throwable> {

private static final String SERVER_ERROR_MESSAGE = "error.serverError";
private static final String FEDERATION_ERROR_MESSAGE = "error.federationError";
private static final String AUTH_ERROR_MESSAGE = "error.authError";

private final Pages pages = new Pages(new TemplateRenderer());
@Context UriInfo uriInfo;
@Context Request request;
@Context HttpHeaders headers;

// Note: below fields MUST be non-final for mocking
private MediaTypeNegotiator mediaTypeNegotiator = new ResteasyMediaTypeNegotiator();

private Logger logger = LoggerFactory.getLogger(ThrowableExceptionMapper.class);

@Override
Expand All @@ -66,55 +61,46 @@ public Response toResponse(Throwable exception) {
return Response.seeOther(ve.seeOther()).build();
}

return buildContentNegotiatedErrorResponse(ve.localizedMessage(), Status.BAD_REQUEST);
return buildErrorResponse(ve.localizedMessage(), Status.BAD_REQUEST);
}

// the remaining exceptions are unexpected, let's log them
log(exception);

if (exception instanceof FederationException fe) {
var errorMessage = new Message(FEDERATION_ERROR_MESSAGE, fe.reason().name());
return buildContentNegotiatedErrorResponse(errorMessage, Status.INTERNAL_SERVER_ERROR);
return buildErrorResponse(errorMessage, Status.INTERNAL_SERVER_ERROR);
}

if (exception instanceof AuthException ae) {
var errorMessage = new Message(AUTH_ERROR_MESSAGE, ae.reason().name());
return buildErrorResponse(errorMessage, Status.INTERNAL_SERVER_ERROR);
}

var status = Status.INTERNAL_SERVER_ERROR;

var errorMessage = new Message(SERVER_ERROR_MESSAGE, (String) null);
return buildContentNegotiatedErrorResponse(errorMessage, status);
return buildErrorResponse(errorMessage, status);
}

private Response buildContentNegotiatedErrorResponse(Message message, StatusType status) {
private Response buildErrorResponse(Message message, StatusType status) {

var headerString = headers.getHeaderString("Accept-Language");
var locale = getNegotiatedLocale(headerString);

var mediaType =
mediaTypeNegotiator.bestMatch(
headers.getAcceptableMediaTypes(),
List.of(MediaType.TEXT_HTML_TYPE, MediaType.APPLICATION_JSON_TYPE));

if (MediaType.TEXT_HTML_TYPE.equals(mediaType)) {
var body = pages.error(message, locale);

// 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)) {
var body = new Problem("/server_error", message.messageKey());
return Response.status(status).entity(body).type(MediaType.APPLICATION_JSON_TYPE).build();
}

return Response.status(status).build();
var body = pages.error(message, locale);

// 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();
}

private void debugLog(Throwable exception) {
Expand Down Expand Up @@ -145,22 +131,4 @@ private LoggingEventBuilder addRequestContext(LoggingEventBuilder builder) {

return builder.addKeyValue("httpRequest", map);
}

interface MediaTypeNegotiator {
MediaType bestMatch(List<MediaType> desiredMediaType, List<MediaType> supportedMediaTypes);
}

private static class ResteasyMediaTypeNegotiator implements MediaTypeNegotiator {

@Override
public MediaType bestMatch(
List<MediaType> desiredMediaType, List<MediaType> supportedMediaTypes) {

// note: resteasy needs mutable lists
return MediaTypeHelper.getBestMatch(
new ArrayList<>(desiredMediaType), new ArrayList<>(supportedMediaTypes));
}
}

public record Problem(@JsonProperty("type") String type, @JsonProperty("title") String title) {}
}
23 changes: 12 additions & 11 deletions ehealthid-rp/src/main/resources/i18n_de_DE.properties
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
lang=de-DE
title=Anmeldung mit GesundheitsID
error.login=Einloggen mit GesundheitsID
error.serverError=Ohh nein! Unerwarteter Serverfehler. Bitte versuchen Sie es erneut.
error.federationError=Ohh nein! Unerwarteter Fehler der GesundheitsID. Bitte versuchen Sie es erneut. Grund: %s
error.noProvider=Kein Identitätsanbieter ausgewählt. Bitte zurückgehen.
error.invalidSession=Oops, Sitzung unbekannt oder abgelaufen. Bitte starten Sie erneut.
error.insecureRedirect=Unsicherer redirect_uri='%s'. Falsch konfigurierter Server, bitte verwenden Sie 'https'.
error.authError=Ohh nein! Unerwarteter Fehler der GesundheitsID. Unterstützt ihre Krankenkasse bereits Apps? Grund: %s
error.badRedirect=Ungültige redirect_uri='%s'. Übergebener Link ist nicht gültig.
error.untrustedRedirect=Nicht vertrauenswürdiger redirect_uri=%s. Falsch konfigurierter Server.
error.unsupportedScope=Scope '%s' wird nicht unterstützt
error.unsupportedResponseType=Nicht unterstützter Antworttyp: '%s'
error.blankUri=Leere Uri
error.badUri=Falsch uri='%s'
error.blankUri=Leere Uri
error.federationError=Ohh nein! Unerwarteter Fehler der GesundheitsID. Bitte versuchen Sie es erneut. Grund: %s
error.insecureRedirect=Unsicherer redirect_uri='%s'. Falsch konfigurierter Server, bitte verwenden Sie 'https'.
error.invalidSession=Oops, Sitzung unbekannt oder abgelaufen. Bitte starten Sie erneut.
error.login=Einloggen mit GesundheitsID
error.noProvider=Kein Identitätsanbieter ausgewählt. Bitte zurückgehen.
error.noRedirect=keine redirect_uri
error.serverError=Ohh nein! Unerwarteter Serverfehler. Bitte versuchen Sie es erneut.
error.unparsableHeader=Fehlgeformter Accept-Language-Header-Wert kann nicht analysiert werden
idp.selection=Wählen Sie Ihren GesundheitsID Anbieter
error.unsupportedResponseType=Nicht unterstützter Antworttyp: '%s'
error.unsupportedScope=Scope '%s' wird nicht unterstützt
error.untrustedRedirect=Nicht vertrauenswürdiger redirect_uri=%s. Falsch konfigurierter Server.
idp.login=Einloggen
idp.selection=Wählen Sie Ihren GesundheitsID Anbieter
23 changes: 12 additions & 11 deletions ehealthid-rp/src/main/resources/i18n_en_US.properties
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
lang=en-US
title=Login with GesundheitsID
error.serverError=Ohh no! Unexpected server error. Please try again.
error.federationError=Ohh no! Unexpected eHealthID error. Cause: %s
error.noProvider =No identity provider selected. Please go back
error.invalidSession=Oops, session unknown or expired. Please start again.
error.insecureRedirect=Insecure redirect_uri='%s'. Misconfigured server, please use 'https'.
error.authError=Ohh no! Unexpected eHealthID error. Does your health insurance support apps? Cause: %s
error.badRedirect=Bad redirect_uri='%s'. Passed link is not valid.
error.untrustedRedirect=Untrusted redirect_uri=%s. Misconfigured server.
error.blankUri=Blank uri
error.unsupportedScope=scope '%s' not supported
error.unsupportedResponseType=Unsupported response type: '%s'
error.badUri=Bad uri='%s'
error.blankUri=Blank uri
error.federationError=Ohh no! Unexpected eHealthID error. Cause: %s
error.insecureRedirect=Insecure redirect_uri='%s'. Misconfigured server, please use 'https'.
error.invalidSession=Oops, session unknown or expired. Please start again.
error.login=Log in with GesundheitsID
error.noProvider=No identity provider selected. Please go back
error.noRedirect=No redirect_uri
error.serverError=Ohh no! Unexpected server error. Please try again.
error.unparsableHeader=Unable to parse malformed Accept-Language header value
error.login=Log in with GesundheitsID
idp.selection=Select your GesundheitsID Provider
error.unsupportedResponseType=Unsupported response type: '%s'
error.unsupportedScope=scope '%s' not supported
error.untrustedRedirect=Untrusted redirect_uri=%s. Misconfigured server.
idp.login=Login
idp.selection=Select your GesundheitsID Provider
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import com.oviva.ehealthid.fedclient.IdpEntry;
import com.oviva.ehealthid.relyingparty.svc.AuthService;
import com.oviva.ehealthid.relyingparty.svc.AuthService.AuthorizationRequest;
import com.oviva.ehealthid.relyingparty.svc.AuthService.AuthorizationResponse;
import com.oviva.ehealthid.relyingparty.svc.AuthService.CallbackRequest;
import com.oviva.ehealthid.relyingparty.svc.AuthService.SelectedIdpRequest;
import com.oviva.ehealthid.relyingparty.svc.ValidationException;
import com.oviva.ehealthid.relyingparty.util.IdGenerator;
import com.oviva.ehealthid.relyingparty.ws.AuthEndpoint.AuthResponse;
import jakarta.ws.rs.core.Response.Status;
import java.net.URI;
import java.util.List;
Expand Down Expand Up @@ -103,44 +101,6 @@ void auth_success() {
}
}

@Test
void authJson_success() {
var identityProviders = List.of(new IdpEntry("a", "A", null), new IdpEntry("b", "B", null));

var sessionId = IdGenerator.generateID();
var authService = mock(AuthService.class);
when(authService.auth(any()))
.thenReturn(new AuthorizationResponse(identityProviders, sessionId));
var sut = new AuthEndpoint(authService);

var scope = "openid";
var state = UUID.randomUUID().toString();
var nonce = UUID.randomUUID().toString();
var responseType = "code";
var clientId = "myapp";

// when
try (var res = sut.authJson(scope, state, responseType, clientId, REDIRECT_URI, nonce)) {

// then
assertEquals(Status.OK.getStatusCode(), res.getStatus());

var authResponse = res.readEntity(AuthResponse.class);
var actualIdentityProviders = authResponse.identityProviders();
assertEquals(identityProviders.size(), actualIdentityProviders.size());
for (int i = 0; i < identityProviders.size(); i++) {
var expected = identityProviders.get(i);
var actual = actualIdentityProviders.get(i);
assertEquals(expected.iss(), actual.iss());
assertEquals(expected.name(), actual.name());
assertEquals(expected.logoUrl(), actual.logoUrl());
}

var sessionCookie = res.getCookies().get("session_id");
assertEquals(sessionId, sessionCookie.getValue());
}
}

@Test
void callback_success() {

Expand Down
Loading

0 comments on commit 9e2929f

Please sign in to comment.