/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.pac4j; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.inject.Binder; import com.google.inject.TypeLiteral; import com.google.inject.multibindings.MapBinder; import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.OptionalBinder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import org.jooby.*; import org.jooby.internal.pac4j.*; import org.jooby.scope.Providers; import org.jooby.scope.RequestScoped; import org.pac4j.core.authorization.authorizer.Authorizer; import org.pac4j.core.authorization.checker.AuthorizationChecker; import org.pac4j.core.authorization.checker.DefaultAuthorizationChecker; import org.pac4j.core.client.Client; import org.pac4j.core.client.Clients; import org.pac4j.core.client.finder.ClientFinder; import org.pac4j.core.client.finder.DefaultClientFinder; import org.pac4j.core.context.Pac4jConstants; import org.pac4j.core.context.WebContext; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.credentials.UsernamePasswordCredentials; import org.pac4j.core.credentials.authenticator.Authenticator; import org.pac4j.core.profile.CommonProfile; import org.pac4j.http.client.indirect.IndirectBasicAuthClient; import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator; import java.net.URI; import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import static java.util.Objects.*; /** * <h1>pac4j module</h1> * <p> * Authentication module via: <a href="https://github.com/pac4j/pac4j">pac4j</a>. * </p> * * <h2>exposes</h2> * <ul> * <li>{@link Clients}</li> * <li>{@link WebContext} as {@link RequestScoped}</li> * <li>{@link Route.Filter} per each registered {@link Client}</li> * <li>Callback {@link Route.Filter}</li> * </ul> * * <h2>usage</h2> * * <pre> * { * * get("/public", () {@literal ->} ..); * * use(new Auth()); * * get("/private", () {@literal ->} ..); * } * </pre> * <p> * Previous example adds a very basic but ready to use form login auth every time you try to access * to <code>/private</code> or any route defined below the auth module. * </p> * * <h2>clients</h2> * <p> * <a href="https://github.com/pac4j/pac4j">pac4j</a> is a powerful library that supports multiple * clients and/or authentication protocols. In the next example, we will see how to configure the * most basic of them, but also some complex protocols. * </p> * * <h3>basic auth</h3> * <p> * If basic auth is all you need, then: * </p> * * <pre> * { * use(new Auth().basic()); * } * </pre> * * <p> * A {@link IndirectBasicAuthClient} depends on {@link Authenticator}, default is * {@link SimpleTestUsernamePasswordAuthenticator} which is great for development, but nothing good * for other environments. Next example setup a basic auth with a custom {@link Authenticator}: * </p> * * <pre> * { * use(new Auth().basic("*", MyUsernamePasswordAuthenticator.class)); * } * </pre> * * <h3>form auth</h3> * <p> * Form authentication will be activated by calling {@link #form()}: * </p> * * <pre> * { * use(new Auth().form()); * } * </pre> * * <p> * Form is the default authentication method so previous example is the same as: * </p> * * <pre> * { * use(new Auth()); * } * </pre> * * <p> * Like basic auth, form auth depends on a {@link Authenticator}. * </p> * * <p> * A login form will be ready under the path: <code>/login</code>. Again, it is a very basic login * form useful for development. If you need a custom login page, just add a route before the * {@link Auth} module, like: * </p> * * <pre> * { * get("/login", () {@literal ->} Results.html("login")); * * use(new Auth()); * } * </pre> * * <p> * Simply and easy! * </p> * * <h3>oauth, openid, etc...</h3> * <p> * Twitter, example: * </p> * * <pre> * { * use(new Auth() * .client(conf {@literal ->} * new TwitterClient(conf.getString("twitter.key"), conf.getString("twitter.secret")))); * } * </pre> * * <p> * Keep in mind you will have to add the require Maven dependency to your project, beside that it is * pretty straight forward. * </p> * * <h2>protecting urls</h2> * <p> * By default a {@link Client} will protect all the urls defined below the module, because routes in * {@link Jooby} are executed in the order they where defined. * </p> * <p> * You can customize what urls are protected by specifying a path pattern: * </p> * * <pre> * { * use(new Auth().form("/private/**")); * * get("/hello", () {@literal ->} "no auth"); * * get("/private", () {@literal ->} "auth"); * } * </pre> * * <p> * Here the <code>/hello</code> path is un-protected, because the client will intercept everything * under <code>/private</code>. * </p> * * <h2>user profile</h2> * <p> * Jooby relies on {@link AuthStore} for saving and retrieving a {@link CommonProfile}. By default, * the {@link CommonProfile} is stored in the {@link Session} via {@link AuthSessionStore}. * </p> * <p> * After a successful authentication the {@link CommonProfile} is accessible as a request scoped * attribute: * </p> * * <pre> * { * use(new Auth().form()); * * get("/private", req {@literal ->} req.require(HttpProfile.class)); * } * </pre> * * <p> * facebook (or any oauth, openid, etc...) * <p> * <pre> * { * use(new Auth().client(new FacebookClient(key, secret)); * * get("/private", req {@literal ->} req.require(FacebookProfile.class)); * } * </pre> * * <p> * Custom {@link AuthStore} is provided via {@link Auth#store(Class)} method: * </p> * * <pre> * { * use(new Auth().store(MyDbStore.class)); * * get("/private", req {@literal ->} req.require(HttpProfile.class)); * } * </pre> * * <h2>logout</h2> * <p> * A default <code>/logout</code> handler is provided it too. The handler will remove the profile * from {@link AuthStore} by calling the {@link AuthStore#unset(String)} method. The default login * will redirect to <code>/</code>. * </p> * <p> * A custom logout and redirect urls can be set via <code>.conf</code> file or programmatically: * </p> * * <pre> * { * use(new Auth().logout("/mylogout", "/redirectTo")); * } * </pre> * * @author edgar * @since 0.6.0 */ public class Auth implements Jooby.Module { /** * Name of the local request variable that holds the username. */ public static final String ID = Auth.class.getName() + ".id"; public static final String CNAME = Auth.class.getName() + ".client.id"; private Multimap<String, BiFunction<Binder, Config, AuthFilter>> bindings = ArrayListMultimap .create(); @SuppressWarnings("rawtypes") private Class<? extends AuthStore> storeClass = AuthSessionStore.class; private Optional<String> logoutUrl = Optional.empty(); private Optional<String> redirecTo = Optional.empty(); private Multimap<String, Map.Entry<String, Object>> authorizers = ArrayListMultimap.create(); private Set<Object> bindedProfiles = new HashSet<>(); /** * Protect one or more urls with an {@link Authorizer}. For example: * * <pre> * { * use(new Auth() * .form("*") * .authorizer("admin", "/admin/**", new RequireAnyRoleAuthorizer("admin")) * ); * } * </pre> * * <p> * Previous example will protect any url with form authentication and require and admin role for * <code>/admin/</code> or subpath of it. * </p> * <p> * NOTE: make sure url is protected by one pac4j client. * </p> * * @param name Authorizer name. * @param pattern URL pattern to protected. * @param authorizer Authorizer to apply. * @return This module. */ public Auth authorizer(final String name, final String pattern, final Authorizer<?> authorizer) { authorizer(authorizer, name, pattern); return this; } /** * Protect one or more urls with an {@link Authorizer}. For example: * <pre> * { * use(new Auth() * .form("*") * .authorizer("admin", "/admin/**", MyAuthorizer.class) * ); * } * </pre> * <p> * Previous example will protect any url with form authentication and require and admin role for * <code>/admin/</code> or subpath of it. * </p> * <p> * NOTE: make sure url is protected by one pac4j client. * </p> * * @param name Authorizer name. * @param pattern URL pattern to protected. * @param authorizer Authorizer to apply. * @return This module. */ @SuppressWarnings("rawtypes") public Auth authorizer(final String name, final String pattern, final Class<? extends Authorizer> authorizer) { authorizer(authorizer, name, pattern); return this; } /** * Protect one or more urls with an {@link Authorizer}. For example: * <pre> * { * use(new Auth() * .form("*") * .authorizer("admin", "/admin/**", MyAuthorizer.class) * ); * } * </pre> * <p> * Previous example will protect any url with form authentication and require and admin role for * <code>/admin/</code> or subpath of it. * </p> * * @param name Authorizer name. * @param pattern URL pattern to protected. * @param authorizer Authorizer to apply. */ private void authorizer(final Object authorizer, final String name, final String pattern) { requireNonNull(name, "An authorizer's name is required."); requireNonNull(pattern, "An authorizer's pattern is required."); requireNonNull(authorizer, "An authorizer is required."); authorizers.put(pattern, Maps.immutableEntry(name, authorizer)); } /** * Add a form auth client. * * @param pattern URL pattern to protect. * @param authenticator Authenticator to use. * @return This module. */ public Auth form(final String pattern, final Class<? extends Authenticator<UsernamePasswordCredentials>> authenticator) { bindings.put(pattern, (binder, conf) -> { TypeLiteral<Authenticator<UsernamePasswordCredentials>> usernamePasswordAuthenticator = new TypeLiteral<Authenticator<UsernamePasswordCredentials>>() { }; binder.bind(usernamePasswordAuthenticator.getRawType()).to(authenticator); bindProfile(binder, CommonProfile.class); Multibinder.newSetBinder(binder, Client.class) .addBinding().toProvider(FormAuth.class); return new FormFilter(conf.getString("auth.form.loginUrl"), conf.getString("application.path") + authCallbackPath(conf)); }); return this; } /** * Add a form auth client. It setup a {@link SimpleTestUsernamePasswordAuthenticator}. * Useful for development. * * @param pattern URL pattern to protect. * @return This module. */ public Auth form(final String pattern) { return form(pattern, SimpleTestUsernamePasswordAuthenticator.class); } /** * Add a form auth client, protecting all the urls <code>*</code>. It setup a * {@link SimpleTestUsernamePasswordAuthenticator}. Useful for development. * * @return This module. */ public Auth form() { return form("*"); } /** * Add a basic auth client. * * @param pattern URL pattern to protect. * @param authenticator Authenticator to use. * @return This module. */ public Auth basic(final String pattern, final Class<? extends Authenticator<UsernamePasswordCredentials>> authenticator) { bindings.put(pattern, (binder, config) -> { TypeLiteral<Authenticator<UsernamePasswordCredentials>> usernamePasswordAuthenticator = new TypeLiteral<Authenticator<UsernamePasswordCredentials>>() { }; binder.bind(usernamePasswordAuthenticator.getRawType()).to(authenticator); bindProfile(binder, CommonProfile.class); Multibinder.newSetBinder(binder, Client.class) .addBinding().toProvider(BasicAuth.class); return new AuthFilter(IndirectBasicAuthClient.class, CommonProfile.class); }); return this; } /** * Add a basic auth client. It setup a {@link SimpleTestUsernamePasswordAuthenticator}. Useful * for development. * * @param pattern URL pattern to protect. * @return This module. */ public Auth basic(final String pattern) { return basic(pattern, SimpleTestUsernamePasswordAuthenticator.class); } /** * Add a basic auth client, protecting all the urls <code>*</code>. It setup a * {@link SimpleTestUsernamePasswordAuthenticator}. Useful for development. * * @return This module. */ public Auth basic() { return basic("*"); } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param client Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ public <C extends Credentials, U extends CommonProfile> Auth client(final Client<C, U> client) { return client("*", client); } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param client Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ public <C extends Credentials, U extends CommonProfile> Auth client( final Class<? extends Client<C, U>> client) { return client("*", client); } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param pattern URL pattern to protect. * @param client Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ public <C extends Credentials, U extends CommonProfile> Auth client(final String pattern, final Client<C, U> client) { return client(pattern, config -> client); } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param provider Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ public <C extends Credentials, U extends CommonProfile> Auth client( final Function<Config, Client<C, U>> provider) { return client("*", provider); } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param pattern URL pattern to protect. * @param provider Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ @SuppressWarnings({"unchecked", "rawtypes"}) public <C extends Credentials, U extends CommonProfile> Auth client(final String pattern, final Function<Config, Client<C, U>> provider) { bindings.put(pattern, (binder, config) -> { Client<C, U> client = provider.apply(config); Multibinder.newSetBinder(binder, Client.class) .addBinding().toInstance(client); Class clientType = client.getClass(); Class profileType = ClientType.typeOf(clientType); bindProfile(binder, profileType); return new AuthFilter(clientType, profileType); }); return this; } /** * Add an auth client, like facebook, twitter, github, etc...Please note the require dependency * must be in the classpath. * * @param pattern URL pattern to protect. * @param client Client to add. * @param <C> Credentials. * @param <U> CommonProfile. * @return This module. */ @SuppressWarnings({"rawtypes", "unchecked"}) public <C extends Credentials, U extends CommonProfile> Auth client(final String pattern, final Class<? extends Client<C, U>> client) { bindings.put(pattern, (binder, config) -> { Multibinder.newSetBinder(binder, Client.class) .addBinding().to(client); Class profileType = ClientType.typeOf(client); bindProfile(binder, profileType); return new AuthFilter(client, profileType); }); return this; } /** * Setup the {@link AuthStore} to use. Keep in mind the store is binded it as singleton. * * @param store Store to use. * @return This module. */ public <U extends CommonProfile> Auth store(final Class<? extends AuthStore<U>> store) { this.storeClass = requireNonNull(store, "Store is required."); return this; } /** * Set the logout and redirect URL patterns. * * @param logoutUrl Logout url, default is <code>/logout</code>. * @param redirecTo Redirect url, default is <code>/</code>. * @return This module. */ public Auth logout(final String logoutUrl, final String redirecTo) { this.logoutUrl = Optional.of(logoutUrl); this.redirecTo = Optional.of(redirecTo); return this; } /** * Set the logout and redirect URL patterns. * * @param logoutUrl Logout url, default is <code>/logout</code>. * @return This module. */ public Auth logout(final String logoutUrl) { this.logoutUrl = Optional.of(logoutUrl); this.redirecTo = Optional.empty(); return this; } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public void configure(final Env env, final Config conf, final Binder binder) { binder.bind(Clients.class).toProvider(ClientsProvider.class); binder.bind(org.pac4j.core.config.Config.class).toProvider(ConfigProvider.class); OptionalBinder.newOptionalBinder(binder, AuthorizationChecker.class) .setDefault().toInstance(new DefaultAuthorizationChecker()); OptionalBinder.newOptionalBinder(binder, ClientFinder.class) .setDefault().toInstance(new DefaultClientFinder()); Router routes = env.router(); MapBinder<String, Authorizer> authorizers = MapBinder .newMapBinder(binder, String.class, Authorizer.class); routes.use("*", authCallbackPath(conf), (req, rsp, chain) -> req .require(AuthCallback.class).handle(req, rsp, chain)) .excludes("/favicon.ico") .name("auth(Callback)"); routes.use("*", logoutUrl.orElse(conf.getString("auth.logout.url")), new AuthLogout(redirecTo.orElse(conf.getString("auth.logout.redirectTo")))) .name("auth(Logout)"); if (bindings.size() == 0) { // no auth client, go dev friendly and add a form auth form(); } // bindings.values().forEach(it -> it.accept(binder, config)); bindings.asMap().entrySet().forEach(e -> { String pattern = e.getKey(); List<AuthFilter> filters = new ArrayList<>(); e.getValue().forEach(it -> { AuthFilter filter = it.apply(binder, conf); if (filters.size() == 0) { // push 1st filter filters.add(filter); } else { // push recentely created filter to head and discard it. filters.get(0).setName(filter.getName()); } }); AuthFilter head = filters.get(0); routes.use("*", pattern, head).name("auth(" + head.getName() + ")").excludes("/favicon.ico"); }); binder.bind(AuthCallback.class); binder.bind(AuthStore.class).to(storeClass); binder.bind(WebContext.class).to(AuthContext.class).in(RequestScoped.class); this.authorizers.asMap().entrySet().forEach(e -> { String pattern = e.getKey(); String names = e.getValue().stream() .map(Map.Entry::getKey) .collect(Collectors.joining(Pac4jConstants.ELEMENT_SEPRATOR)); // bind route routes.use("*", pattern, new AuthorizerFilter(names)) .name("auth(" + names + ")"); // bind authorizers e.getValue().forEach(v -> { String name = v.getKey(); Object authorizer = v.getValue(); if (authorizer instanceof Authorizer) { authorizers.addBinding(name).toInstance((Authorizer) authorizer); } else { authorizers.addBinding(name).to((Class) authorizer); } }); }); } private String authCallbackPath(final Config conf) { String fullcallback = conf.getString("auth.callback"); String root = conf.getString("application.path"); String callback = URI.create(fullcallback).getPath().replace(root, ""); return Route.normalize(callback); } @Override public Config config() { return ConfigFactory.parseResources(getClass(), "auth.conf"); } @SuppressWarnings({"unchecked", "rawtypes"}) private void bindProfile(final Binder binder, final Class root) { Class profile = root; while (profile != Object.class) { if (bindedProfiles.add(profile)) { binder.bind(profile).toProvider(Providers.outOfScope(profile)).in(RequestScoped.class); } profile = profile.getSuperclass(); } } }