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 @@
- * 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 extends Client> 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 extends Client> clientType, Consumer
- * 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 extends Client> 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 extends Client> 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 extends Client> 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 Promiselogin(Context ctx, Class extends Client> 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 extends Client> 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 super Optional > downstream, - Optional userProfileOptional, Block onEmpty) throws Exception { + private static void toProfile( + Class type, + Downstream super Optional > 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 extends Client> 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 extends Client> } private static Promise > performDirectAuthentication(Context ctx, - Class extends Client> 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 extends Client> 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; }