Skip to content

Commit b5ebb4e

Browse files
committed
Add AuthService util methods to make getting started with Arcade easier
Adds `client.auth().start(...)` and `client.auth().waitForCompletion(...)` In the async client: `asyncClient.auth().start()`, async clients must handle blocking polling call manually.
1 parent cfa7d4e commit b5ebb4e

8 files changed

Lines changed: 541 additions & 0 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,41 @@ ArcadeClient clientWithOptions = client.withOptions(optionsBuilder -> {
128128

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

131+
## User Authentication
132+
133+
To initiate an OAuth 2 authenticated flow with a user, use the `AuthService.start` method:
134+
135+
```java
136+
import dev.arcade.client.ArcadeClient;
137+
import dev.arcade.models.AuthorizationResponse;
138+
139+
// See above on creating a client
140+
ArcadeClient client;
141+
142+
// get the auth service, and call start
143+
AuthorizationResponse authResponse = client.auth().start(
144+
"{arcade_user_id}", // email or user ID of an Arcade user
145+
"{auth_provider}", // provider name
146+
"oauth2", // provider type
147+
List.of("{scope1}", "{scope2}")); // list of scopes
148+
149+
// check the response status
150+
authResponse.status()
151+
.filter(status -> status != AuthorizationResponse.Status.COMPLETED)
152+
.ifPresent(status ->
153+
System.out.println("Click this link to authorize: " + authResponse.url().get()));
154+
```
155+
```java
156+
// if the authorization is NOT complete, you can wait using the following method:
157+
client.auth().waitForCompletion(authResponse);
158+
```
159+
160+
> [!CAUTION]
161+
> This method should not be used in web applications as it will block the current thread.
162+
> For web apps, you will need to poll the `status` endpoint by calling `client.auth.status(...)`.
163+
164+
For more details, see the [Authorized Tool Calling](https://docs.arcade.dev/en/guides/tool-calling/custom-apps/auth-tool-calling) docs.
165+
131166
## Requests and responses
132167

133168
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.

arcade-java-core/src/main/kotlin/dev/arcade/services/async/AuthServiceAsync.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,25 @@ interface AuthServiceAsync {
177177
requestOptions: RequestOptions = RequestOptions.none(),
178178
): CompletableFuture<HttpResponseFor<AuthorizationResponse>>
179179
}
180+
181+
// -------------------------------------------------------------------------
182+
// Start of manually added code
183+
// -------------------------------------------------------------------------
184+
185+
/**
186+
* Starts the authorization process for a given provider and scopes.
187+
*
188+
* @param userId The user ID for which authorization is being requested.
189+
* @param provider The authorization provider (e.g., 'github', 'google', 'linkedin',
190+
* 'microsoft', 'slack', 'spotify', 'x', 'zoom').
191+
* @param providerType The type of authorization provider. Optional, defaults to 'oauth2'.
192+
* @param scopes A list of scopes required for authorization, if any.
193+
* @return A CompletableFuture containing the authorization response based on the request.
194+
*/
195+
fun start(
196+
userId: String,
197+
provider: String,
198+
providerType: String = "oauth2",
199+
scopes: List<String> = emptyList(),
200+
): CompletableFuture<AuthorizationResponse>
180201
}

arcade-java-core/src/main/kotlin/dev/arcade/services/async/AuthServiceAsyncImpl.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import dev.arcade.core.prepareAsync
1818
import dev.arcade.models.AuthorizationResponse
1919
import dev.arcade.models.auth.AuthAuthorizeParams
2020
import dev.arcade.models.auth.AuthConfirmUserParams
21+
import dev.arcade.models.auth.AuthRequest
2122
import dev.arcade.models.auth.AuthStatusParams
2223
import dev.arcade.models.auth.ConfirmUserResponse
2324
import java.util.concurrent.CompletableFuture
@@ -161,4 +162,36 @@ class AuthServiceAsyncImpl internal constructor(private val clientOptions: Clien
161162
}
162163
}
163164
}
165+
166+
// -------------------------------------------------------------------------
167+
// Start of manually added code
168+
// -------------------------------------------------------------------------
169+
170+
override fun start(
171+
userId: String,
172+
provider: String,
173+
providerType: String,
174+
scopes: List<String>,
175+
): CompletableFuture<AuthorizationResponse> {
176+
return authorize(
177+
AuthAuthorizeParams.builder()
178+
.authRequest(
179+
AuthRequest.builder()
180+
.userId(userId)
181+
.authRequirement(
182+
AuthRequest.AuthRequirement.builder()
183+
.providerId(provider)
184+
.providerType(providerType)
185+
.oauth2(
186+
AuthRequest.AuthRequirement.Oauth2.builder()
187+
.scopes(scopes)
188+
.build()
189+
)
190+
.build()
191+
)
192+
.build()
193+
)
194+
.build()
195+
)
196+
}
164197
}

arcade-java-core/src/main/kotlin/dev/arcade/services/blocking/AuthService.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,43 @@ interface AuthService {
177177
requestOptions: RequestOptions = RequestOptions.none(),
178178
): HttpResponseFor<AuthorizationResponse>
179179
}
180+
181+
// -------------------------------------------------------------------------
182+
// Start of manually added code
183+
// -------------------------------------------------------------------------
184+
185+
/**
186+
* Starts the authorization process for a given provider and scopes.
187+
*
188+
* @param userId The user ID for which authorization is being requested.
189+
* @param provider The authorization provider (e.g., 'github', 'google', 'linkedin',
190+
* 'microsoft', 'slack', 'spotify', 'x', 'zoom').
191+
* @param providerType The type of authorization provider. Optional, defaults to 'oauth2'.
192+
* @param scopes A list of scopes required for authorization, if any.
193+
* @return The authorization response based on the request.
194+
*/
195+
fun start(
196+
userId: String,
197+
provider: String,
198+
providerType: String = "oauth2",
199+
scopes: List<String> = emptyList(),
200+
): AuthorizationResponse
201+
202+
/**
203+
* Waits for the authorization process to complete, for example,
204+
* <pre><code>
205+
* val authResponse = client.auth().start("you@example.com", "github")
206+
* val authResult = client.auth().waitForCompletion(authResponse)
207+
* </code></pre>
208+
*/
209+
fun waitForCompletion(authorizationResponse: AuthorizationResponse): AuthorizationResponse
210+
211+
/**
212+
* Waits for the authorization process to complete, for example,
213+
* <pre><code>
214+
* val authResponse = client.auth().start("you@example.com", "github")
215+
* val authResult = client.auth().waitForCompletion(authResponse.id().get())
216+
* </code></pre>
217+
*/
218+
fun waitForCompletion(authorizationResponseId: String): AuthorizationResponse
180219
}

arcade-java-core/src/main/kotlin/dev/arcade/services/blocking/AuthServiceImpl.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ import dev.arcade.core.prepare
1818
import dev.arcade.models.AuthorizationResponse
1919
import dev.arcade.models.auth.AuthAuthorizeParams
2020
import dev.arcade.models.auth.AuthConfirmUserParams
21+
import dev.arcade.models.auth.AuthRequest
2122
import dev.arcade.models.auth.AuthStatusParams
2223
import dev.arcade.models.auth.ConfirmUserResponse
2324
import java.util.function.Consumer
2425

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

28+
companion object {
29+
const val DEFAULT_LONGPOLL_WAIT_TIME = 45L
30+
}
31+
2732
private val withRawResponse: AuthService.WithRawResponse by lazy {
2833
WithRawResponseImpl(clientOptions)
2934
}
@@ -150,4 +155,63 @@ class AuthServiceImpl internal constructor(private val clientOptions: ClientOpti
150155
}
151156
}
152157
}
158+
159+
// -------------------------------------------------------------------------
160+
// Start of manually added code
161+
// -------------------------------------------------------------------------
162+
163+
override fun start(
164+
userId: String,
165+
provider: String,
166+
providerType: String,
167+
scopes: List<String>,
168+
): AuthorizationResponse {
169+
return authorize(
170+
AuthAuthorizeParams.builder()
171+
.authRequest(
172+
AuthRequest.builder()
173+
.userId(userId)
174+
.authRequirement(
175+
AuthRequest.AuthRequirement.builder()
176+
.providerId(provider)
177+
.providerType(providerType)
178+
.oauth2(
179+
AuthRequest.AuthRequirement.Oauth2.builder()
180+
.scopes(scopes)
181+
.build()
182+
)
183+
.build()
184+
)
185+
.build()
186+
)
187+
.build()
188+
)
189+
}
190+
191+
override fun waitForCompletion(
192+
authorizationResponse: AuthorizationResponse
193+
): AuthorizationResponse {
194+
var response = authorizationResponse
195+
while (AuthorizationResponse.Status.COMPLETED != response.status().get()) {
196+
response =
197+
status(
198+
AuthStatusParams.builder()
199+
.id(response.id().get())
200+
.wait(DEFAULT_LONGPOLL_WAIT_TIME)
201+
.build()
202+
)
203+
}
204+
return response
205+
}
206+
207+
override fun waitForCompletion(authorizationResponseId: String): AuthorizationResponse {
208+
return waitForCompletion(
209+
status(
210+
AuthStatusParams.builder()
211+
.id(authorizationResponseId)
212+
.wait(DEFAULT_LONGPOLL_WAIT_TIME)
213+
.build()
214+
)
215+
)
216+
}
153217
}

arcade-java-core/src/test/kotlin/dev/arcade/services/async/AuthServiceAsyncTest.kt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
package dev.arcade.services.async
44

55
import dev.arcade.TestServerExtension
6+
import dev.arcade.client.okhttp.ArcadeOkHttpClient
67
import dev.arcade.client.okhttp.ArcadeOkHttpClientAsync
8+
import dev.arcade.models.auth.AuthAuthorizeParams
79
import dev.arcade.models.auth.AuthRequest
810
import dev.arcade.models.auth.AuthStatusParams
911
import dev.arcade.models.auth.ConfirmUserRequest
1012
import org.junit.jupiter.api.Test
1113
import org.junit.jupiter.api.extension.ExtendWith
14+
import org.mockito.kotlin.spy
15+
import org.mockito.kotlin.verify
1216

1317
@ExtendWith(TestServerExtension::class)
1418
internal class AuthServiceAsyncTest {
@@ -79,4 +83,104 @@ internal class AuthServiceAsyncTest {
7983
val authorizationResponse = authorizationResponseFuture.get()
8084
authorizationResponse.validate()
8185
}
86+
87+
// -------------------------------------------------------------------------
88+
// Start of manually added code
89+
// -------------------------------------------------------------------------
90+
91+
@Test
92+
fun start() {
93+
val expected =
94+
AuthAuthorizeParams.builder()
95+
.authRequest(
96+
AuthRequest.builder()
97+
.userId("user_id")
98+
.authRequirement(
99+
AuthRequest.AuthRequirement.builder()
100+
.providerId("provider_id")
101+
.providerType("provider_type")
102+
.oauth2(
103+
AuthRequest.AuthRequirement.Oauth2.builder()
104+
.scopes(listOf("scope_one", "scope_two"))
105+
.build()
106+
)
107+
.build()
108+
)
109+
.build()
110+
)
111+
.build()
112+
113+
verifyAuthorize(expected) { auth ->
114+
auth.start("user_id", "provider_id", "provider_type", listOf("scope_one", "scope_two"))
115+
}
116+
}
117+
118+
@Test
119+
fun start_noScopes() {
120+
val expected =
121+
AuthAuthorizeParams.builder()
122+
.authRequest(
123+
AuthRequest.builder()
124+
.userId("user_id")
125+
.authRequirement(
126+
AuthRequest.AuthRequirement.builder()
127+
.providerId("provider_id")
128+
.providerType("provider_type")
129+
.oauth2(
130+
AuthRequest.AuthRequirement.Oauth2.builder()
131+
.scopes(emptyList())
132+
.build()
133+
)
134+
.build()
135+
)
136+
.build()
137+
)
138+
.build()
139+
140+
verifyAuthorize(expected) { auth -> auth.start("user_id", "provider_id", "provider_type") }
141+
}
142+
143+
@Test
144+
fun start_noProviderType() {
145+
val expected =
146+
AuthAuthorizeParams.builder()
147+
.authRequest(
148+
AuthRequest.builder()
149+
.userId("user_id")
150+
.authRequirement(
151+
AuthRequest.AuthRequirement.builder()
152+
.providerId("provider_id")
153+
.providerType("oauth2")
154+
.oauth2(
155+
AuthRequest.AuthRequirement.Oauth2.builder()
156+
.scopes(emptyList())
157+
.build()
158+
)
159+
.build()
160+
)
161+
.build()
162+
)
163+
.build()
164+
165+
verifyAuthorize(expected) { auth -> auth.start("user_id", "provider_id") }
166+
}
167+
168+
private fun verifyAuthorize(
169+
expected: AuthAuthorizeParams,
170+
testCode: (AuthServiceAsync) -> Unit,
171+
) {
172+
// given
173+
val client =
174+
ArcadeOkHttpClient.builder()
175+
.baseUrl(TestServerExtension.BASE_URL)
176+
.apiKey("My API Key")
177+
.build()
178+
val auth = spy(client.async().auth())
179+
180+
// when
181+
testCode.invoke(auth)
182+
183+
// then
184+
verify(auth).authorize(expected)
185+
}
82186
}

0 commit comments

Comments
 (0)