Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,41 @@ ArcadeClient clientWithOptions = client.withOptions(optionsBuilder -> {

The `withOptions()` method does not affect the original client or service.

## User Authentication

To initiate an OAuth 2 authenticated flow with a user, use the `AuthService.start` method:

```java
import dev.arcade.client.ArcadeClient;
import dev.arcade.models.AuthorizationResponse;

// See above on creating a client
ArcadeClient client;

// get the auth service, and call start
AuthorizationResponse authResponse = client.auth().start(
"{arcade_user_id}", // email or user ID of an Arcade user
"{auth_provider}", // provider name
"oauth2", // provider type
List.of("{scope1}", "{scope2}")); // list of scopes

// check the response status
authResponse.status()
.filter(status -> status != AuthorizationResponse.Status.COMPLETED)
.ifPresent(status ->
System.out.println("Click this link to authorize: " + authResponse.url().get()));
```
```java
// if the authorization is NOT complete, you can wait using the following method:
client.auth().waitForCompletion(authResponse);
```

> [!CAUTION]
> This method should not be used in web applications as it will block the current thread.
> For web apps, you will need to poll the `status` endpoint by calling `client.auth.status(...)`.

For more details, see the [Authorized Tool Calling](https://docs.arcade.dev/en/guides/tool-calling/custom-apps/auth-tool-calling) docs.

## Requests and responses

To send a request to the Arcade API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,25 @@ interface AuthServiceAsync {
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture<HttpResponseFor<AuthorizationResponse>>
}

// -------------------------------------------------------------------------
// Start of manually added code
// -------------------------------------------------------------------------

/**
* Starts the authorization process for a given provider and scopes.
*
* @param userId The user ID for which authorization is being requested.
* @param provider The authorization provider (e.g., 'github', 'google', 'linkedin',
* 'microsoft', 'slack', 'spotify', 'x', 'zoom').
* @param providerType The type of authorization provider. Optional, defaults to 'oauth2'.
* @param scopes A list of scopes required for authorization, if any.
* @return A CompletableFuture containing the authorization response based on the request.
*/
fun start(
userId: String,
provider: String,
providerType: String = "oauth2",
scopes: List<String> = emptyList(),
): CompletableFuture<AuthorizationResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import dev.arcade.core.prepareAsync
import dev.arcade.models.AuthorizationResponse
import dev.arcade.models.auth.AuthAuthorizeParams
import dev.arcade.models.auth.AuthConfirmUserParams
import dev.arcade.models.auth.AuthRequest
import dev.arcade.models.auth.AuthStatusParams
import dev.arcade.models.auth.ConfirmUserResponse
import java.util.concurrent.CompletableFuture
Expand Down Expand Up @@ -161,4 +162,36 @@ class AuthServiceAsyncImpl internal constructor(private val clientOptions: Clien
}
}
}

// -------------------------------------------------------------------------
// Start of manually added code
// -------------------------------------------------------------------------

override fun start(
userId: String,
provider: String,
providerType: String,
scopes: List<String>,
): CompletableFuture<AuthorizationResponse> {
return authorize(
AuthAuthorizeParams.builder()
.authRequest(
AuthRequest.builder()
.userId(userId)
.authRequirement(
AuthRequest.AuthRequirement.builder()
.providerId(provider)
.providerType(providerType)
.oauth2(
AuthRequest.AuthRequirement.Oauth2.builder()
.scopes(scopes)
.build()
)
.build()
)
.build()
)
.build()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,43 @@ interface AuthService {
requestOptions: RequestOptions = RequestOptions.none(),
): HttpResponseFor<AuthorizationResponse>
}

// -------------------------------------------------------------------------
// Start of manually added code
// -------------------------------------------------------------------------

/**
* Starts the authorization process for a given provider and scopes.
*
* @param userId The user ID for which authorization is being requested.
* @param provider The authorization provider (e.g., 'github', 'google', 'linkedin',
* 'microsoft', 'slack', 'spotify', 'x', 'zoom').
* @param providerType The type of authorization provider. Optional, defaults to 'oauth2'.
* @param scopes A list of scopes required for authorization, if any.
* @return The authorization response based on the request.
*/
fun start(
userId: String,
provider: String,
providerType: String = "oauth2",
scopes: List<String> = emptyList(),
): AuthorizationResponse

/**
* Waits for the authorization process to complete, for example,
* <pre><code>
* val authResponse = client.auth().start("you@example.com", "github")
* val authResult = client.auth().waitForCompletion(authResponse)
* </code></pre>
*/
fun waitForCompletion(authorizationResponse: AuthorizationResponse): AuthorizationResponse

/**
* Waits for the authorization process to complete, for example,
* <pre><code>
* val authResponse = client.auth().start("you@example.com", "github")
* val authResult = client.auth().waitForCompletion(authResponse.id().get())
* </code></pre>
*/
fun waitForCompletion(authorizationResponseId: String): AuthorizationResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ import dev.arcade.core.prepare
import dev.arcade.models.AuthorizationResponse
import dev.arcade.models.auth.AuthAuthorizeParams
import dev.arcade.models.auth.AuthConfirmUserParams
import dev.arcade.models.auth.AuthRequest
import dev.arcade.models.auth.AuthStatusParams
import dev.arcade.models.auth.ConfirmUserResponse
import java.util.function.Consumer

class AuthServiceImpl internal constructor(private val clientOptions: ClientOptions) : AuthService {

companion object {
const val DEFAULT_LONGPOLL_WAIT_TIME = 45L
}

private val withRawResponse: AuthService.WithRawResponse by lazy {
WithRawResponseImpl(clientOptions)
}
Expand Down Expand Up @@ -150,4 +155,63 @@ class AuthServiceImpl internal constructor(private val clientOptions: ClientOpti
}
}
}

// -------------------------------------------------------------------------
// Start of manually added code
// -------------------------------------------------------------------------

override fun start(
userId: String,
provider: String,
providerType: String,
scopes: List<String>,
): AuthorizationResponse {
return authorize(
AuthAuthorizeParams.builder()
.authRequest(
AuthRequest.builder()
.userId(userId)
.authRequirement(
AuthRequest.AuthRequirement.builder()
.providerId(provider)
.providerType(providerType)
.oauth2(
AuthRequest.AuthRequirement.Oauth2.builder()
.scopes(scopes)
.build()
)
.build()
)
.build()
)
.build()
)
}

override fun waitForCompletion(
authorizationResponse: AuthorizationResponse
): AuthorizationResponse {
var response = authorizationResponse
while (AuthorizationResponse.Status.COMPLETED != response.status().get()) {
response =
status(
AuthStatusParams.builder()
.id(response.id().get())
.wait(DEFAULT_LONGPOLL_WAIT_TIME)
.build()
)
}
return response
}

override fun waitForCompletion(authorizationResponseId: String): AuthorizationResponse {
return waitForCompletion(
status(
AuthStatusParams.builder()
.id(authorizationResponseId)
.wait(DEFAULT_LONGPOLL_WAIT_TIME)
.build()
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
package dev.arcade.services.async

import dev.arcade.TestServerExtension
import dev.arcade.client.okhttp.ArcadeOkHttpClient
import dev.arcade.client.okhttp.ArcadeOkHttpClientAsync
import dev.arcade.models.auth.AuthAuthorizeParams
import dev.arcade.models.auth.AuthRequest
import dev.arcade.models.auth.AuthStatusParams
import dev.arcade.models.auth.ConfirmUserRequest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify

@ExtendWith(TestServerExtension::class)
internal class AuthServiceAsyncTest {
Expand Down Expand Up @@ -79,4 +83,104 @@ internal class AuthServiceAsyncTest {
val authorizationResponse = authorizationResponseFuture.get()
authorizationResponse.validate()
}

// -------------------------------------------------------------------------
// Start of manually added code
// -------------------------------------------------------------------------

@Test
fun start() {
val expected =
AuthAuthorizeParams.builder()
.authRequest(
AuthRequest.builder()
.userId("user_id")
.authRequirement(
AuthRequest.AuthRequirement.builder()
.providerId("provider_id")
.providerType("provider_type")
.oauth2(
AuthRequest.AuthRequirement.Oauth2.builder()
.scopes(listOf("scope_one", "scope_two"))
.build()
)
.build()
)
.build()
)
.build()

verifyAuthorize(expected) { auth ->
auth.start("user_id", "provider_id", "provider_type", listOf("scope_one", "scope_two"))
}
}

@Test
fun start_noScopes() {
val expected =
AuthAuthorizeParams.builder()
.authRequest(
AuthRequest.builder()
.userId("user_id")
.authRequirement(
AuthRequest.AuthRequirement.builder()
.providerId("provider_id")
.providerType("provider_type")
.oauth2(
AuthRequest.AuthRequirement.Oauth2.builder()
.scopes(emptyList())
.build()
)
.build()
)
.build()
)
.build()

verifyAuthorize(expected) { auth -> auth.start("user_id", "provider_id", "provider_type") }
}

@Test
fun start_noProviderType() {
val expected =
AuthAuthorizeParams.builder()
.authRequest(
AuthRequest.builder()
.userId("user_id")
.authRequirement(
AuthRequest.AuthRequirement.builder()
.providerId("provider_id")
.providerType("oauth2")
.oauth2(
AuthRequest.AuthRequirement.Oauth2.builder()
.scopes(emptyList())
.build()
)
.build()
)
.build()
)
.build()

verifyAuthorize(expected) { auth -> auth.start("user_id", "provider_id") }
}

private fun verifyAuthorize(
expected: AuthAuthorizeParams,
testCode: (AuthServiceAsync) -> Unit,
) {
// given
val client =
ArcadeOkHttpClient.builder()
.baseUrl(TestServerExtension.BASE_URL)
.apiKey("My API Key")
.build()
val auth = spy(client.async().auth())

// when
testCode.invoke(auth)

// then
verify(auth).authorize(expected)
}
}
Loading