diff --git a/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala b/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala index ed91fc98..d5775399 100644 --- a/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala +++ b/client/src/main/scala/ch/wsl/box/client/views/components/LoginForm.scala @@ -39,20 +39,21 @@ case class LoginForm(login: ModelProperty[LoginData] => Unit) { if(ClientConf.openid.nonEmpty) { Seq( hr(clear.both), - label("Login providers:"), br + label("Login with:"), br ) } else Seq[Modifier](), - ClientConf.openid.map{ openid => - button(BootstrapStyles.Float.right(),ClientConf.style.boxButton,borderRadius := 10.px, borderColor := ClientConf.styleConf.colors.mainColor, borderWidth := 3.px, borderStyle := "solid", - - img(src := openid.logo, maxWidth := 80.px), - onclick :+= ((e:Event) => { - e.preventDefault() - val redirectUri = URLEncoder.encode(s"${ClientConf.frontendUrl}/authenticate/${openid.provider_id}","UTF-8") - window.location.href = s"${openid.authorize_url}?client_id=${openid.client_id}&scope=${openid.scope}&response_type=code&state=${UUID.randomUUID()}&redirect_uri=$redirectUri" - }) - ) - }, + div(display.flex,justifyContent.spaceAround, + ClientConf.openid.map{ openid => + button(ClientConf.style.boxButton, + img(src := openid.logo, maxWidth := 80.px), + onclick :+= ((e:Event) => { + e.preventDefault() + val redirectUri = URLEncoder.encode(s"${ClientConf.frontendUrl}/authenticate/${openid.provider_id}","UTF-8") + window.location.href = s"${openid.authorize_url}?client_id=${openid.client_id}&scope=${openid.scope}&response_type=code&state=${UUID.randomUUID()}&redirect_uri=$redirectUri" + }) + ) + } + ), div(clear.both) ) diff --git a/project/Settings.scala b/project/Settings.scala index 83e93235..b8238988 100755 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -175,8 +175,8 @@ object Settings { "org.apache.xmlgraphics" % "batik-transcoder" % "1.16", "org.apache.xmlgraphics" % "batik-codec" % "1.16", "com.softwaremill.sttp.client4" %% "core" % "4.0.9", - "com.softwaremill.sttp.client4" %% "circe" % "4.0.9" - + "com.softwaremill.sttp.client4" %% "circe" % "4.0.9", + "org.bouncycastle" % "bcpkix-jdk18on" % "1.83" //"mil.nga.geopackage" % "geopackage" % "6.6.3" // "com.github.pureconfig" %% "pureconfig" % "0.17.3" diff --git a/server/src/main/scala/ch/wsl/box/rest/Boot.scala b/server/src/main/scala/ch/wsl/box/rest/Boot.scala index 1fc7ed45..dee115f9 100755 --- a/server/src/main/scala/ch/wsl/box/rest/Boot.scala +++ b/server/src/main/scala/ch/wsl/box/rest/Boot.scala @@ -17,13 +17,14 @@ import wvlet.airframe.Design import ch.wsl.box.model.Migrate import ch.wsl.box.rest.logic.cron.{BoxCronLoader, CronScheduler} import ch.wsl.box.rest.logic.notification.{MailHandler, NotificationsHandler} +import ch.wsl.box.rest.utils.CertificateUtils import scala.concurrent.{Await, ExecutionContext, Future, Promise} import scala.concurrent.duration._ import scala.io.StdIn -class Box(name:String,version:String)(implicit services: Services) { +class Box(name:String,version:String,https:Boolean)(implicit services: Services) { implicit val executionContext = services.executionContext implicit val system: ActorSystem = services.actorSystem @@ -66,10 +67,18 @@ class Box(name:String,version:String)(implicit services: Services) { Root(s"$name $version",akkaConf, origins).route } + def server = if(https) { + Http().newServerAt( host, port).enableHttps(CertificateUtils.sslContext).bind(routes) + } else { + Http().newServerAt( host, port).bind(routes) + } + + val httpsStr = if(https) "https" else "http" + for{ //pl <- preloading //_ <- pl.terminate(1.seconds) - binding <- Http().bindAndHandle(routes, host, port) //attach the root route + binding <- server //attach the root route res <- { println( s""" @@ -83,7 +92,7 @@ class Box(name:String,version:String)(implicit services: Services) { | |=================================== | - |Box server started at http://$host:$port + |Box server started at $httpsStr://$host:$port |""".stripMargin) binding.whenTerminationSignalIssued.map{ _ => @@ -102,9 +111,10 @@ class Box(name:String,version:String)(implicit services: Services) { object Boot extends App { - val (name,app_version) = args.length match { - case 2 => (args(0),args(1)) - case _ => ("Standalone","DEV") + val (name,app_version,https) = args.length match { + case 3 => (args(0),args(1),args(2).toBoolean) + case 2 => (args(0),args(1),false) + case _ => ("Standalone","DEV",true) } def run(name:String,app_version:String,module:Design) { @@ -124,7 +134,7 @@ object Boot extends App { Registry.loadBox() module.build[Services] { services => - val server = new Box(name, app_version)(services) + val server = new Box(name, app_version,https)(services) implicit val executionContext = services.executionContext val binding = { diff --git a/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala b/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala index 4349c41c..bc6ac334 100644 --- a/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala +++ b/server/src/main/scala/ch/wsl/box/rest/auth/oidc/AuthFlow.scala @@ -66,22 +66,29 @@ object AuthFlow { // } def code(provider:OIDCConf,c:String)(implicit ex:ExecutionContext, services: Services):Future[Either[ResponseException[String],CurrentUser]] = { - def authToken = basicRequest - .post(uri"${provider.token_url}") - .body(Map( - "grant_type" -> "authorization_code", - "client_id" -> provider.client_id, - "client_secret" -> provider.client_secret, - "code" -> c, - "redirect_uri" -> s"${services.config.frontendUrl}/authenticate/${provider.provider_id}" - )) - .response(asJson[OpenIDToken]) - .send(backend) + def authToken = { + val r = basicRequest + .post(uri"${provider.token_url}") + .body(Map( + "grant_type" -> "authorization_code", + "client_id" -> provider.client_id, + "client_secret" -> provider.client_secret, + "code" -> c, + "redirect_uri" -> s"${services.config.frontendUrl}/authenticate/${provider.provider_id}" + )) + .response(asJson[OpenIDToken]) + +// println(r.toCurl) + + r.send(backend).map{ x => + x + } + } def userInfo(token:OpenIDToken) = { import UserInfo._ basicRequest - .get(uri"https://gitlabext.wsl.ch/oauth/userinfo") + .get(uri"${provider.user_info_url}") .response(asJson[UserInfo]) .auth.bearer(token.access_token) .send(backend) diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala index 4a12f93f..08727eeb 100755 --- a/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala +++ b/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala @@ -57,7 +57,7 @@ object FormMetadataFactory extends Logging with MetadataFactory{ case Some(u) => Auth.rolesOf(u) case None => Future.successful(Seq()) } - } yield user.map(u => (BoxSession(CurrentUser(DbInfo(u,u,roles),UserInfo(u,u,None,roles,Json.Null))),form.exists(_.public_list))) + } yield user.map(u => (BoxSession(CurrentUser(DbInfo(u,u,roles),UserInfo(u,u,u,None,roles,Json.Null))),form.exists(_.public_list))) } diff --git a/server/src/main/scala/ch/wsl/box/rest/utils/Auth.scala b/server/src/main/scala/ch/wsl/box/rest/utils/Auth.scala index 9485826a..81eb6d0d 100644 --- a/server/src/main/scala/ch/wsl/box/rest/utils/Auth.scala +++ b/server/src/main/scala/ch/wsl/box/rest/utils/Auth.scala @@ -47,7 +47,7 @@ object Auth extends Logging { for{ validUser <- checkAuth(name,password) roles <- if(validUser) rolesOf(username) else Future.successful(Seq()) - } yield if(validUser) Some(CurrentUser(DbInfo(username,name,roles),UserInfo(name,name,None,roles,Json.Null))) else None + } yield if(validUser) Some(CurrentUser(DbInfo(username,name,roles),UserInfo(name,name,name,None,roles,Json.Null))) else None } diff --git a/server/src/main/scala/ch/wsl/box/rest/utils/BoxSession.scala b/server/src/main/scala/ch/wsl/box/rest/utils/BoxSession.scala index 027c1f50..64aa7247 100755 --- a/server/src/main/scala/ch/wsl/box/rest/utils/BoxSession.scala +++ b/server/src/main/scala/ch/wsl/box/rest/utils/BoxSession.scala @@ -7,21 +7,27 @@ import io.circe._ import io.circe.syntax._ import io.circe.generic.auto._ import io.circe.parser._ +import scribe.Logging import scala.concurrent.ExecutionContext -import scala.util.Try +import scala.util.{Failure, Success, Try} case class BoxSession(user:CurrentUser) { def userProfile(implicit services:Services): UserProfile = UserProfile(user.db.username,user.db.app_username) } -object BoxSession { +object BoxSession extends Logging { implicit def serializer: SessionSerializer[BoxSession, String] = new SingleValueSessionSerializer( s => s.asJson.noSpaces, - (un: String) => Try { - val session = parse(un).flatMap(_.as[BoxSession]).toOption.get - session + (un: String) => { + parse(un).flatMap(_.as[BoxSession]) match { + case Left(value) => { + logger.warn(s"Session not decoded: ${value.getMessage}") + Failure(value) + } + case Right(value) => Success(value) + } }) def fromLogin(request:LoginRequest)(implicit services: Services,executionContext: ExecutionContext) = Auth.getCurrentUser(request.username,request.password).map(_.map(cu => BoxSession(cu))) diff --git a/server/src/main/scala/ch/wsl/box/rest/utils/CertificateUtils.scala b/server/src/main/scala/ch/wsl/box/rest/utils/CertificateUtils.scala new file mode 100644 index 00000000..221d994b --- /dev/null +++ b/server/src/main/scala/ch/wsl/box/rest/utils/CertificateUtils.scala @@ -0,0 +1,59 @@ +package ch.wsl.box.rest.utils + +import akka.http.scaladsl.{ConnectionContext, HttpsConnectionContext} + +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.cert.X509Certificate +import java.util.{Date, Locale} +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder + +import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} + +object CertificateUtils { + def generateSelfSignedCertificate: KeyStore = { + val keyPairGen = KeyPairGenerator.getInstance("RSA") + keyPairGen.initialize(2048) + val keyPair = keyPairGen.generateKeyPair() + + val subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic.getEncoded) + + val certBuilder = new X509v3CertificateBuilder( + new X500Name("CN=localhost"), + new java.math.BigInteger(64, new java.security.SecureRandom()), + new Date(System.currentTimeMillis()), + new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000), + new X500Name("CN=localhost"), + subPubKeyInfo + ) + + val signer: ContentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate) + val cert: X509Certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)) + + val keyStore = KeyStore.getInstance("JKS") + keyStore.load(null, null) + keyStore.setKeyEntry("selfsigned", keyPair.getPrivate, "password".toCharArray, Array(cert)) + + keyStore + } + + def sslContext:HttpsConnectionContext = { + val keyStore = CertificateUtils.generateSelfSignedCertificate + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + keyManagerFactory.init(keyStore, "password".toCharArray) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(keyStore) + + val sslContext: SSLContext = SSLContext.getInstance("TLS") + sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, null) + + ConnectionContext.httpsServer(sslContext) + + } +} diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/oidc/UserInfo.scala b/shared/src/main/scala/ch/wsl/box/model/shared/oidc/UserInfo.scala index 0ebc75de..44dfdc3b 100644 --- a/shared/src/main/scala/ch/wsl/box/model/shared/oidc/UserInfo.scala +++ b/shared/src/main/scala/ch/wsl/box/model/shared/oidc/UserInfo.scala @@ -6,6 +6,7 @@ import io.circe.syntax.EncoderOps import io.circe.{Decoder, HCursor, Json, JsonObject} case class UserInfo( + sub:String, name:String, preferred_username:String, email:Option[String], @@ -15,18 +16,19 @@ case class UserInfo( object UserInfo { - def simple(username:String) = UserInfo(username,username,None,Seq(),Json.Null) + def simple(username:String) = UserInfo(username,username,username,None,Seq(),Json.Null) implicit val decoderRaw: Decoder[UserInfo] = new Decoder[UserInfo] { override def apply(c: HCursor): Result[UserInfo] = for { name <- c.downField("name").as[String] - preferred_username <- c.downField("preferred_username").as[String] email <- c.downField("email").as[Option[String]] + preferred_username <- c.downField("preferred_username").as[Option[String]] + sub <- c.downField("sub").as[String] roles <- c.downField("roles").as[Option[Seq[String]]] claims <- c.downField("claims").as[Option[Json]] } yield { val newClaims:Json = claims.getOrElse(Json.obj()).deepMerge(c.value.asObject.map(_.filterKeys(_ != "claims").asJson).getOrElse(Json.obj())) - UserInfo(name, preferred_username, email,roles.toList.flatten,newClaims) + UserInfo(sub, name, preferred_username.orElse(email).getOrElse(sub), email,roles.toList.flatten,newClaims) } } } \ No newline at end of file