From 0bf8399204c59006b85f0097b5d2fc2084200c95 Mon Sep 17 00:00:00 2001 From: paulb Date: Tue, 20 Feb 2024 14:05:23 +0100 Subject: [PATCH 1/3] Enable basic auth from configured user/password --- .../ConfiguredBasicAuthAuthenticator.scala | 30 +++++++++++++++++++ app/modules/SecurityModule.scala | 30 +++++++++++++------ conf/application.conf | 8 +++++ 3 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 app/modules/ConfiguredBasicAuthAuthenticator.scala diff --git a/app/modules/ConfiguredBasicAuthAuthenticator.scala b/app/modules/ConfiguredBasicAuthAuthenticator.scala new file mode 100644 index 00000000..c13badf9 --- /dev/null +++ b/app/modules/ConfiguredBasicAuthAuthenticator.scala @@ -0,0 +1,30 @@ +package modules + +import org.pac4j.core.context.WebContext +import org.pac4j.core.context.session.SessionStore +import org.pac4j.core.credentials.{Credentials, UsernamePasswordCredentials} +import org.pac4j.core.credentials.authenticator.Authenticator +import org.pac4j.core.exception.CredentialsException +import org.pac4j.core.profile.CommonProfile +import org.pac4j.core.util.{CommonHelper, Pac4jConstants} + + +case class ConfiguredBasicAuthAuthenticator(validUserId: String, validPassword: String) extends Authenticator { + + override def validate(credentials: Credentials, context: WebContext, sessionStore: SessionStore): Unit = { + if (credentials == null) throw new CredentialsException("No credential") + val userCredentials = credentials.asInstanceOf[UsernamePasswordCredentials] + val username = userCredentials.getUsername() + val password = userCredentials.getPassword() + if (CommonHelper.isBlank(username)) throw new CredentialsException("Username cannot be blank") + if (CommonHelper.isBlank(password)) throw new CredentialsException("Password cannot be blank") + if (CommonHelper.areNotEquals(username, validUserId)) throw new CredentialsException("Username : '" + username + "' does not match valid user") + if (CommonHelper.areNotEquals(password, validPassword)) throw new CredentialsException("Password does not match valid password") + val profile = new CommonProfile + profile.setId(username) + profile.addAttribute(Pac4jConstants.USERNAME, username) + userCredentials.setUserProfile(profile) + } + +} + diff --git a/app/modules/SecurityModule.scala b/app/modules/SecurityModule.scala index 3585c3af..71fb2219 100644 --- a/app/modules/SecurityModule.scala +++ b/app/modules/SecurityModule.scala @@ -1,11 +1,12 @@ package modules import com.google.inject.{AbstractModule, Provides} -import org.pac4j.core.client.Clients +import org.pac4j.core.client.{Client, Clients} import org.pac4j.core.client.direct.AnonymousClient import org.pac4j.core.config.Config import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile +import org.pac4j.http.client.direct.DirectBasicAuthClient import org.pac4j.play.scala.{DefaultSecurityComponents, Pac4jScalaTemplateHelper, SecurityComponents} import org.pac4j.play.store.{PlayCookieSessionStore, ShiroAesDataEncrypter} import org.pac4j.play.{CallbackController, LogoutController} @@ -43,16 +44,24 @@ class SecurityModule(environment: Environment, configuration: Configuration) ext @Provides def provideConfig(): Config = { val maybeConfiguredClientName = configuration.getOptional[String](ConfigKeyAuthClient).filter(_.nonEmpty) - val authClientOpt = maybeConfiguredClientName.map { - case "SAML2Client" => createSaml2Client(s"$ConfigKeyPrefixClientConfig.SAML2Client") + val config: Option[Config] = maybeConfiguredClientName.map { + case "DirectBasicAuthClient" => createConfiguredDirectBasicAuthConfig(s"$ConfigKeyPrefixClientConfig.ConfiguredDirectBasicAuthClient") + case "SAML2Client" => createSaml2Config(s"$ConfigKeyPrefixClientConfig.SAML2Client") case other => throw new RuntimeException(s"Unsupported auth client config value: $other") } - val allClients = authClientOpt.toSeq :+ new AnonymousClient() - // callback URL path as configured in `routes` - val clients = new Clients(s"$baseUrl/callback", allClients:_*) - new Config(clients) + config match { + case Some(config) => config + case None => throw new RuntimeException(s"Failed configuring auth client") + } + } + + private def createConfiguredDirectBasicAuthConfig(keyPrefix: String): Config = { + val username = configuration.get[String](s"$keyPrefix.username") + val password = configuration.get[String](s"$keyPrefix.password") + new Config(new DirectBasicAuthClient(ConfiguredBasicAuthAuthenticator(username, password))) } - private def createSaml2Client(keyPrefix: String): SAML2Client = { + + private def createSaml2Config(keyPrefix: String): Config = { val cfg = new SAML2Configuration( configuration.get[String](s"$keyPrefix.keystore"), configuration.get[String](s"$keyPrefix.keystorePassword"), @@ -62,7 +71,10 @@ class SecurityModule(environment: Environment, configuration: Configuration) ext cfg.setServiceProviderEntityId(configuration.get[String](s"$keyPrefix.serviceProviderEntityId")) cfg.setServiceProviderMetadataPath(configuration.get[String](s"$keyPrefix.serviceProviderMetadataPath")) cfg.setMaximumAuthenticationLifetime(configuration.get[Long](s"$keyPrefix.maximumAuthenticationLifetime")) - new SAML2Client(cfg) + val allClients = Option(new SAML2Client(cfg)).toSeq :+ new AnonymousClient() + // callback URL path as configured in `routes` + val clients = new Clients(s"$baseUrl/callback", allClients:_*) + new Config(clients) } } diff --git a/conf/application.conf b/conf/application.conf index 2d99c368..f1bf3e34 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -173,6 +173,7 @@ play.http.session.maxAge = ${?SMUI_SESSION_MAXAGE} # This defines the pac4j client to use for authentication. Uses the well-defined pac4j names (OidcClient, # DirectBasicAuthClient...), currently supported: +# - DirectBasicAuthClient # - SAML2Client # If no or empty value is set, no authentication is used. smui.auth.client = "" @@ -218,6 +219,13 @@ smui.auth.clients.SAML2Client { maximumAuthenticationLifetime = ${?SMUI_SAML_MAXIMUM_AUTHENTICATION_LIFETIME} } +# The DirectBasicAuthClient configuration which is used *only* when the DirectBasicAuthClient is configured as smui.auth.client +smui.auth.clients.ConfiguredDirectBasicAuthClient { + username = "smui_user" + username = ${?SMUI_BASIC_AUTH_PASS} + password = "smui_pass" + password = ${?SMUI_BASIC_AUTH_PASS} +} # pac4j rules for configuring the pac4j SecurityFilter pac4j.security.rules = [ From e29218d2b910427d18c488f92665072c1097f85e Mon Sep 17 00:00:00 2001 From: paulb Date: Tue, 20 Feb 2024 18:30:48 +0100 Subject: [PATCH 2/3] Fix test with RuntimeException --- app/modules/SecurityModule.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/modules/SecurityModule.scala b/app/modules/SecurityModule.scala index 71fb2219..8af1d785 100644 --- a/app/modules/SecurityModule.scala +++ b/app/modules/SecurityModule.scala @@ -49,10 +49,7 @@ class SecurityModule(environment: Environment, configuration: Configuration) ext case "SAML2Client" => createSaml2Config(s"$ConfigKeyPrefixClientConfig.SAML2Client") case other => throw new RuntimeException(s"Unsupported auth client config value: $other") } - config match { - case Some(config) => config - case None => throw new RuntimeException(s"Failed configuring auth client") - } + config.getOrElse(new Config()) } private def createConfiguredDirectBasicAuthConfig(keyPrefix: String): Config = { From 8e8f238363297fceb861c7c38f40ee68742d04f9 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Tue, 20 Feb 2024 12:37:44 -0500 Subject: [PATCH 3/3] prep for next release, get SmuiVersionSpec to pass --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9b53cecc..3ab2e03b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import com.typesafe.sbt.GitBranchPrompt name := "search-management-ui" -version := "4.0.6" +version := "4.0.7" maintainer := "Contact productful.io " scalaVersion := "2.12.17"