Skip to content

Commit

Permalink
Changed clientSecret to optional for token exchange methods; defaults…
Browse files Browse the repository at this point in the history
… 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
<!-- Your PR comment must contain the following line for us to merge the
PR. -->
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.
  • Loading branch information
mrashed-dev authored Jan 26, 2024
1 parent 0a796d3 commit 26038be
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 6 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/main/kotlin/com/nylas/models/CodeExchangeRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand All @@ -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).
Expand Down
38 changes: 37 additions & 1 deletion src/main/kotlin/com/nylas/models/TokenExchangeRequest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/nylas/resources/Auth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
81 changes: 79 additions & 2 deletions src/test/kotlin/com/nylas/resources/AuthTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<String>()
val typeCaptor = argumentCaptor<Type>()
val requestBodyCaptor = argumentCaptor<String>()
val queryParamCaptor = argumentCaptor<IQueryParams>()
verify(mockNylasClient).executePost<CodeExchangeResponse>(
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",
Expand Down Expand Up @@ -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<String>()
val typeCaptor = argumentCaptor<Type>()
val requestBodyCaptor = argumentCaptor<String>()
val queryParamCaptor = argumentCaptor<IQueryParams>()
verify(mockNylasClient).executePost<CodeExchangeResponse>(
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
Expand Down

0 comments on commit 26038be

Please sign in to comment.