diff --git a/src/main/java/com/ysoft/geecon/OAuthResource.java b/src/main/java/com/ysoft/geecon/OAuthResource.java index ed1a69f..f7f58a3 100644 --- a/src/main/java/com/ysoft/geecon/OAuthResource.java +++ b/src/main/java/com/ysoft/geecon/OAuthResource.java @@ -1,6 +1,7 @@ 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.repo.ClientsRepo; import com.ysoft.geecon.repo.SessionsRepo; @@ -121,7 +122,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("Unsupported grant type"); + default -> throw new OAuthException(ErrorResponse.Error.invalid_request, "Unsupported grant type"); }; } @@ -149,23 +150,23 @@ public class OAuthResource { private AccessTokenResponse redeemAuthorizationCode(TokenParams params) { var session = sessionsRepo.redeemAuthorizationCode(params.getCode()) - .orElseThrow(() -> new OAuthException("Invalid code")); + .orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid code")); validateClient(params, session); if (!session.validateCodeChallenge(params.getCodeVerifier())) { - throw new OAuthException("Invalid code verifier"); + throw new OAuthException(ErrorResponse.Error.access_denied, "Invalid code verifier"); } return session.tokens(); } private AccessTokenResponse redeemDeviceCode(TokenParams params) { var session = sessionsRepo.getByAuthorizationCode(params.getDeviceCode()) - .orElseThrow(() -> new OAuthException("Invalid code")); + .orElseThrow(() -> new OAuthException(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("Authorization pending"); + throw new OAuthException(ErrorResponse.Error.authorization_pending, "Authorization pending"); } } @@ -177,24 +178,24 @@ public class OAuthResource { private OAuthClient validateClient(AuthParams params) { var client = clientsRepo.getClient(params.getClientId()) - .orElseThrow(() -> new RuntimeException("Not a valid client")); + .orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client")); if (!client.validateRedirectUri(params.getRedirectUri())) { - throw new RuntimeException("Invalid redirect URI"); + throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI"); } if (StringUtil.isNullOrEmpty(params.getState())) { - throw new RuntimeException("Invalid state"); + throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid state"); } return client; } private OAuthClient validateClient(TokenParams params, AuthorizationSession session) { var client = clientsRepo.getClient(params.getClientId()) - .orElseThrow(() -> new RuntimeException("Not a valid client")); + .orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client")); if (!session.validateRedirectUri(params.getRedirectUri())) { - throw new RuntimeException("Invalid redirect URI"); + throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI"); } if (!client.validateSecret(params.getClientSecret())) { - throw new RuntimeException("Invalid secret"); + throw new OAuthException(ErrorResponse.Error.unauthorized_client, "Invalid secret"); } return client; } diff --git a/src/main/java/com/ysoft/geecon/error/ErrorResponse.java b/src/main/java/com/ysoft/geecon/error/ErrorResponse.java new file mode 100644 index 0000000..145a7b3 --- /dev/null +++ b/src/main/java/com/ysoft/geecon/error/ErrorResponse.java @@ -0,0 +1,10 @@ +package com.ysoft.geecon.error; + +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 + } +} diff --git a/src/main/java/com/ysoft/geecon/error/ExceptionMappers.java b/src/main/java/com/ysoft/geecon/error/ExceptionMappers.java index fa5290f..7eecaea 100644 --- a/src/main/java/com/ysoft/geecon/error/ExceptionMappers.java +++ b/src/main/java/com/ysoft/geecon/error/ExceptionMappers.java @@ -35,6 +35,6 @@ class ExceptionMappers { @CheckedTemplate public static class Templates { - public static native TemplateInstance error(OAuthException.ErrorResponse response); + public static native TemplateInstance error(ErrorResponse response); } } \ No newline at end of file diff --git a/src/main/java/com/ysoft/geecon/error/OAuthException.java b/src/main/java/com/ysoft/geecon/error/OAuthException.java index ed511a3..d691fd4 100644 --- a/src/main/java/com/ysoft/geecon/error/OAuthException.java +++ b/src/main/java/com/ysoft/geecon/error/OAuthException.java @@ -1,30 +1,24 @@ package com.ysoft.geecon.error; -import com.fasterxml.jackson.annotation.JsonProperty; - public class OAuthException extends RuntimeException { private final ErrorResponse response; - // https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-error-response-2 public OAuthException(ErrorResponse response) { super("OAuth error: " + response.error() + " " + response.description()); this.response = response; } - public OAuthException(String error, String description) { + public OAuthException(ErrorResponse.Error error, String description) { this(new ErrorResponse(error, description)); } @Deprecated public OAuthException(String message) { - this(message, message); + this(ErrorResponse.Error.server_error, message); } public ErrorResponse getResponse() { return response; } - public record ErrorResponse(@JsonProperty("error") String error, - @JsonProperty("error_description") String description) { - } } diff --git a/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java b/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java index 99fb0c8..14d4e00 100644 --- a/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java +++ b/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java @@ -3,6 +3,7 @@ package com.ysoft.geecon; import com.ysoft.geecon.dto.DeviceResponse; import com.ysoft.geecon.dto.OAuthClient; import com.ysoft.geecon.dto.User; +import com.ysoft.geecon.error.ErrorResponse; import com.ysoft.geecon.helpers.ConsentScreen; import com.ysoft.geecon.helpers.DeviceAuthorizationGrantFlow; import com.ysoft.geecon.helpers.DeviceCodeScreen; @@ -71,6 +72,7 @@ public class DeviceAuthGrantTest { public void deviceAuthGrant_authorizationPending() throws IOException { DeviceAuthorizationGrantFlow flow = new DeviceAuthorizationGrantFlow(deviceUri, CLIENT); flow.start(); - flow.exchangeDeviceCodeError(); + ErrorResponse errorResponse = flow.exchangeDeviceCodeError(); + assertThat(errorResponse.error(), is(ErrorResponse.Error.authorization_pending)); } } \ No newline at end of file diff --git a/src/test/java/com/ysoft/geecon/helpers/DeviceAuthorizationGrantFlow.java b/src/test/java/com/ysoft/geecon/helpers/DeviceAuthorizationGrantFlow.java index d5143f1..2e6152c 100644 --- a/src/test/java/com/ysoft/geecon/helpers/DeviceAuthorizationGrantFlow.java +++ b/src/test/java/com/ysoft/geecon/helpers/DeviceAuthorizationGrantFlow.java @@ -3,7 +3,7 @@ package com.ysoft.geecon.helpers; import com.ysoft.geecon.dto.AccessTokenResponse; import com.ysoft.geecon.dto.DeviceResponse; import com.ysoft.geecon.dto.OAuthClient; -import com.ysoft.geecon.error.OAuthException; +import com.ysoft.geecon.error.ErrorResponse; import java.io.IOException; @@ -56,7 +56,7 @@ public class DeviceAuthorizationGrantFlow { .extract().as(AccessTokenResponse.class); } - public OAuthException.ErrorResponse exchangeDeviceCodeError() { + public ErrorResponse exchangeDeviceCodeError() { return given() .formParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code") .formParam("client_id", client.clientId()) @@ -68,6 +68,6 @@ public class DeviceAuthorizationGrantFlow { .contentType(JSON) .body("error", is(notNullValue())) .body("error_description", is(notNullValue())) - .extract().as(OAuthException.ErrorResponse.class); + .extract().as(ErrorResponse.class); } }