Skip to content

Security configuration

LELEU Jérôme edited this page Mar 11, 2019 · 5 revisions

1) General

You need to define the authentication mechanisms (Client) and authorization checks (Authorizer) you want.

The configuration (org.pac4j.core.config.Config) contains all the clients and authorizers required by the application to handle security.

The Config is bound for injection in a SecurityModule (or whatever the name you call it):

In Java:

public class SecurityModule extends AbstractModule {

    ...

    @Override
    protected void configure() {
        bind(HandlerCache.class).to(Pac4jHandlerCache.class);

        bind(Pac4jRoleHandler.class).to(MyPac4jRoleHandler.class);
        final PlayCacheSessionStore playCacheSessionStore = new PlayCacheSessionStore(getProvider(SyncCacheApi.class));
        //bind(PlaySessionStore.class).toInstance(playCacheSessionStore);
        bind(PlaySessionStore.class).to(PlayCacheSessionStore.class);

        // callback
        final CallbackController callbackController = new CallbackController();
        callbackController.setDefaultUrl("/");
        callbackController.setMultiProfile(true);
        bind(CallbackController.class).toInstance(callbackController);

        // logout
        final LogoutController logoutController = new LogoutController();
        logoutController.setDefaultUrl("/?defaulturlafterlogout");
        //logoutController.setDestroySession(true);
        bind(LogoutController.class).toInstance(logoutController);
    }

    @Provides
    protected FacebookClient provideFacebookClient() {
        final String fbId = configuration.getString("fbId");
        final String fbSecret = configuration.getString("fbSecret");
        return new FacebookClient(fbId, fbSecret);
    }

    @Provides
    protected TwitterClient provideTwitterClient() {
        return new TwitterClient("HVSQGAw2XmiwcKOTvZFbQ", "FSiO9G9VRR4KCuksky0kgGuo8gAVndYymr4Nl7qc8AA");
    }

    @Provides
    protected FormClient provideFormClient() {
        return new FormClient(baseUrl + "/loginForm", new SimpleTestUsernamePasswordAuthenticator());
    }

    ...

    @Provides
    protected SAML2Client provideSaml2Client() {
        final SAML2ClientConfiguration cfg = new SAML2ClientConfiguration("resource:samlKeystore.jks",
                "pac4j-demo-passwd", "pac4j-demo-passwd", "resource:openidp-feide.xml");
        cfg.setMaximumAuthenticationLifetime(3600);
        cfg.setServiceProviderEntityId("urn:mace:saml:pac4j.org");
        cfg.setServiceProviderMetadataPath(new File("target", "sp-metadata.xml").getAbsolutePath());
        return new SAML2Client(cfg);
    }

    @Provides
    protected Config provideConfig(FacebookClient facebookClient, TwitterClient twitterClient, FormClient formClient,
                                   IndirectBasicAuthClient indirectBasicAuthClient, CasClient casClient, SAML2Client saml2Client,
                                   OidcClient oidcClient, ParameterClient parameterClient, DirectBasicAuthClient directBasicAuthClient,
                                   CasProxyReceptor casProxyReceptor, DirectFormClient directFormClient) {
        final Clients clients = new Clients(baseUrl + "/callback", facebookClient, twitterClient, formClient,
                indirectBasicAuthClient, casClient, saml2Client, oidcClient, parameterClient, directBasicAuthClient,
                new AnonymousClient(), casProxyReceptor, directFormClient);

        final Config config = new Config(clients);
        config.addAuthorizer("admin", new RequireAnyRoleAuthorizer<>("ROLE_ADMIN"));
        config.addAuthorizer("custom", new CustomAuthorizer());
        config.addMatcher("excludedPath", new PathMatcher().excludeRegex("^/facebook/notprotected\\.html$"));
        config.setHttpActionAdapter(new DemoHttpActionAdapter());
        return config;
    }
}

See a full example here.

In Scala:

class SecurityModule(environment: Environment, configuration: Configuration) extends AbstractModule {

  val baseUrl = configuration.getString("baseUrl").get

  override def configure(): Unit = {

    bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])

    // callback
    val callbackController = new CallbackController()
    callbackController.setDefaultUrl("/?defaulturlafterlogout")
    callbackController.setMultiProfile(true)
    bind(classOf[CallbackController]).toInstance(callbackController)

    // logout
    val logoutController = new LogoutController()
    logoutController.setDefaultUrl("/")
    bind(classOf[LogoutController]).toInstance(logoutController)

    // security components used in controllers
    bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])
  }

  ...

  @Provides
  def provideCasProxyReceptor: CasProxyReceptor = new CasProxyReceptor()

  @Provides
  def provideCasClient(casProxyReceptor: CasProxyReceptor) = {
    val casConfiguration = new CasConfiguration("http://localhost:8888/cas/login") // ("https://casserverpac4j.herokuapp.com/login")
    casConfiguration.setProtocol(CasProtocol.CAS20)
    casConfiguration.setProxyReceptor(casProxyReceptor)
    new CasClient(casConfiguration)
  }

  @Provides
  def provideOidcClient: OidcClient[OidcProfile] = {
    val oidcConfiguration = new OidcConfiguration()
    oidcConfiguration.setClientId("343992089165-i1es0qvej18asl33mvlbeq750i3ko32k.apps.googleusercontent.com")
    oidcConfiguration.setSecret("unXK_RSCbCXLTic2JACTiAo9")
    oidcConfiguration.setDiscoveryURI("https://accounts.google.com/.well-known/openid-configuration")
    oidcConfiguration.addCustomParam("prompt", "consent")
    val oidcClient = new OidcClient[OidcProfile](oidcConfiguration)
    oidcClient.addAuthorizationGenerator(new RoleAdminAuthGenerator)
    oidcClient
  }

  @Provides
  def provideParameterClient: ParameterClient = {
    val jwtAuthenticator = new JwtAuthenticator()
    jwtAuthenticator.addSignatureConfiguration(new SecretSignatureConfiguration("12345678901234567890123456789012"))
    val parameterClient = new ParameterClient("token", jwtAuthenticator)
    parameterClient.setSupportGetRequest(true)
    parameterClient.setSupportPostRequest(false)
    parameterClient
  }

  @Provides
  def provideDirectBasicAuthClient: DirectBasicAuthClient = new DirectBasicAuthClient(new SimpleTestUsernamePasswordAuthenticator)

  @Provides
  def provideConfig(facebookClient: FacebookClient, twitterClient: TwitterClient, formClient: FormClient, indirectBasicAuthClient: IndirectBasicAuthClient,
                    casClient: CasClient, saml2Client: SAML2Client, oidcClient: OidcClient[OidcProfile], parameterClient: ParameterClient, directBasicAuthClient: DirectBasicAuthClient,
                    casProxyReceptor: CasProxyReceptor): Config = {
    val clients = new Clients(baseUrl + "/callback", facebookClient, twitterClient, formClient,
      indirectBasicAuthClient, casClient, saml2Client, oidcClient, parameterClient, directBasicAuthClient,
      new AnonymousClient(), casProxyReceptor)

    val config = new Config(clients)
    config.addAuthorizer("admin", new RequireAnyRoleAuthorizer[Nothing]("ROLE_ADMIN"))
    config.addAuthorizer("custom", new CustomAuthorizer)
    config.addMatcher("excludedPath", new PathMatcher().excludeRegex("^/filter/facebook/notprotected\\.html$"))
    config.setHttpActionAdapter(new DemoHttpActionAdapter())
    config
  }
}

See a full example here.

http://localhost:8080/callback is the url of the callback endpoint, which is only necessary for indirect clients.

Notice that you can also configure a specific HttpActionAdapter to handle specific HTTP actions (like redirections, forbidden / unauthorized pages) via the setHttpActionAdapter method of the Config object. The default available implementation is the DefaultHttpActionAdapter, but you can subclass it to define your own HTTP 401 / 403 error pages for example.

Notice that you can also define matchers via the addMatcher(name, Matcher) method.

You can also define a specific SecurityLogic via the setSecurityLogic method.

2) Choosing the right PlaySessionStore

You have two options to store the profiles of your authenticated users:

  1. the PlayCacheSessionStore saves your data in the Play cache

  2. the PlayCookieSessionStore saves your data in the Play session cookie (somehow preserving Play's statelessness) but with the drawback of removing some data (access token, refresh token...)

Bind the session store you want with the PlaySessionStore:

Java:

bind(PlaySessionStore.class).to(PlayCacheSessionStore.class);

or

bind(PlaySessionStore.class).to(PlayCookieSessionStore.class);

Scala:

bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])

or

bind(classOf[PlaySessionStore]).to(classOf[PlayCookieSessionStore])

By default, the PlayCookieSessionStore internally uses a ShiroAesDataEncrypter to encrypt your data, which requires the shiro-core dependency to be explicitly declared.

You can use your own DataEncrypter (or the ShiroAesDataEncrypter with your specific key) via:

Java:

DataEncrypter encrypter = new MyDataEncrypter();
PlayCookieSessionStore playCookieSessionStore = new PlayCookieSessionStore(encrypter);
bind(PlaySessionStore.class).toInstance(playCookieSessionStore);

Scala:

val encrypter = new MyDataEncrypter()
val playCookieSessionStore = new PlayCookieSessionStore(encrypter)
bind(classOf[PlaySessionStore]).toInstance(playCookieSessionStore)
Clone this wiki locally