diff --git a/src/main/kotlin/com/workos/WorkOS.kt b/src/main/kotlin/com/workos/WorkOS.kt index 4ef78ec2..e7a9d61b 100644 --- a/src/main/kotlin/com/workos/WorkOS.kt +++ b/src/main/kotlin/com/workos/WorkOS.kt @@ -9,10 +9,12 @@ import com.workos.auditlogs.AuditLogsApi import com.workos.common.exceptions.BadRequestException import com.workos.common.exceptions.GenericServerException import com.workos.common.exceptions.NotFoundException +import com.workos.common.exceptions.OAuthException import com.workos.common.exceptions.UnauthorizedException import com.workos.common.exceptions.UnprocessableEntityException import com.workos.common.http.BadRequestExceptionResponse import com.workos.common.http.GenericErrorResponse +import com.workos.common.http.OAuthErrorResponse import com.workos.common.http.RequestConfig import com.workos.common.http.UnprocessableEntityExceptionResponse import com.workos.directorysync.DirectorySyncApi @@ -363,8 +365,15 @@ class WorkOS( when (val status = response.statusCode) { 400 -> { - val responseData = mapper.readValue(payload, BadRequestExceptionResponse::class.java) - throw BadRequestException(responseData.message, responseData.code, responseData.errors, requestId) + // Try to parse as OAuth error first + val isOAuthError = payload.contains("\"error\"") && payload.contains("\"error_description\"") + if (isOAuthError) { + val oauthError = mapper.readValue(payload, OAuthErrorResponse::class.java) + throw OAuthException(oauthError.error, oauthError.error_description, requestId) + } else { + val responseData = mapper.readValue(payload, BadRequestExceptionResponse::class.java) + throw BadRequestException(responseData.message, responseData.code, responseData.errors, requestId) + } } 401 -> { diff --git a/src/main/kotlin/com/workos/common/exceptions/OAuthException.kt b/src/main/kotlin/com/workos/common/exceptions/OAuthException.kt new file mode 100644 index 00000000..9f243998 --- /dev/null +++ b/src/main/kotlin/com/workos/common/exceptions/OAuthException.kt @@ -0,0 +1,14 @@ +package com.workos.common.exceptions + +/** + * Thrown when the API returns an OAuth error (e.g., invalid_grant). + * + * @param error The OAuth error code. + * @param errorDescription The OAuth error description. + * @param requestId The ID of the correlating request specified in the 'X-Request-ID' header. + */ +class OAuthException( + val error: String?, + val errorDescription: String?, + val requestId: String +) : Exception(errorDescription) diff --git a/src/main/kotlin/com/workos/common/http/OAuthErrorResponse.kt b/src/main/kotlin/com/workos/common/http/OAuthErrorResponse.kt new file mode 100644 index 00000000..f348fadf --- /dev/null +++ b/src/main/kotlin/com/workos/common/http/OAuthErrorResponse.kt @@ -0,0 +1,6 @@ +package com.workos.common.http + +data class OAuthErrorResponse( + val error: String?, + val error_description: String? +) diff --git a/src/test/kotlin/com/workos/test/user_management/OAuthErrorHandlingTest.kt b/src/test/kotlin/com/workos/test/user_management/OAuthErrorHandlingTest.kt new file mode 100644 index 00000000..47209417 --- /dev/null +++ b/src/test/kotlin/com/workos/test/user_management/OAuthErrorHandlingTest.kt @@ -0,0 +1,38 @@ + +package com.workos.test.user_management + +import com.workos.common.exceptions.OAuthException +import com.workos.test.TestBase +import org.junit.jupiter.api.Assertions.assertThrows +import kotlin.test.Test + +class OAuthErrorHandlingTest : TestBase() { + val workos = createWorkOSClient() + + @Test + fun authenticateWithCodeShouldThrowOAuthExceptionOnInvalidGrant() { + stubResponse( + "/user_management/authenticate", + """ + { + "error" : "invalid_grant", + "error_description" : "The code 'D01K0EWQ9V6SYP9F5D14QPHBQ8Edd' has expired or is invalid." + } + """, + responseStatus = 400, + requestBody = """{ + "client_id": "client_id", + "client_secret": "apiKey", + "grant_type": "authorization_code", + "code": "invalid_code" + }""" + ) + + assertThrows(OAuthException::class.java) { + workos.userManagement.authenticateWithCode( + "client_id", + "invalid_code" + ) + } + } +}