From 26038be75d637da2b731743fcef310a966e8138f Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:41:01 +0400 Subject: [PATCH] Changed clientSecret to optional for token exchange methods; defaults to API Key now (#193) # Description clientSecret is not required if the API Key used for the SDK and the clientId belong to the same application. # License I confirm that this contribution is made under the terms of the MIT license and that I have the authority necessary to make this contribution on behalf of its copyright owner. --- CHANGELOG.md | 5 ++ .../com/nylas/models/CodeExchangeRequest.kt | 14 +++- .../com/nylas/models/TokenExchangeRequest.kt | 38 ++++++++- src/main/kotlin/com/nylas/resources/Auth.kt | 6 ++ .../kotlin/com/nylas/resources/AuthTests.kt | 81 ++++++++++++++++++- 5 files changed, 138 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4eea5f0..9c40e76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Nylas Java SDK Changelog +## [2.0.0-beta.5] - TBD + +### Changed +* Changed `clientSecret` to optional for token exchange methods; defaults to API Key now + ## [2.0.0-beta.4] - Released 2024-01-09 ### BREAKING CHANGES diff --git a/src/main/kotlin/com/nylas/models/CodeExchangeRequest.kt b/src/main/kotlin/com/nylas/models/CodeExchangeRequest.kt index 00e46e68..6899a7b9 100644 --- a/src/main/kotlin/com/nylas/models/CodeExchangeRequest.kt +++ b/src/main/kotlin/com/nylas/models/CodeExchangeRequest.kt @@ -23,9 +23,10 @@ data class CodeExchangeRequest( var clientId: String, /** * Client secret of the application. + * If not provided, the API Key will be used instead. */ @Json(name = "client_secret") - var clientSecret: String, + var clientSecret: String? = null, /** * The original plain text code verifier (code_challenge) used in the initial authorization request (PKCE). */ @@ -44,16 +45,23 @@ data class CodeExchangeRequest( * @param redirectUri Should match the same redirect URI that was used for getting the code during the initial authorization request. * @param code OAuth 2.0 code fetched from the previous step. * @param clientId Client ID of the application. - * @param clientSecret Client secret of the application. */ data class Builder( private val redirectUri: String, private val code: String, private val clientId: String, - private val clientSecret: String, ) { + private var clientSecret: String? = null private var codeVerifier: String? = null + /** + * Set the client secret. + * If not provided, the API Key will be used instead. + * @param clientSecret The client secret. + * @return The builder. + */ + fun clientSecret(clientSecret: String) = apply { this.clientSecret = clientSecret } + /** * Set the code verifier. * Should be the original plain text code verifier (code_challenge) used in the initial authorization request (PKCE). diff --git a/src/main/kotlin/com/nylas/models/TokenExchangeRequest.kt b/src/main/kotlin/com/nylas/models/TokenExchangeRequest.kt index bbbdb4da..60e6a307 100644 --- a/src/main/kotlin/com/nylas/models/TokenExchangeRequest.kt +++ b/src/main/kotlin/com/nylas/models/TokenExchangeRequest.kt @@ -23,13 +23,49 @@ data class TokenExchangeRequest( var clientId: String, /** * Client secret of the application. + * If not provided, the API Key will be used instead. */ @Json(name = "client_secret") - var clientSecret: String, + var clientSecret: String? = null, ) { /** * The grant type for the request. For refreshing tokens, it should always be 'refresh_token'. */ @Json(name = "grant_type") private var grantType: String = "refresh_token" + + /** + * A builder for creating a [TokenExchangeRequest]. + * + * @param redirectUri Should match the same redirect URI that was used for getting the code during the initial authorization request. + * @param refreshToken Token to refresh/request your short-lived access token + * @param clientId Client ID of the application. + */ + data class Builder( + private val redirectUri: String, + private val refreshToken: String, + private val clientId: String, + ) { + private var clientSecret: String? = null + + /** + * Set the client secret. + * If not provided, the API Key will be used instead. + * @param clientSecret The client secret. + * @return The builder. + */ + fun clientSecret(clientSecret: String) = apply { this.clientSecret = clientSecret } + + /** + * Build the [TokenExchangeRequest]. + * + * @return The [TokenExchangeRequest]. + */ + fun build() = TokenExchangeRequest( + redirectUri = redirectUri, + refreshToken = refreshToken, + clientId = clientId, + clientSecret = clientSecret, + ) + } } diff --git a/src/main/kotlin/com/nylas/resources/Auth.kt b/src/main/kotlin/com/nylas/resources/Auth.kt index 5772339e..a9d971dc 100644 --- a/src/main/kotlin/com/nylas/resources/Auth.kt +++ b/src/main/kotlin/com/nylas/resources/Auth.kt @@ -38,6 +38,9 @@ class Auth(private val client: NylasClient) { @Throws(NylasOAuthError::class, NylasSdkTimeoutError::class) fun exchangeCodeForToken(request: CodeExchangeRequest): CodeExchangeResponse { val path = "v3/connect/token" + if (request.clientSecret == null) { + request.clientSecret = client.apiKey + } val serializedRequestBody = JsonHelper.moshi() .adapter(CodeExchangeRequest::class.java) @@ -108,6 +111,9 @@ class Auth(private val client: NylasClient) { @Throws(NylasOAuthError::class, NylasSdkTimeoutError::class) fun refreshAccessToken(request: TokenExchangeRequest): CodeExchangeResponse { val path = "v3/connect/token" + if (request.clientSecret == null) { + request.clientSecret = client.apiKey + } val serializedRequestBody = JsonHelper.moshi() .adapter(TokenExchangeRequest::class.java) diff --git a/src/test/kotlin/com/nylas/resources/AuthTests.kt b/src/test/kotlin/com/nylas/resources/AuthTests.kt index 12ee0d76..059a1083 100644 --- a/src/test/kotlin/com/nylas/resources/AuthTests.kt +++ b/src/test/kotlin/com/nylas/resources/AuthTests.kt @@ -45,6 +45,7 @@ class AuthTests { grantId = "abc-123-grant-id" mockNylasClient = Mockito.mock(NylasClient::class.java) whenever(mockNylasClient.newUrlBuilder()).thenReturn(HttpUrl.get(baseUrl).newBuilder()) + whenever(mockNylasClient.apiKey).thenReturn("test-api-key") auth = Auth(mockNylasClient) } @@ -101,7 +102,7 @@ class AuthTests { @Nested inner class TokenTests { @Test - fun `exchangeCodeForToken should return the correct URL`() { + fun `exchangeCodeForToken calls requests with the correct params`() { val adapter = JsonHelper.moshi().adapter(CodeExchangeRequest::class.java) val request = CodeExchangeRequest( redirectUri = "https://example.com/oauth/callback", @@ -133,7 +134,46 @@ class AuthTests { } @Test - fun `refreshAccessToken should return the correct URL`() { + fun `exchangeCodeForToken clientSecret defaults to API key`() { + val adapter = JsonHelper.moshi().adapter(CodeExchangeRequest::class.java) + val request = CodeExchangeRequest( + redirectUri = "https://example.com/oauth/callback", + code = "abc-123-code", + clientId = "abc-123", + codeVerifier = "abc-123-verifier", + ) + val expectedRequest = mapOf( + "redirect_uri" to "https://example.com/oauth/callback", + "code" to "abc-123-code", + "client_id" to "abc-123", + "client_secret" to "test-api-key", + "code_verifier" to "abc-123-verifier", + "grant_type" to "authorization_code", + ) + + auth.exchangeCodeForToken(request) + + val json = adapter.toJson(request) + val jsonMap = JsonHelper.jsonToMap(json) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture(), + ) + + assertEquals("v3/connect/token", pathCaptor.firstValue) + assertEquals(CodeExchangeResponse::class.java, typeCaptor.firstValue) + assertEquals(jsonMap, expectedRequest) + assertEquals("authorization_code", jsonMap["grant_type"]) + } + + @Test + fun `refreshAccessToken calls requests with the correct params`() { val adapter = JsonHelper.moshi().adapter(TokenExchangeRequest::class.java) val request = TokenExchangeRequest( redirectUri = "https://example.com/oauth/callback", @@ -162,6 +202,43 @@ class AuthTests { assertEquals(json, requestBodyCaptor.firstValue) assertEquals("refresh_token", jsonMap["grant_type"]) } + + @Test + fun `refreshAccessToken clientSecret defaults to API`() { + val adapter = JsonHelper.moshi().adapter(TokenExchangeRequest::class.java) + val request = TokenExchangeRequest( + redirectUri = "https://example.com/oauth/callback", + refreshToken = "abc-123-refresh-token", + clientId = "abc-123", + ) + val expectedRequest = mapOf( + "redirect_uri" to "https://example.com/oauth/callback", + "client_id" to "abc-123", + "client_secret" to "test-api-key", + "refresh_token" to "abc-123-refresh-token", + "grant_type" to "refresh_token", + ) + + auth.refreshAccessToken(request) + + val json = adapter.toJson(request) + val jsonMap = JsonHelper.jsonToMap(json) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture(), + ) + + assertEquals("v3/connect/token", pathCaptor.firstValue) + assertEquals(CodeExchangeResponse::class.java, typeCaptor.firstValue) + assertEquals(jsonMap, expectedRequest) + assertEquals("refresh_token", jsonMap["grant_type"]) + } } @Nested