From 9e2929f34a8505b85c71f9131ff0d42236b7d9c5 Mon Sep 17 00:00:00 2001 From: Thomas Richner Date: Mon, 9 Dec 2024 13:53:31 +0100 Subject: [PATCH] EPA-171: Remove JSON option, improve error messages (#118) --- .../relyingparty/ws/AuthEndpoint.java | 29 ------- .../ws/ThrowableExceptionMapper.java | 80 ++++++------------- .../src/main/resources/i18n_de_DE.properties | 23 +++--- .../src/main/resources/i18n_en_US.properties | 23 +++--- .../relyingparty/ws/AuthEndpointTest.java | 40 ---------- .../ws/ThrowableExceptionMapperTest.java | 65 ++++++++------- .../oviva/ehealthid/auth/AuthException.java | 30 +++++++ .../oviva/ehealthid/auth/AuthExceptions.java | 47 +++++++---- .../steps/SelectSectoralIdpStepImpl.java | 13 ++- .../ehealthid/auth/AuthExceptionsTest.java | 57 +++++++++++++ 10 files changed, 213 insertions(+), 194 deletions(-) create mode 100644 ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthException.java create mode 100644 ehealthid/src/test/java/com/oviva/ehealthid/auth/AuthExceptionsTest.java diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java index 8b43936..f9db90d 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpoint.java @@ -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; @@ -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()) { diff --git a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapper.java b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapper.java index 8bef48c..15a030e 100644 --- a/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapper.java +++ b/ehealthid-rp/src/main/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapper.java @@ -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; @@ -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; @@ -33,15 +30,13 @@ public class ThrowableExceptionMapper implements ExceptionMapper { 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 @@ -66,7 +61,7 @@ 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 @@ -74,47 +69,38 @@ public Response toResponse(Throwable 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) { @@ -145,22 +131,4 @@ private LoggingEventBuilder addRequestContext(LoggingEventBuilder builder) { return builder.addKeyValue("httpRequest", map); } - - interface MediaTypeNegotiator { - MediaType bestMatch(List desiredMediaType, List supportedMediaTypes); - } - - private static class ResteasyMediaTypeNegotiator implements MediaTypeNegotiator { - - @Override - public MediaType bestMatch( - List desiredMediaType, List 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) {} } diff --git a/ehealthid-rp/src/main/resources/i18n_de_DE.properties b/ehealthid-rp/src/main/resources/i18n_de_DE.properties index 46df774..fb694c8 100644 --- a/ehealthid-rp/src/main/resources/i18n_de_DE.properties +++ b/ehealthid-rp/src/main/resources/i18n_de_DE.properties @@ -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 diff --git a/ehealthid-rp/src/main/resources/i18n_en_US.properties b/ehealthid-rp/src/main/resources/i18n_en_US.properties index e32fd78..b5c4250 100644 --- a/ehealthid-rp/src/main/resources/i18n_en_US.properties +++ b/ehealthid-rp/src/main/resources/i18n_en_US.properties @@ -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 diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java index 23dc895..98277c9 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/AuthEndpointTest.java @@ -5,7 +5,6 @@ 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; @@ -13,7 +12,6 @@ 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; @@ -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() { diff --git a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapperTest.java b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapperTest.java index 87843dd..f9a720f 100644 --- a/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapperTest.java +++ b/ehealthid-rp/src/test/java/com/oviva/ehealthid/relyingparty/ws/ThrowableExceptionMapperTest.java @@ -7,16 +7,16 @@ import static org.mockito.Mockito.*; import com.github.mustachejava.util.HtmlEscaper; +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; -import com.oviva.ehealthid.relyingparty.ws.ThrowableExceptionMapper.Problem; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.ServerErrorException; 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; import org.junit.jupiter.api.extension.ExtendWith; @@ -145,7 +145,6 @@ void toResponse_authentication() { void toResponse_withBody() { when(uriInfo.getRequestUri()).thenReturn(REQUEST_URI); - when(headers.getAcceptableMediaTypes()).thenReturn(List.of(MediaType.WILDCARD_TYPE)); mockHeaders("de-DE"); // when @@ -157,27 +156,6 @@ void toResponse_withBody() { assertNotNull(res.getEntity()); } - @Test - void toResponse_withJson() { - - when(headers.getAcceptableMediaTypes()) - .thenReturn( - List.of( - MediaType.APPLICATION_JSON_TYPE, - MediaType.TEXT_HTML_TYPE, - MediaType.WILDCARD_TYPE)); - - var msg = "Ooops! An error :/"; - - // when - var res = mapper.toResponse(new ValidationException(new Message(msg))); - - // then - assertEquals(400, res.getStatus()); - assertEquals(MediaType.APPLICATION_JSON_TYPE, res.getMediaType()); - assertEquals(new Problem("/server_error", msg), res.getEntity()); - } - @Test void toResponse_withBody_Unauthorized() { // when @@ -201,9 +179,8 @@ void toResponse_withBody_seeOthers() { } @Test - void toResponse_withBody_withValidationExc() { + void toResponse_withBody_withValidationException() { - when(headers.getAcceptableMediaTypes()).thenReturn(List.of(MediaType.WILDCARD_TYPE)); doReturn("de-DE").when(headers).getHeaderString("Accept-Language"); // when @@ -211,7 +188,40 @@ void toResponse_withBody_withValidationExc() { // then assertEquals(400, res.getStatus()); - System.out.println(res.getEntity()); + assertEquals(MediaType.TEXT_HTML_TYPE, res.getMediaType()); + assertNotNull(res.getEntity()); + } + + @Test + void toResponse_withBody_withFederationException() { + + doReturn("de-DE").when(headers).getHeaderString("Accept-Language"); + doReturn(URI.create("https://example.com")).when(uriInfo).getRequestUri(); + doReturn(null).when(headers).getHeaderString("user-agent"); + + // when + var res = + mapper.toResponse( + new FederationException("Nope!", FederationException.Reason.BAD_FEDERATION_MASTER)); + + // then + assertEquals(500, res.getStatus()); + assertEquals(MediaType.TEXT_HTML_TYPE, res.getMediaType()); + assertNotNull(res.getEntity()); + } + + @Test + void toResponse_withBody_withAuthenticationException() { + + doReturn("de-DE").when(headers).getHeaderString("Accept-Language"); + doReturn(URI.create("https://example.com")).when(uriInfo).getRequestUri(); + doReturn(null).when(headers).getHeaderString("user-agent"); + + // when + var res = mapper.toResponse(new AuthException("Nope!", AuthException.Reason.INVALID_ID_TOKEN)); + + // then + assertEquals(500, res.getStatus()); assertEquals(MediaType.TEXT_HTML_TYPE, res.getMediaType()); assertNotNull(res.getEntity()); } @@ -221,7 +231,6 @@ void toResponse_withBody_withValidationExc() { void toResponse_withBody_withValidationExceptionAndDynamicContent( String language, String messageKey, String message) { - when(headers.getAcceptableMediaTypes()).thenReturn(List.of(MediaType.WILDCARD_TYPE)); doReturn(language).when(headers).getHeaderString("Accept-Language"); // when diff --git a/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthException.java b/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthException.java new file mode 100644 index 0000000..844cdb5 --- /dev/null +++ b/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthException.java @@ -0,0 +1,30 @@ +package com.oviva.ehealthid.auth; + +public class AuthException extends RuntimeException { + + private final Reason reason; + + public AuthException(String message, Reason reason) { + super(message); + this.reason = reason; + } + + public AuthException(String message, Throwable cause, Reason reason) { + super(message, cause); + this.reason = reason; + } + + public Reason reason() { + return reason; + } + + public enum Reason { + UNKNOWN, + INVALID_PAR_URI, + MISSING_PAR_ENDPOINT, + FAILED_PAR_REQUEST, + MISSING_AUTHORIZATION_ENDPOINT, + MISSING_OPENID_CONFIGURATION_IN_ENTITY_STATEMENT, + INVALID_ID_TOKEN + } +} diff --git a/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthExceptions.java b/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthExceptions.java index a69b424..1d6c2a7 100644 --- a/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthExceptions.java +++ b/ehealthid/src/main/java/com/oviva/ehealthid/auth/AuthExceptions.java @@ -3,31 +3,46 @@ public class AuthExceptions { private AuthExceptions() {} - public static RuntimeException invalidParRequestUri(String uri) { - return new RuntimeException("invalid par request_uri '%s'".formatted(uri)); + public static AuthException invalidParRequestUri(String uri) { + // the field is called `request_uri`, hence the uri instead of url naming + return new AuthException( + "invalid par request_uri '%s'".formatted(uri), AuthException.Reason.INVALID_PAR_URI); } - public static RuntimeException missingAuthorizationUrl(String sub) { - return new RuntimeException( - "entity statement of '%s' has no authorization url configuration".formatted(sub)); + public static AuthException missingAuthorizationEndpoint(String sub) { + return new AuthException( + "entity statement of '%s' has no authorization endpoint configuration".formatted(sub), + AuthException.Reason.MISSING_AUTHORIZATION_ENDPOINT); } - public static RuntimeException missingParUrl(String sub) { - return new RuntimeException( - "entity statement of '%s' has no pushed authorization request configuration" - .formatted(sub)); + public static AuthException missingParEndpoint(String sub) { + return new AuthException( + "entity statement of '%s' has no pushed authorization request endpoint configuration" + .formatted(sub), + AuthException.Reason.MISSING_PAR_ENDPOINT); } - public static RuntimeException missingOpenIdConfigurationInEntityStatement(String sub) { - return new RuntimeException( - "entity statement of '%s' lacks openid configuration".formatted(sub)); + public static AuthException failedParRequest(String issuer, Exception cause) { + return new AuthException( + "PAR request failed sub=%s".formatted(issuer), + cause, + AuthException.Reason.FAILED_PAR_REQUEST); } - public static RuntimeException badIdTokenSignature(String issuer) { - return new RuntimeException("bad ID token signature from sub=%s".formatted(issuer)); + public static AuthException missingOpenIdConfigurationInEntityStatement(String sub) { + return new AuthException( + "entity statement of '%s' lacks openid configuration".formatted(sub), + AuthException.Reason.MISSING_OPENID_CONFIGURATION_IN_ENTITY_STATEMENT); } - public static RuntimeException badIdToken(String issuer, Exception cause) { - return new RuntimeException("bad ID token from sub=%s".formatted(issuer), cause); + public static AuthException badIdTokenSignature(String issuer) { + return new AuthException( + "bad ID token signature from sub=%s".formatted(issuer), + AuthException.Reason.INVALID_ID_TOKEN); + } + + public static AuthException badIdToken(String issuer, Exception cause) { + return new AuthException( + "bad ID token from sub=%s".formatted(issuer), cause, AuthException.Reason.INVALID_ID_TOKEN); } } diff --git a/ehealthid/src/main/java/com/oviva/ehealthid/auth/internal/steps/SelectSectoralIdpStepImpl.java b/ehealthid/src/main/java/com/oviva/ehealthid/auth/internal/steps/SelectSectoralIdpStepImpl.java index 546704e..0efea8b 100644 --- a/ehealthid/src/main/java/com/oviva/ehealthid/auth/internal/steps/SelectSectoralIdpStepImpl.java +++ b/ehealthid/src/main/java/com/oviva/ehealthid/auth/internal/steps/SelectSectoralIdpStepImpl.java @@ -8,9 +8,11 @@ import com.oviva.ehealthid.fedclient.IdpEntry; import com.oviva.ehealthid.fedclient.api.EntityStatement; import com.oviva.ehealthid.fedclient.api.EntityStatement.OpenidProvider; +import com.oviva.ehealthid.fedclient.api.HttpException; import com.oviva.ehealthid.fedclient.api.OpenIdClient; import com.oviva.ehealthid.fedclient.api.OpenIdClient.ParResponse; import com.oviva.ehealthid.fedclient.api.ParBodyBuilder; +import com.oviva.ehealthid.util.JsonCodec; import edu.umd.cs.findbugs.annotations.NonNull; import jakarta.ws.rs.core.UriBuilder; import java.net.URI; @@ -80,7 +82,12 @@ public List fetchIdpOptions() { .acrValues("gematik-ehealth-loa-high") .responseType("code"); - var res = doPushedAuthorizationRequest(parBody, trustedIdpEntityStatement.body()); + ParResponse res = null; + try { + res = doPushedAuthorizationRequest(parBody, trustedIdpEntityStatement.body()); + } catch (HttpException | JsonCodec.JsonException e) { + throw AuthExceptions.failedParRequest(sectoralIdpIss, e); + } var redirectUri = buildAuthorizationUrl(res.requestUri(), trustedIdpEntityStatement.body()); @@ -104,7 +111,7 @@ private URI buildAuthorizationUrl(String parRequestUri, EntityStatement trustedE var authzEndpoint = openidConfig.authorizationEndpoint(); if (authzEndpoint == null || authzEndpoint.isBlank()) { - throw AuthExceptions.missingAuthorizationUrl(trustedEntityStatement.sub()); + throw AuthExceptions.missingAuthorizationEndpoint(trustedEntityStatement.sub()); } return UriBuilder.fromUri(authzEndpoint) @@ -119,7 +126,7 @@ private ParResponse doPushedAuthorizationRequest( var openidConfig = getIdpOpenIdProvider(trustedEntityStatement); var parEndpoint = openidConfig.pushedAuthorizationRequestEndpoint(); if (parEndpoint == null || parEndpoint.isBlank()) { - throw AuthExceptions.missingParUrl(trustedEntityStatement.sub()); + throw AuthExceptions.missingParEndpoint(trustedEntityStatement.sub()); } return openIdClient.requestPushedUri(URI.create(parEndpoint), builder); diff --git a/ehealthid/src/test/java/com/oviva/ehealthid/auth/AuthExceptionsTest.java b/ehealthid/src/test/java/com/oviva/ehealthid/auth/AuthExceptionsTest.java new file mode 100644 index 0000000..53581f4 --- /dev/null +++ b/ehealthid/src/test/java/com/oviva/ehealthid/auth/AuthExceptionsTest.java @@ -0,0 +1,57 @@ +package com.oviva.ehealthid.auth; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AuthExceptionsTest { + + @Test + void invalidParRequestUri() { + var got = AuthExceptions.invalidParRequestUri("https://example.com"); + assertEquals(AuthException.Reason.INVALID_PAR_URI, got.reason()); + } + + @Test + void missingAuthorizationEndpoint() { + var got = AuthExceptions.missingAuthorizationEndpoint("https://example.com"); + assertEquals(AuthException.Reason.MISSING_AUTHORIZATION_ENDPOINT, got.reason()); + } + + @Test + void missingParEndpoint() { + var got = AuthExceptions.missingParEndpoint("https://example.com"); + assertEquals(AuthException.Reason.MISSING_PAR_ENDPOINT, got.reason()); + } + + @Test + void failedParRequest() { + var cause = new IllegalArgumentException(); + var got = AuthExceptions.failedParRequest("https://fedmaster.example.com", cause); + + assertEquals(AuthException.Reason.FAILED_PAR_REQUEST, got.reason()); + assertEquals(cause, got.getCause()); + } + + @Test + void missingOpenIdConfigurationInEntityStatement() { + var got = AuthExceptions.missingOpenIdConfigurationInEntityStatement("https://example.com"); + + assertEquals( + AuthException.Reason.MISSING_OPENID_CONFIGURATION_IN_ENTITY_STATEMENT, got.reason()); + } + + @Test + void badIdTokenSignature() { + var got = AuthExceptions.badIdTokenSignature("https://example.com"); + assertEquals(AuthException.Reason.INVALID_ID_TOKEN, got.reason()); + } + + @Test + void badIdToken() { + var cause = new IllegalArgumentException(); + var got = AuthExceptions.badIdToken("https://example.com", cause); + assertEquals(AuthException.Reason.INVALID_ID_TOKEN, got.reason()); + assertEquals(cause, got.getCause()); + } +}