[Fix #1544] auth Content to be completable future - #1545
Conversation
d48e441 to
ceebc05
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the auth subsystem to support non-blocking authentication flows by changing AuthProvider.content(...) (and related token/provider APIs) to return CompletableFuture, and propagates that async behavior through HTTP/OpenAPI execution paths.
Changes:
- Update
AuthProvider.content(...)andAccessTokenProvider.validateAndGet(...)to returnCompletableFuture, and adjust built-in auth providers accordingly. - Refactor HTTP request execution to be future-based end-to-end (including auth header injection) and move OpenAPI parsing onto the workflow executor.
- Refactor JWT representation (replace impl-specific class with a core
JWTrecord) and update/add tests.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| impl/test/src/test/java/io/serverlessworkflow/impl/test/CustomAuthProviderFactoryOverrideTest.java | Updates test auth provider override to the new async content() signature. |
| impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java | Moves OpenAPI parsing to supplyAsync and composes HTTP executor application via futures. |
| impl/jwt-impl/src/test/java/io/serverlessworkflow/impl/http/jwt/JacksonJWTImplTest.java | Updates JWT tests to align with the new JWT representation/claim access patterns. |
| impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTImpl.java | Removes the old JWT implementation class (replaced by core JWT record). |
| impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java | Instantiates the new core JWT record during token conversion. |
| impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestExecutor.java | Changes request executor contract to return CompletableFuture<WorkflowModel>. |
| impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java | Delegates directly to async RequestExecutor rather than wrapping in supplyAsync. |
| impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java | Adapts token provider API to return CompletableFuture<JWT>. |
| impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java | Adds async auth header injection and returns a CompletableFuture request result. |
| impl/core/src/test/java/io/serverlessworkflow/impl/auth/JWTTest.java | Adds tests for the new JWT.toCollection(...) helper. |
| impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java | Adapts resource loading auth header creation to async AuthProvider.content(...) via .join(). |
| impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java | Refactors script context construction to happen inside the executed runnable/callable. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/OpenIdAuthProvider.java | Makes provider class public (and aligns with updated async base provider behavior). |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/OAuth2AuthProvider.java | Makes provider class public (and aligns with updated async base provider behavior). |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java | Replaces interface with a record and adds helpers for time/collection claim decoding. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/DigestAuthProvider.java | Adapts digest auth header generation to return a CompletableFuture<String>. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/DefaultAuthProviderFactory.java | Adds protected factory methods to enable overriding provider instantiation. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java | Returns async auth content by composing token provider futures. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/BearerAuthProvider.java | Wraps bearer token computation in a completed future. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java | Wraps basic auth computation in a completed future. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/AuthProvider.java | Changes content(...) to return CompletableFuture<String>. |
| impl/core/src/main/java/io/serverlessworkflow/impl/auth/AccessTokenProvider.java | Changes validateAndGet(...) to return CompletableFuture<JWT>. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java:62
validateAndGet()now returnsCompletableFuture<JWT>, but it still throws exceptions synchronously (e.g., issuer validation or token fetch failures). That breaks async composition because callers may never receive a failed future (the exception escapes before a future is returned).
public CompletableFuture<JWT> validateAndGet(
WorkflowContext workflow, TaskContext context, WorkflowModel model) {
Map<String, Object> token = invoke(workflow, context, model);
JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
if (issuers != null && !issuers.isEmpty()) {
impl/jwt-impl/src/main/java/io/serverlessworkflow/impl/executors/http/oauth/jackson/JacksonJWTConverter.java:39
- JWT parsing should validate the expected 3-part structure (header.payload.signature). The current check allows unexpected part counts and the error message mentions ':' even though the separator is '.'.
if (parts.length < 2) {
throw new IllegalArgumentException(
"Invalid JWT token format. There should at least two parts separated by :");
}
return new JWT(token, fromPart2Map(parts[0]), fromPart2Map(parts[1]));
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java:52
type()usesOptional.of((String) header.get("typ")), which will throw a NullPointerException if the header contains the "typ" key with a null value. Prefer a null-safe fallback to the claim value.
public Optional<String> type() {
return header.containsKey("typ")
? Optional.of((String) header.get("typ"))
: Optional.ofNullable((String) claims.get("typ"));
}
impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java:117
auth.content(...).join()re-introduces blocking in the resource loading path. This can negate the benefit of the async AuthProvider API and can lead to thread starvation/deadlocks if the auth future is completed on the same executor that is currently callingload()(e.g., whenload()is invoked insidesupplyAsync). Consider adding an asyncload...variant (or otherwise composing the auth future) so callers can remain non-blocking.
.map(
auth ->
AuthUtils.authHeaderValue(
auth.scheme(),
auth.content(workflowContext, taskContext, model, uri).join())));
impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java:67
- In the non-
awaitbranch, movingScriptContextconstruction into the submitted task changes semantics: any failures inargumentExpr/environmentExpr/codeSupplierwill now happen asynchronously and be effectively dropped (since the returnedCompletableFutureis already completed). Previously those errors surfaced synchronously fromapply(), and the script input was snapshotted at submission time. Consider constructingScriptContextbefore submitting so invalid expressions/config are still detected and the context is deterministic.
} else {
workflowContext
.definition()
.application()
.executorService()
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java:62
validateAndGet()now returns aCompletableFuture, but it can still throw synchronously (frominvoke(),fromToken(), or issuer validation). This breaks async composition (e.g.,CommonOAuthProvider.content(...).thenApply(...)will throw instead of returning a future completed exceptionally). Wrap the body and return a failed future on exception.
public CompletableFuture<JWT> validateAndGet(
WorkflowContext workflow, TaskContext context, WorkflowModel model) {
Map<String, Object> token = invoke(workflow, context, model);
JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
if (issuers != null && !issuers.isEmpty()) {
158d1f3 to
fe11779
Compare
… future Signed-off-by: fjtirado <ftirados@ibm.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java:62
- validateAndGet() now returns a CompletableFuture, but it can still throw synchronously (e.g., issuer validation failures or invoke()/fromToken exceptions). That breaks async call chains; wrap the body and return a failed future on exceptions.
public CompletableFuture<JWT> validateAndGet(
WorkflowContext workflow, TaskContext context, WorkflowModel model) {
Map<String, Object> token = invoke(workflow, context, model);
JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
if (issuers != null && !issuers.isEmpty()) {
impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java:27
- JWT is now a record but it doesn’t enforce non-null components or immutability. Since JacksonJWTConverter builds mutable maps, callers can mutate header/claims (and nulls can later cause NPEs). Consider validating and wrapping maps as unmodifiable in a compact record constructor.
public record JWT(String token, Map<String, Object> header, Map<String, Object> claims) {
impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java:52
- type() uses Optional.of((String) header.get("typ")) when the key exists, which can throw NPE if the header contains typ=null (or ClassCastException if it’s not a String). Using String.valueOf + null checks avoids surprising failures while keeping behavior equivalent.
public Optional<String> type() {
return header.containsKey("typ")
? Optional.of((String) header.get("typ"))
: Optional.ofNullable((String) claims.get("typ"));
}
impl/openapi/src/main/java/io/serverlessworkflow/impl/executors/openapi/OpenAPIExecutor.java:76
- Typo in comment: “Me may refactor…” should be “We may refactor…”.
// Me may refactor this even further to reuse the same executor (since the base URI is
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (6)
impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java:117
- Using
auth.content(...).join()blocks the current thread. Ifcontent()completes on the same executor that is performing the load (or has limited threads), this can deadlock and it also wraps failures in aCompletionException, making error handling less explicit. It may be safer to introduce an asyncloadAsync(...)/loadURIAsync(...)path that composes the auth future instead of joining, and update call sites that already operate on CompletableFutures to use it.
.map(
auth ->
AuthUtils.authHeaderValue(
auth.scheme(),
auth.content(workflowContext, taskContext, model, uri).join())));
impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java:88
toCollection(...)returns a rawCollectionunchanged when the claim value is aCollection, even if it contains elements that are not instances ofclazz. This can violate the method’s generic contract and surface later as aClassCastExceptionin consumers. Consider filtering/casting elements to the requested type.
if (v instanceof Collection col) {
return col;
}
impl/core/src/main/java/io/serverlessworkflow/impl/auth/BasicAuthProvider.java:71
String.getBytes()uses the platform default charset, which can make the generated Basic auth token non-deterministic across environments. Prefer an explicit charset (UTF-8) when converting the formatted credentials to bytes.
String.format(
USER_PASSWORD,
userFilter.apply(workflow, task, model),
passwordFilter.apply(workflow, task, model))
.getBytes()));
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestExecutor.java:117
auth.content(...)is invoked synchronously. If an AuthProvider performs blocking work before returning its CompletableFuture (e.g., builds a completedFuture after doing IO), this will block the caller thread and can defeat the goal of async auth; it can also deadlock if called from a constrained executor. Consider offloading the invocation ofauth.contentonto the workflow executor before composing the returned future.
String scheme = auth.scheme();
return auth.content(workflow, task, model, uri)
.thenAccept(
impl/core/src/main/java/io/serverlessworkflow/impl/auth/JWT.java:27
JWTnow stores the provided header/claims maps directly. SinceJacksonJWTConverterbuilds mutableMapinstances, callers can currently mutatejwt.header()/jwt.claims()which can lead to surprising behavior and makes JWT effectively non-immutable. Consider defensively copying to unmodifiable maps in the record constructor (and validating non-null inputs).
public record JWT(String token, Map<String, Object> header, Map<String, Object> claims) {
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/auth/JaxRSAccessTokenProvider.java:72
validateAndGet(...)now returns aCompletableFuture, but it can still throw synchronously (e.g., frominvoke(...),fromToken(...), or issuer validation). This breaks async composition (callers usingthenApply/thenComposemay see immediate exceptions instead of an exceptionally-completed future) and can causeAuthProvider.content()to throw despite its async signature. Wrap the body in try/catch and return a failed future on error (optionally also offload the blocking HTTP call to an executor).
public CompletableFuture<JWT> validateAndGet(
WorkflowContext workflow, TaskContext context, WorkflowModel model) {
Map<String, Object> token = invoke(workflow, context, model);
JWT jwt = jwtConverter.fromToken((String) token.get("access_token"));
if (issuers != null && !issuers.isEmpty()) {
jwt.issuer()
.ifPresent(
issuer -> {
if (!issuers.contains(issuer)) {
throw new IllegalStateException("Token issuer is not valid: " + issuer);
}
});
}
return CompletableFuture.completedFuture(jwt);
}
Fix #1544