From d8a2f8959780ee9d9e3cc254661c6cca27f8c743 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Mon, 18 Dec 2023 22:40:26 -0800 Subject: [PATCH] Update Pac4j to 6.0.0 (#14) * Update for Pac4j 6.0.0 * Bump version, update readme * Validate credentials * Update Java version for CI --- .github/workflows/ci.yml | 2 +- README.md | 13 +- pom.xml | 6 +- src/main/java/ratpack/pac4j/RatpackPac4j.java | 205 ++++++++++-------- .../pac4j/internal/Pac4jAuthenticator.java | 7 +- .../pac4j/internal/RatpackWebContext.java | 16 +- 6 files changed, 142 insertions(+), 107 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a5cb3..2727650 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: Java CI env: - JDK_CURRENT: 11.0.10 + JDK_CURRENT: 17.0.9+8 DISTRIBUTION: zulu on: diff --git a/README.md b/README.md index 0c818b4..555bc80 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ This org.pac4j:ratpack-pac4j library is a mirror of the official Ratpack / pac4j module (io.ratpack:ratpack-pac4j) with newer versions of pac4j (as the Ratpack 1.x stream is stuck to pac4j v1.8.x). -Pac4j | Ratpack | New org.pac4j:ratpack-pac4j | Changes | Official io.ratpack:ratpack-pac4j -------|---------|------------------------|---------|---------------------------------- -v1.8.x | v1.4.6 | v1.4.6 | No changes: both modules are identical | v1.4.6 -v2.x (v2.1.0) | v1.5.0 | v2.0.0 | The method signatures have changed: `CommonProfile` replaces `UserProfile` and `HttpAction` replaces `RequiresHttpAction` | it doesn't exist -v3.x (v3.3.0) | v1.5.0 | v3.0.0 | Created a `RatpackSessionStore` and manually retrieved the `client_name`. | it doesn't exist -v5.x | v1.9.0 | v4.x | | it doesn't exist +| Pac4j | Ratpack | New org.pac4j:ratpack-pac4j | Changes | Official io.ratpack:ratpack-pac4j | +|---------------|---------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------|-----------------------------------| +| v1.8.x | v1.4.6 | v1.4.6 | No changes: both modules are identical | v1.4.6 | +| v2.x (v2.1.0) | v1.5.0 | v2.0.0 | The method signatures have changed: `CommonProfile` replaces `UserProfile` and `HttpAction` replaces `RequiresHttpAction` | it doesn't exist | +| v3.x (v3.3.0) | v1.5.0 | v3.0.0 | Created a `RatpackSessionStore` and manually retrieved the `client_name`. | it doesn't exist | +| v5.x | v1.9.0 | v4.x | | it doesn't exist | +| v6.x | v1.9.0 | v5.x | | it doesn't exist | See the [official documentation](https://ratpack.io/manual/1.9.0/pac4j.html#pac4j) and the [demo](https://github.com/pac4j/ratpack-pac4j-demo). diff --git a/pom.xml b/pom.xml index f2cee25..85e9fb2 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ ratpack-pac4j jar pac4j implementation for Ratpack - 4.0.1-SNAPSHOT + 5.0.0-SNAPSHOT Security library for Ratpck based on pac4j https://github.com/pac4j/ratpack-pac4j @@ -66,8 +66,8 @@ - 5.1.4 - 11 + 6.0.0 + 17 1.9.0 diff --git a/src/main/java/ratpack/pac4j/RatpackPac4j.java b/src/main/java/ratpack/pac4j/RatpackPac4j.java index 9acc8f8..d418488 100644 --- a/src/main/java/ratpack/pac4j/RatpackPac4j.java +++ b/src/main/java/ratpack/pac4j/RatpackPac4j.java @@ -16,7 +16,11 @@ package ratpack.pac4j; +import static java.util.Arrays.asList; + import com.google.common.collect.ImmutableList; +import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import org.pac4j.core.authorization.authorizer.Authorizer; import org.pac4j.core.client.Client; @@ -25,9 +29,7 @@ import org.pac4j.core.context.WebContext; import org.pac4j.core.exception.TechnicalException; import org.pac4j.core.exception.http.HttpAction; -import org.pac4j.core.exception.http.OkAction; import org.pac4j.core.exception.http.RedirectionAction; -import org.pac4j.core.exception.http.WithLocationAction; import org.pac4j.core.profile.UserProfile; import ratpack.exec.Blocking; import ratpack.exec.Downstream; @@ -45,24 +47,21 @@ import ratpack.path.PathBinding; import ratpack.util.Types; -import java.util.List; -import java.util.Optional; - -import static java.util.Arrays.asList; - /** * Provides integration with the Pac4j library for authentication and authorization. *

- * Pac4j support many different authentication providers, such as external sources like GitHub, Twitter, Facebook etc., as well - * as proprietary local authentication sources. + * Pac4j support many different authentication providers, such as external sources like GitHub, Twitter, Facebook etc., + * as well as proprietary local authentication sources. *

- * The {@link #authenticator(Client[])} method provides a handler that implements the authentication process, - * and is required in all apps wanting to use authentication. + * The {@link #authenticator(Client[])} method provides a handler that implements the authentication process, and is + * required in all apps wanting to use authentication. *

- * The {@link #requireAuth(Class, Authorizer...)} method provides a handler that acts like a filter, ensuring that the user is authenticated for all requests. - * This can be used for requiring authentication for all requests starting with a particular request path for example. + * The {@link #requireAuth(String, Authorizer...)} method provides a handler that acts like a filter, ensuring that the + * user is authenticated for all requests. This can be used for requiring authentication for all requests starting with + * a particular request path for example. *

- * The {@link #userProfile(Context)}, {@link #login(Context, Class)} and {@link #logout(Context)} methods provide programmatic authentication mechanisms. + * The {@link #userProfile(Context)}, {@link #login(Context, String)} and {@link #logout(Context)} methods provide + * programmatic authentication mechanisms. */ public class RatpackPac4j { @@ -85,10 +84,11 @@ public static Handler authenticator(Client... clients) { } /** - * Creates a handler that implements authentication when the request path matches, and makes a Pac4j {@link Clients} available to downstream handlers otherwise. + * Creates a handler that implements authentication when the request path matches, and makes a Pac4j {@link Clients} + * available to downstream handlers otherwise. *

- * This methods performs the same function as {@link #authenticator(String, ClientsProvider)}, - * but is more convenient to use when the {@link Client} instances do not depend on the request environment. + * This methods performs the same function as {@link #authenticator(String, ClientsProvider)}, but is more convenient + * to use when the {@link Client} instances do not depend on the request environment. * * @param path the path to bind the authenticator to (relative to the current request path binding) * @param clients the supported authentication clients @@ -106,24 +106,28 @@ public static Handler authenticator(String path, Client... clients) { * @since 1.1 */ public interface ClientsProvider { + Iterable get(Context ctx); } /** - * Creates a handler that implements authentication when the request path matches, and makes a Pac4j {@link Clients} available to downstream handlers otherwise. + * Creates a handler that implements authentication when the request path matches, and makes a Pac4j {@link Clients} + * available to downstream handlers otherwise. *

- * This handler MUST be BEFORE any code in the handler pipeline that tries to identify the user, such as a {@link #requireAuth} handler in the pipeline. - * It should be added to the handler chain via the {@link Chain#all(Handler)}. - * That is, it should not be added with {@link Chain#get(Handler)} or any method that filters based on request method. - * It is common for this handler to be one of the first handlers in the pipeline. + * This handler MUST be BEFORE any code in the handler pipeline that tries to identify the user, such as + * a {@link #requireAuth} handler in the pipeline. It should be added to the handler chain via the + * {@link Chain#all(Handler)}. That is, it should not be added with {@link Chain#get(Handler)} or any method that + * filters based on request method. It is common for this handler to be one of the first handlers in the pipeline. *

- * This handler performs two different functions, based on whether the given path matches the {@link PathBinding#getPastBinding()} component of the current path binding. - * If the path matches, the handler will attempt authentication, which may involve redirecting to an external auth provider, which may then redirect back to this handler. - * If authentication is successful, the {@link UserProfile} of the authenticated user will be placed into the session. - * The user will then be redirected back to the URL that initiated the authentication. + * This handler performs two different functions, based on whether the given path matches the + * {@link PathBinding#getPastBinding()} component of the current path binding. If the path matches, the handler will + * attempt authentication, which may involve redirecting to an external auth provider, which may then redirect back to + * this handler. If authentication is successful, the {@link UserProfile} of the authenticated user will be placed + * into the session. The user will then be redirected back to the URL that initiated the authentication. *

- * If the path does not match, the handler will push an instance of {@link Clients} into the context registry and pass control downstream. - * The {@link Clients} instance will be retrieved downstream by any {@link #requireAuth(Class, Authorizer...)} handler (or use of {@link #login(Context, Class)}. + * If the path does not match, the handler will push an instance of {@link Clients} into the context registry and pass + * control downstream. The {@link Clients} instance will be retrieved downstream by any + * {@link #requireAuth(String, Authorizer...)} handler (or use of {@link #login(Context, String)}). * * @param path the path to bind the authenticator to (relative to the current request path binding) * @param clientsProvider the provider of authentication clients @@ -136,17 +140,19 @@ public static Handler authenticator(String path, ClientsProvider clientsProvider /** * An authentication and authorization “filter”. *

- * This handler can be used to ensure that a user profile is available for all downstream handlers. - * If there is no user profile present in the session (i.e. user not logged in), authentication will be initiated based on the given client type (i.e. redirect to the {@link #authenticator(Client[])} handler). - * If there is a {@link UserProfile} present in the session, this handler will push the user profile into the context registry before delegating downstream. - * If there is a {@link UserProfile} present in the context registry, this handler will simply delegate downstream. + * This handler can be used to ensure that a user profile is available for all downstream handlers. If there is no + * user profile present in the session (i.e. user not logged in), authentication will be initiated based on the given + * client type (i.e. redirect to the {@link #authenticator(Client[])} handler). If there is a {@link UserProfile} + * present in the session, this handler will push the user profile into the context registry before delegating + * downstream. If there is a {@link UserProfile} present in the context registry, this handler will simply delegate + * downstream. *

- * If there is a {@link UserProfile}, each of the given authorizers will be tested in turn and all must return true. - * If so, control will flow to the next handler. - * Otherwise, a {@code 403} {@link Context#clientError(int) client error} will be issued. + * If there is a {@link UserProfile}, each of the given authorizers will be tested in turn and all must return + * true. If so, control will flow to the next handler. Otherwise, a {@code 403} + * {@link Context#clientError(int) client error} will be issued. *

- * This handler requires a {@link Clients} instance available in the context registry. - * As such, this handler should be downstream of the {@link #authenticator(Client[])} handler. + * This handler requires a {@link Clients} instance available in the context registry. As such, this handler should be + * downstream of the {@link #authenticator(Client[])} handler. * *

{@code
    * import org.pac4j.core.profile.UserProfile;
@@ -195,14 +201,15 @@ public static Handler authenticator(String path, ClientsProvider clientsProvider
    * }
    * }
* - * @param clientType the client type to use to authenticate with if required + * @param clientName the name of the client to use to authenticate with if required * @param onFailure the action to take on authorization failure * @param authorizers the authorizers to check authorizations * @return a handler */ - public static Handler requireAuth(Class clientType, Consumer onFailure, Authorizer... authorizers) { + public static Handler requireAuth(String clientName, Consumer onFailure, + Authorizer... authorizers) { List authorizerList = asList(authorizers); - return ctx -> RatpackPac4j.login(ctx, clientType).then(userProfile -> { + return ctx -> RatpackPac4j.login(ctx, clientName).then(userProfile -> { if (authorizerList.isEmpty()) { ctx.getRequest().add(userProfile); ctx.next(); @@ -234,17 +241,19 @@ public static Handler requireAuth(Class clientType, Consumer - * This handler can be used to ensure that a user profile is available for all downstream handlers. - * If there is no user profile present in the session (i.e. user not logged in), authentication will be initiated based on the given client type (i.e. redirect to the {@link #authenticator(Client[])} handler). - * If there is a {@link UserProfile} present in the session, this handler will push the user profile into the context registry before delegating downstream. - * If there is a {@link UserProfile} present in the context registry, this handler will simply delegate downstream. + * This handler can be used to ensure that a user profile is available for all downstream handlers. If there is no + * user profile present in the session (i.e. user not logged in), authentication will be initiated based on the given + * client type (i.e. redirect to the {@link #authenticator(Client[])} handler). If there is a {@link UserProfile} + * present in the session, this handler will push the user profile into the context registry before delegating + * downstream. If there is a {@link UserProfile} present in the context registry, this handler will simply delegate + * downstream. *

- * If there is a {@link UserProfile}, each of the given authorizers will be tested in turn and all must return true. - * If so, control will flow to the next handler. - * Otherwise, a {@code 403} {@link Context#clientError(int) client error} will be issued. + * If there is a {@link UserProfile}, each of the given authorizers will be tested in turn and all must return + * true. If so, control will flow to the next handler. Otherwise, a {@code 403} + * {@link Context#clientError(int) client error} will be issued. *

- * This handler requires a {@link Clients} instance available in the context registry. - * As such, this handler should be downstream of the {@link #authenticator(Client[])} handler. + * This handler requires a {@link Clients} instance available in the context registry. As such, this handler should be + * downstream of the {@link #authenticator(Client[])} handler. * *

{@code
    * import org.pac4j.core.profile.UserProfile;
@@ -293,21 +302,22 @@ public static Handler requireAuth(Class clientType, Consumer
    *
-   * @param clientType the client type to use to authenticate with if required
+   * @param clientName the name of the client to use to authenticate with if required
    * @param authorizers the authorizers to check authorizations
    * @return a handler
    */
-  public static Handler requireAuth(Class clientType, Authorizer... authorizers) {
-    return requireAuth(clientType, ctx -> ctx.clientError(403), authorizers);
+  public static Handler requireAuth(String clientName, Authorizer... authorizers) {
+    return requireAuth(clientName, ctx -> ctx.clientError(403), authorizers);
   }
 
   /**
    * Logs the user in by redirecting to the authenticator, or provides the user profile if already logged in.
    * 

- * This method can be used to programmatically initiate a log in, if required. - * If the user is already logged in, the user profile will be provided via the returned promise. - * If the user is not already logged in, the promise will not be fulfilled and the user will be redirected to the authenticator. - * As such, like {@link #requireAuth(Class, Authorizer...)}, this can only be used downstream of the {@link #authenticator(Client[])} handler. + * This method can be used to programmatically initiate a log in, if required. If the user is already logged in, the + * user profile will be provided via the returned promise. If the user is not already logged in, the promise will not + * be fulfilled and the user will be redirected to the authenticator. As such, like + * {@link #requireAuth(String, Authorizer...)}, this can only be used downstream of the + * {@link #authenticator(Client[])} handler. * *

{@code
    * import org.pac4j.http.client.indirect.IndirectBasicAuthClient;
@@ -350,24 +360,25 @@ public static Handler requireAuth(Class clientType, Authorizer
    * }
* * @param ctx the handling context - * @param clientType the client type to authenticate with + * @param clientName the name of the client to authenticate with * @return a promise for the user profile, fulfilled if logged in */ - public static Promise login(Context ctx, Class clientType) { - if (isDirect(clientType)) { + public static Promise login(Context ctx, String clientName) { + var client = getClient(ctx, clientName); + if (isDirect(client)) { return userProfile(ctx) .flatMap(p -> { if (p.isPresent()) { return Promise.value(p); } else { - return performDirectAuthentication(ctx, clientType); + return performDirectAuthentication(ctx, clientName); } }) .route(p -> !p.isPresent(), p -> ctx.clientError(401)) .map(Optional::get); } else { return userProfile(ctx) - .route(p -> !p.isPresent(), p -> initiateAuthentication(ctx, clientType)) + .route(p -> !p.isPresent(), p -> initiateAuthentication(ctx, clientName)) .map(Optional::get); } } @@ -377,12 +388,12 @@ public static Promise login(Context ctx, Class cl *

* The promised optional will be empty if the user is not authenticated. *

- * This method should be used if the user may have been authenticated. - * That is, when the need for the profile is not downstream of an {@link #requireAuth(Class, Authorizer...)} handler, - * as the auth handler puts the profile into the context registry for easy retrieval. + * This method should be used if the user may have been authenticated. That is, when the need for the profile + * is not downstream of an {@link #requireAuth(String, Authorizer...)} handler, as the auth handler puts the profile + * into the context registry for easy retrieval. *

- * This method returns a promise as it will attempt to load the profile from the session if it - * isn't already in the context registry. + * This method returns a promise as it will attempt to load the profile from the session if it isn't already in the + * context registry. * *

{@code
    * import io.netty.handler.codec.http.HttpHeaderNames;
@@ -454,16 +465,16 @@ public static Promise> userProfile(Context ctx) {
   /**
    * Obtains the logged in user's profile, of the given type, if the user is logged in.
    * 

- * The promised optional will be empty if the user is not authenticated. - * If there exists a {@link UserProfile} for the current user but it is not compatible with the requested type, - * the returned promise will be a failure with a {@link ClassCastException}. + * The promised optional will be empty if the user is not authenticated. If there exists a {@link UserProfile} for the + * current user but it is not compatible with the requested type, the returned promise will be a failure with a + * {@link ClassCastException}. *

- * This method should be used if the user may have been authenticated. - * That is, when the the need for the profile is not downstream of an {@link #requireAuth(Class, Authorizer...)} handler, - * as the auth handler puts the profile into the context registry for easy retrieval. + * This method should be used if the user may have been authenticated. That is, when the the need for the + * profile is not downstream of an {@link #requireAuth(String, Authorizer...)} handler, as the auth handler puts the + * profile into the context registry for easy retrieval. *

- * This method returns a promise as it will attempt to load the profile from the session if it - * isn't already in the context registry. + * This method returns a promise as it will attempt to load the profile from the session if it isn't already in the + * context registry. * * @param ctx the handling context * @param type the type of the user profile @@ -486,7 +497,8 @@ public static Promise> userProfile(Context c /** * Logs out the current user, removing their profile from the session. *

- * The returned operation simply removes the profile from the session, regardless of whether it's actually there or not. + * The returned operation simply removes the profile from the session, regardless of whether it's actually there or + * not. * *

{@code
    * import org.pac4j.http.client.indirect.IndirectBasicAuthClient;
@@ -544,9 +556,8 @@ public static Operation logout(Context ctx) {
   /**
    * Adapts a Ratpack {@link Context} to a Pac4j {@link WebContext}.
    * 

- * The returned WebContext does not have access to the request body. - * {@link WebContext#getRequestParameters()} and associated methods will not include any - * form parameters if the request was a form. + * The returned WebContext does not have access to the request body. {@link WebContext#getRequestParameters()} and + * associated methods will not include any form parameters if the request was a form. * * @param ctx a Ratpack context * @return a Pac4j web context @@ -556,29 +567,33 @@ public static Promise webContext(Context ctx) { return Types.cast(RatpackWebContext.from(ctx, false)); } - private static void toProfile(Class type, Downstream> downstream, - Optional userProfileOptional, Block onEmpty) throws Exception { + private static void toProfile( + Class type, + Downstream> downstream, + Optional userProfileOptional, + Block onEmpty + ) throws Exception { if (userProfileOptional.isPresent()) { final UserProfile userProfile = userProfileOptional.get(); if (type.isInstance(userProfile)) { downstream.success(Optional.of(type.cast(userProfile))); } else { - downstream.error(new ClassCastException("UserProfile is of type " + userProfile.getClass() + ", and is not compatible with " + type)); + downstream.error(new ClassCastException( + "UserProfile is of type " + userProfile.getClass() + ", and is not compatible with " + type)); } } else { onEmpty.execute(); } } - private static void initiateAuthentication(Context ctx, Class clientType) { + private static void initiateAuthentication(Context ctx, String clientName) { Request request = ctx.getRequest(); - Clients clients = ctx.get(Clients.class); - Client client = clients.findClient(clientType).get(); + Client client = getClient(ctx, clientName); RatpackWebContext.from(ctx, false).then(webContext -> { webContext.getSessionStore().set(webContext, Pac4jSessionKeys.REQUESTED_URL.getName(), request.getUri()); try { - Optional action = client.getRedirectionAction(webContext, webContext.getSessionStore()); + Optional action = client.getRedirectionAction(webContext.callContext()); if (action.isPresent()) { webContext.sendResponse(action.get()); @@ -596,23 +611,31 @@ private static void initiateAuthentication(Context ctx, Class } private static Promise> performDirectAuthentication(Context ctx, - Class clientType) { + String clientName) { return RatpackWebContext.from(ctx, false).flatMap(webContext -> Blocking.get(() -> { Clients clients = ctx.get(Clients.class); - return clients.findClient(clientType).flatMap(client -> userProfileFromCredentials(client, webContext)); + return clients.findClient(clientName).flatMap(client -> userProfileFromCredentials(client, webContext)); }) ); } private static Optional userProfileFromCredentials(Client client, RatpackWebContext webContext) throws HttpAction { - return client.getCredentials(webContext, webContext.getSessionStore()).flatMap( - credentials -> client.getUserProfile(credentials, webContext, webContext.getSessionStore()) + return client.getCredentials(webContext.callContext()) + .flatMap(c -> client.validateCredentials(webContext.callContext(), c)) + .flatMap( + credentials -> client.getUserProfile(webContext.callContext(), credentials) ); } - private static boolean isDirect(Class clientType) { - return DirectClient.class.isAssignableFrom(clientType); + private static boolean isDirect(Client client) { + return client instanceof DirectClient; + } + + private static Client getClient(Context ctx, String clientName) { + Clients clients = ctx.get(Clients.class); + return clients.findClient(clientName) + .orElseThrow(() -> new IllegalStateException("No client with name \"%s\" in context".formatted(clientName))); } } diff --git a/src/main/java/ratpack/pac4j/internal/Pac4jAuthenticator.java b/src/main/java/ratpack/pac4j/internal/Pac4jAuthenticator.java index 039dabc..907a876 100644 --- a/src/main/java/ratpack/pac4j/internal/Pac4jAuthenticator.java +++ b/src/main/java/ratpack/pac4j/internal/Pac4jAuthenticator.java @@ -60,7 +60,7 @@ public void handle(Context ctx) throws Exception { .orElseThrow(() -> new TechnicalException("No client found for name: " + clientName)); } ).flatMap(client -> - getProfile(webContext, client) + getProfile(webContext, client) ).map(profile -> { profile.ifPresent(userProfile -> webContext.getProfileManager().save(true, userProfile, false)); Optional originalUrl = sessionData.get(Pac4jSessionKeys.REQUESTED_URL); @@ -106,8 +106,9 @@ private Promise createClients(Context ctx, PathBinding pathBinding) thr private Promise> getProfile(RatpackWebContext webContext, Client client) throws HttpAction { return Blocking.get( - () -> client.getCredentials(webContext, webContext.getSessionStore()) - .flatMap(credentials -> client.getUserProfile(credentials, webContext, webContext.getSessionStore())) + () -> client.getCredentials(webContext.callContext()) + .flatMap(c -> client.validateCredentials(webContext.callContext(), c)) + .flatMap(credentials -> client.getUserProfile(webContext.callContext(), credentials)) ); } diff --git a/src/main/java/ratpack/pac4j/internal/RatpackWebContext.java b/src/main/java/ratpack/pac4j/internal/RatpackWebContext.java index 2895bad..82c2e46 100644 --- a/src/main/java/ratpack/pac4j/internal/RatpackWebContext.java +++ b/src/main/java/ratpack/pac4j/internal/RatpackWebContext.java @@ -20,6 +20,15 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import io.netty.handler.codec.http.cookie.DefaultCookie; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.pac4j.core.context.CallContext; import org.pac4j.core.context.Cookie; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; @@ -43,9 +52,6 @@ import ratpack.session.SessionData; import ratpack.util.MultiValueMap; -import java.net.URI; -import java.util.*; - public class RatpackWebContext implements WebContext { private final Context context; @@ -90,6 +96,10 @@ public static Promise from(Context ctx, boolean bodyBacked) { } } + public CallContext callContext() { + return new CallContext(this, getSessionStore()); + } + public SessionStore getSessionStore() { return this.session; }