error handling split among:

1. user visible exception (directly shown to users as html)
2. redirect exception (pass back to redirect uri)
3. api exception (json)
This commit is contained in:
Dusan Jakub
2023-09-19 21:23:38 +02:00
parent 470600d7f1
commit e081da00da
10 changed files with 141 additions and 46 deletions

View File

@@ -2,7 +2,9 @@ package com.ysoft.geecon;
import com.ysoft.geecon.dto.*;
import com.ysoft.geecon.error.ErrorResponse;
import com.ysoft.geecon.error.OAuthException;
import com.ysoft.geecon.error.OAuthApiException;
import com.ysoft.geecon.error.OAuthRedirectException;
import com.ysoft.geecon.error.OAuthUserVisibleException;
import com.ysoft.geecon.repo.ClientsRepo;
import com.ysoft.geecon.repo.SessionsRepo;
import com.ysoft.geecon.repo.UsersRepo;
@@ -30,7 +32,8 @@ public class OAuthResource {
@FormParam("password") String password) {
sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid session"));
sessionsRepo.getSession(sessionId).orElseThrow(
() -> new OAuthUserVisibleException(ErrorResponse.Error.access_denied, "Invalid session"));
var user = validateUser(username, password);
if (user == null) {
return Templates.login(username, sessionId, "invalid_credentials");
@@ -65,7 +68,7 @@ public class OAuthResource {
@FormParam("sessionId") String sessionId,
@FormParam("scope") List<String> scopes) {
sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid session"));
sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthUserVisibleException(ErrorResponse.Error.access_denied, "Invalid session"));
var session = sessionsRepo.authorizeSession(sessionId, scopes);
String redirectUri = session.params().getRedirectUri();
@@ -120,7 +123,7 @@ public class OAuthResource {
return switch (params.getGrantType()) {
case "authorization_code" -> redeemAuthorizationCode(params);
case "urn:ietf:params:oauth:grant-type:device_code" -> redeemDeviceCode(params);
default -> throw new OAuthException(ErrorResponse.Error.invalid_request, "Unsupported grant type");
default -> throw new OAuthApiException(ErrorResponse.Error.invalid_request, "Unsupported grant type");
};
}
@@ -148,23 +151,23 @@ public class OAuthResource {
private AccessTokenResponse redeemAuthorizationCode(TokenParams params) {
var session = sessionsRepo.redeemAuthorizationCode(params.getCode())
.orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid code"));
.orElseThrow(() -> new OAuthApiException(ErrorResponse.Error.access_denied, "Invalid code"));
validateClient(params, session);
if (!session.validateCodeChallenge(params.getCodeVerifier())) {
throw new OAuthException(ErrorResponse.Error.access_denied, "Invalid code verifier");
throw new OAuthApiException(ErrorResponse.Error.access_denied, "Invalid code verifier");
}
return session.tokens();
}
private AccessTokenResponse redeemDeviceCode(TokenParams params) {
var session = sessionsRepo.getByAuthorizationCode(params.getDeviceCode())
.orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid device code"));
.orElseThrow(() -> new OAuthApiException(ErrorResponse.Error.access_denied, "Invalid device code"));
validateClient(params, session);
if (session.tokens() != null) {
sessionsRepo.redeemAuthorizationCode(params.getDeviceCode());
return session.tokens();
} else {
throw new OAuthException(ErrorResponse.Error.authorization_pending, "Authorization pending");
throw new OAuthApiException(ErrorResponse.Error.authorization_pending, "Authorization pending");
}
}
@@ -176,24 +179,30 @@ public class OAuthResource {
private OAuthClient validateClient(AuthParams params) {
var client = clientsRepo.getClient(params.getClientId())
.orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client"));
// must NOT redirect to not validated client
.orElseThrow(() -> new OAuthUserVisibleException(ErrorResponse.Error.invalid_request, "Not a valid client"));
if (!client.validateRedirectUri(params.getRedirectUri())) {
throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
// must NOT redirect to invalid redirect URI
throw new OAuthUserVisibleException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
}
if (StringUtil.isNullOrEmpty(params.getState())) {
throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid state");
throw new OAuthRedirectException(params, ErrorResponse.Error.invalid_request, "Missing state");
}
if (!params.validateResponseType()) {
throw new OAuthRedirectException(params, ErrorResponse.Error.unsupported_response_type,
"Unsupported response type");
}
return client;
}
private OAuthClient validateClient(TokenParams params, AuthorizationSession session) {
var client = clientsRepo.getClient(params.getClientId())
.orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client"));
.orElseThrow(() -> new OAuthApiException(ErrorResponse.Error.invalid_request, "Not a valid client"));
if (!session.validateRedirectUri(params.getRedirectUri())) {
throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
throw new OAuthApiException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
}
if (!client.validateSecret(params.getClientSecret())) {
throw new OAuthException(ErrorResponse.Error.unauthorized_client, "Invalid secret");
throw new OAuthApiException(ErrorResponse.Error.unauthorized_client, "Invalid secret");
}
return client;
}

View File

@@ -12,6 +12,14 @@ public class AuthParams {
.toList();
}
public boolean validateResponseType() {
try {
return !getResponseTypes().isEmpty();
} catch (IllegalArgumentException exception) {
return false;
}
}
@RestQuery("login_hint")
String loginHint;
@RestQuery("response_type")

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
public record ErrorResponse(@JsonProperty("error") Error error,
@JsonProperty("error_description") String description) {
public enum Error {
invalid_request, unauthorized_client, access_denied, invalid_scope, server_error, temporarily_unavailable, authorization_pending
invalid_request, unauthorized_client, unsupported_response_type,
access_denied, invalid_scope, server_error, temporarily_unavailable, authorization_pending
}
}

View File

@@ -3,33 +3,15 @@ package com.ysoft.geecon.error;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import java.util.Arrays;
@ApplicationScoped
class ExceptionMappers {
@Inject
ResourceInfo resourceInfo;
@ServerExceptionMapper
public Response exception(OAuthException exception) {
Object entity = producesJson() ? exception.getResponse() : Templates.error(exception.getResponse());
return Response.status(Response.Status.BAD_REQUEST).entity(entity).build();
}
private boolean producesJson() {
Produces annotation = resourceInfo.getResourceMethod().getAnnotation(Produces.class);
if (annotation == null) {
return false;
}
String[] produces = annotation.value();
return Arrays.asList(produces).contains(MediaType.APPLICATION_JSON);
return exception.getResponse();
}

View File

@@ -0,0 +1,20 @@
package com.ysoft.geecon.error;
import jakarta.ws.rs.core.Response;
public class OAuthApiException extends OAuthException {
public OAuthApiException(ErrorResponse response) {
super("OAuth error: " + response.error() + " " + response.description(), response);
}
public OAuthApiException(ErrorResponse.Error error, String description) {
this(new ErrorResponse(error, description));
}
@Override
public Response getResponse() {
return Response.status(Response.Status.BAD_REQUEST).entity(response).build();
}
}

View File

@@ -1,19 +1,14 @@
package com.ysoft.geecon.error;
public class OAuthException extends RuntimeException {
private final ErrorResponse response;
import jakarta.ws.rs.core.Response;
public OAuthException(ErrorResponse response) {
super("OAuth error: " + response.error() + " " + response.description());
public abstract class OAuthException extends RuntimeException {
protected final ErrorResponse response;
public OAuthException(String message, ErrorResponse response) {
super(message);
this.response = response;
}
public OAuthException(ErrorResponse.Error error, String description) {
this(new ErrorResponse(error, description));
}
public ErrorResponse getResponse() {
return response;
}
public abstract Response getResponse();
}

View File

@@ -0,0 +1,27 @@
package com.ysoft.geecon.error;
import com.ysoft.geecon.dto.AuthParams;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
public class OAuthRedirectException extends OAuthApiException {
private final AuthParams authParams;
public OAuthRedirectException(AuthParams authParams, ErrorResponse.Error error, String description) {
super(error, description);
this.authParams = authParams;
}
public AuthParams getAuthParams() {
return authParams;
}
public Response getResponse() {
UriBuilder uri = UriBuilder.fromUri(authParams.getRedirectUri())
.fragment("")
.queryParam("state", authParams.getState())
.queryParam("error", response.error())
.queryParam("error_description", response.description());
return Response.seeOther(uri.build()).build();
}
}

View File

@@ -0,0 +1,15 @@
package com.ysoft.geecon.error;
import jakarta.ws.rs.core.Response;
public class OAuthUserVisibleException extends OAuthApiException {
public OAuthUserVisibleException(ErrorResponse.Error error, String description) {
super(error, description);
}
public Response getResponse() {
Object entity = ExceptionMappers.Templates.error(response);
return Response.status(Response.Status.BAD_REQUEST).entity(entity).build();
}
}