mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-03-20 08:15:16 +01:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
20
src/main/java/com/ysoft/geecon/error/OAuthApiException.java
Normal file
20
src/main/java/com/ysoft/geecon/error/OAuthApiException.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user