├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── build.sbt ├── launch-test-server ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── com │ │ └── tresata │ │ └── akka │ │ └── http │ │ └── spnego │ │ ├── KerberosConfiguration.scala │ │ ├── SpnegoAuthenticator.scala │ │ ├── SpnegoDirectives.scala │ │ └── Token.scala └── test │ └── scala │ └── com │ └── tresata │ └── akka │ └── http │ └── spnego │ └── TokenSpec.scala └── test-server └── src └── main ├── resources ├── application.conf ├── log4j.properties └── testKeystore └── scala └── com └── tresata └── akka └── http └── spnego └── Main.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - java: 8 12 | - java: 11 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v1 17 | - name: setup 18 | uses: olafurpg/setup-scala@v10 19 | with: 20 | java-version: "adopt@1.${{ matrix.java }}" 21 | - name: test 22 | run: sbt -v -Dfile.encoding=UTF8 +test 23 | shell: bash 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | lib_managed/ 3 | tmp/ 4 | .bsp/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/tresata/akka-http-spnego/actions/workflows/ci.yml/badge.svg) 2 | 3 | # akka-http-spnego 4 | akka-http-spnego provides Kerberos based authentication for akka-http using SPNEGO. SPNEGO is a way to do GSSAPI authentication between clients and servers using the HTTP authentication header. 5 | 6 | The included test-server project is both an example of how to use this project and a way to quickly test it within your Kerberos environment. It provides a secure ping-pong service (you do a GET to endpoint /ping and it responds with pong for the authenticated user). 7 | To run it first make sure: 8 | * Kerberos is properly installed and configured on both server and client 9 | * DNS is properly configured for the server (it's hostname resolves correctly to its ip-address and the other way around). 10 | 11 | Next do the following: 12 | * Create a keytab for HTTP/yourserver@YOURDOMAIN on the server 13 | * Edit test-server/src/main/resources/application.conf to set tresata.akka.http.spnego.kerberos.principal and tresata.akka.http.spnego.kerberos.keytab 14 | * Launch the test server with: ./launch-test-server 15 | * On the client make sure you are logged into Kerberos (with kinit) 16 | * On the client create a test connection with: curl -k --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt https://yourserver:12345/ping 17 | 18 | Have fun! 19 | Team @ Tresata 20 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val sharedSettings = Seq( 2 | organization := "com.tresata", 3 | version := "0.6.0-SNAPSHOT", 4 | licenses += "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt"), 5 | scalaVersion := "2.12.15", 6 | crossScalaVersions := Seq("2.12.15", "2.13.5"), 7 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-target:jvm-1.8", "-feature", "-language:_", "-Xlint:-package-object-classes,-adapted-args,_", 8 | "-Ywarn-dead-code", "-Ywarn-value-discard", "-Ywarn-unused"), 9 | Test / compile / scalacOptions := (Test / compile / scalacOptions).value.filter(_ != "-Ywarn-value-discard").filter(_ != "-Ywarn-unused"), 10 | Compile / console / scalacOptions := (Compile / console / scalacOptions).value.filter(_ != "-Ywarn-unused-import"), 11 | Test / console / scalacOptions := (Test / console / scalacOptions).value.filter(_ != "-Ywarn-unused-import"), 12 | publishMavenStyle := true, 13 | pomIncludeRepository := { x => false }, 14 | Test / publishArtifact := false, 15 | publishTo := { 16 | if (isSnapshot.value) 17 | Some("snapshots" at "https://server02.tresata.com:8084/artifactory/oss-libs-snapshot-local") 18 | else 19 | Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2") 20 | }, 21 | credentials += Credentials(Path.userHome / ".m2" / "credentials_sonatype"), 22 | credentials += Credentials(Path.userHome / ".m2" / "credentials_artifactory"), 23 | pomExtra := ( 24 | https://github.com/tresata/akka-http-spnego 25 | 26 | git@github.com:tresata/akka-http-spnego.git 27 | scm:git:git@github.com:tresata/akka-http-spnego.git 28 | 29 | 30 | 31 | koertkuipers 32 | Koert Kuipers 33 | https://github.com/koertkuipers 34 | 35 | 36 | ) 37 | ) 38 | 39 | lazy val `akka-http-spnego` = (project in file(".")).settings( 40 | sharedSettings 41 | ).settings( 42 | name := "akka-http-spnego", 43 | libraryDependencies ++= Seq( 44 | "org.slf4j" % "slf4j-api" % "1.7.25" % "compile", 45 | "com.typesafe.akka" %% "akka-http" % "10.2.6" % "compile", 46 | "com.typesafe.akka" %% "akka-stream" % "2.6.16" % "compile", 47 | "commons-codec" % "commons-codec" % "1.15" % "compile", 48 | "org.scalatest" %% "scalatest-funspec" % "3.2.10" % "test" 49 | ) 50 | ) 51 | 52 | lazy val `test-server` = (project in file("test-server")).settings( 53 | sharedSettings 54 | ).settings( 55 | name := "test-server", 56 | libraryDependencies ++= Seq( 57 | "org.slf4j" % "slf4j-log4j12" % "1.7.25" % "compile", 58 | "com.typesafe.akka" %% "akka-slf4j" % "2.6.16" % "compile" 59 | ), 60 | publish := { }, 61 | publishLocal := { } 62 | ).dependsOn(`akka-http-spnego`) 63 | -------------------------------------------------------------------------------- /launch-test-server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sbt -Djavax.net.ssl.keyStore=test-server/src/main/resources/testKeystore -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.debug=ssl test-server/run 4 | 5 | # curl -k --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt https://someserver:12345/ping 6 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.5 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.1.1") 2 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | tresata.akka.http.spnego { 2 | signature.secret = "secret" 3 | token.validity = 3600 seconds 4 | cookie.domain = "" 5 | cookie.path = "" 6 | kerberos.principal = "HTTP/server@DOMAIN.COM" 7 | kerberos.keytab = "/tmp/http.keytab" 8 | kerberos.debug = false 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/com/tresata/akka/http/spnego/KerberosConfiguration.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import javax.security.auth.login.{Configuration, AppConfigurationEntry} 4 | 5 | import scala.collection.JavaConverters._ 6 | 7 | case class KerberosConfiguration(keytab: String, principal: String, debug: Boolean) extends Configuration { 8 | val ticketCache = Option(System.getenv("KRB5CCNAME")) 9 | 10 | override def getAppConfigurationEntry(name: String): Array[AppConfigurationEntry] = Array( 11 | new AppConfigurationEntry( 12 | "com.sun.security.auth.module.Krb5LoginModule", 13 | AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, 14 | (Map( 15 | "keyTab" -> keytab, 16 | "principal" -> principal, 17 | "useKeyTab" -> "true", 18 | "storeKey" -> "true", 19 | "doNotPrompt" -> "true", 20 | "useTicketCache" -> "true", 21 | "renewTGT" -> "true", 22 | "isInitiator" -> "false", 23 | "refreshKrb5Config" -> "true", 24 | "debug" -> debug.toString 25 | ) ++ ticketCache.map{ x => Map("ticketCache" -> x) }.getOrElse(Map.empty)).asJava 26 | ) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/com/tresata/akka/http/spnego/SpnegoAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import java.io.IOException 4 | import java.nio.charset.StandardCharsets.UTF_8 5 | import java.security.{PrivilegedAction, PrivilegedExceptionAction, PrivilegedActionException} 6 | import javax.security.auth.Subject 7 | import javax.security.auth.login.LoginContext 8 | import javax.security.auth.kerberos.KerberosPrincipal 9 | import scala.concurrent.Future 10 | import scala.collection.JavaConverters._ 11 | 12 | import com.typesafe.config.{Config, ConfigFactory} 13 | 14 | import org.ietf.jgss.{GSSManager, GSSCredential} 15 | 16 | import akka.event.LoggingAdapter 17 | 18 | import akka.http.scaladsl.model.HttpHeader 19 | import akka.http.scaladsl.model.headers.{Cookie, HttpCookie, HttpChallenge, RawHeader} 20 | import akka.http.scaladsl.server.{Directive0, Directive1, RequestContext, Rejection, AuthenticationFailedRejection, MalformedHeaderRejection} 21 | import akka.http.scaladsl.server.AuthenticationFailedRejection.{CredentialsMissing, CredentialsRejected} 22 | import akka.http.scaladsl.server.directives.CookieDirectives.setCookie 23 | import akka.http.scaladsl.server.directives.BasicDirectives.{extractExecutionContext, extractLog, extract, provide} 24 | import akka.http.scaladsl.server.directives.RouteDirectives.reject 25 | import akka.http.scaladsl.server.directives.FutureDirectives.onSuccess 26 | 27 | import org.apache.commons.codec.binary.Base64 28 | 29 | object SpnegoAuthenticator { 30 | private val cookieName = "akka.http.spnego" 31 | private val Authorization = "authorization" 32 | private val negotiate = "Negotiate" 33 | private val wwwAuthenticate = "WWW-Authenticate" 34 | 35 | def apply(config: Config = ConfigFactory.load())(implicit log: LoggingAdapter): SpnegoAuthenticator = { 36 | val principal = config.getString("tresata.akka.http.spnego.kerberos.principal") 37 | val keytab = config.getString("tresata.akka.http.spnego.kerberos.keytab") 38 | val debug = config.getBoolean("tresata.akka.http.spnego.kerberos.debug") 39 | val domain = Some(config.getString("tresata.akka.http.spnego.cookie.domain")).flatMap{ x => if (x == "") None else Some(x) } 40 | val path = Some(config.getString("tresata.akka.http.spnego.cookie.path")).flatMap{ x => if (x == "") None else Some(x) } 41 | val tokenValidity = config.getDuration("tresata.akka.http.spnego.token.validity") 42 | val signatureSecret = config.getString("tresata.akka.http.spnego.signature.secret") 43 | 44 | log.info("principal {}", principal) 45 | log.info("keytab {}", keytab) 46 | log.info("debug {}", debug) 47 | log.info("domain {}", domain) 48 | log.info("path {}", path) 49 | log.info("token validity {}", tokenValidity) 50 | 51 | val tokens = new Tokens(tokenValidity.toMillis, signatureSecret.getBytes(UTF_8)) 52 | new SpnegoAuthenticator(principal, keytab, debug, domain, path, tokens) 53 | } 54 | 55 | def spnegoAuthenticate(config: Config = ConfigFactory.load()): Directive1[Token] = { 56 | extractExecutionContext.flatMap{ implicit ec => 57 | extractLog.flatMap{ implicit log => 58 | log.debug("creating spnego authenticator") 59 | val spnego = SpnegoAuthenticator(config) 60 | extract(ctx => Future(spnego.apply(ctx))).flatMap(onSuccess(_)).flatMap{ 61 | case Left(rejection) => reject(rejection) 62 | case Right(token) => provide(token) & spnego.setSpnegoCookie(token) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | class SpnegoAuthenticator(principal: String, keytab: String, debug: Boolean, domain: Option[String], path: Option[String], tokens: Tokens)(implicit log: LoggingAdapter) { 70 | import SpnegoAuthenticator._ 71 | 72 | private val subject = new Subject(false, Set(new KerberosPrincipal(principal)).asJava, Set.empty[AnyRef].asJava, Set.empty[AnyRef].asJava) 73 | private val kerberosConfiguration = new KerberosConfiguration(keytab, principal, debug) 74 | 75 | private val loginContext = new LoginContext("", subject, null, kerberosConfiguration) 76 | loginContext.login() 77 | 78 | private val gssManager = Subject.doAs(loginContext.getSubject, new PrivilegedAction[GSSManager] { 79 | override def run: GSSManager = GSSManager.getInstance 80 | }) 81 | 82 | private def cookieToken(ctx: RequestContext): Option[Either[Rejection, Token]] = try { 83 | ctx.request.headers.collectFirst{ 84 | case Cookie(cookies) => cookies.find(_.name == cookieName).map{ cookie => log.debug("cookie found"); cookie } 85 | }.flatten.flatMap{ cookie => 86 | Some(tokens.parse(cookie.value)).filter(!_.expired).map{ token => log.debug("spnego token inside cookie not expired"); token } 87 | }.map(Right(_)) 88 | } catch { 89 | case e: TokenParseException => Some(Left(MalformedHeaderRejection(s"Cookie: ${cookieName}", e.getMessage, Some(e)))) // malformed token in cookie 90 | } 91 | 92 | private def clientToken(ctx: RequestContext): Option[Array[Byte]] = ctx.request.headers.collectFirst{ 93 | case HttpHeader(Authorization, value) => value 94 | }.filter(_.startsWith(negotiate)).map{ authHeader => 95 | log.debug("authorization header found") 96 | new Base64(0).decode(authHeader.substring(negotiate.length).trim) 97 | } 98 | 99 | private def challengeHeader(maybeServerToken: Option[Array[Byte]] = None): HttpHeader = RawHeader( 100 | wwwAuthenticate, 101 | negotiate + maybeServerToken.map(" " + new Base64(0).encodeToString(_)).getOrElse("") 102 | ) 103 | 104 | private def kerberosCore(clientToken: Array[Byte]): Either[Rejection, Token] = { 105 | try { 106 | val (maybeServerToken, maybeToken) = Subject.doAs(loginContext.getSubject, new PrivilegedExceptionAction[(Option[Array[Byte]], Option[Token])] { 107 | override def run: (Option[Array[Byte]], Option[Token]) = { 108 | val gssContext = gssManager.createContext(null: GSSCredential) 109 | try { 110 | ( 111 | Option(gssContext.acceptSecContext(clientToken, 0, clientToken.length)), 112 | if (gssContext.isEstablished) Some(tokens.create(gssContext.getSrcName.toString)) else None 113 | ) 114 | } catch { 115 | case e: Throwable => 116 | log.error(e, "error in establishing security context") 117 | throw e 118 | } finally { 119 | gssContext.dispose() 120 | } 121 | } 122 | }) 123 | if (log.isDebugEnabled) 124 | log.debug("maybeServerToken {} maybeToken {}", maybeServerToken.map(new Base64(0).encodeToString(_)), maybeToken) 125 | maybeToken.map{ token => 126 | log.debug("received new token") 127 | Right(token) 128 | }.getOrElse{ 129 | log.debug("no token received but if there is a serverToken then negotiations are ongoing") 130 | Left(AuthenticationFailedRejection(CredentialsMissing, HttpChallenge(challengeHeader(maybeServerToken).value, None))) 131 | } 132 | } catch { 133 | case e: PrivilegedActionException => e.getException match { 134 | case e: IOException => throw e // server error 135 | case e: Throwable => 136 | log.error(e, "negotiation failed") 137 | Left(AuthenticationFailedRejection(CredentialsRejected, HttpChallenge(challengeHeader().value, None))) // rejected 138 | } 139 | } 140 | } 141 | 142 | private def kerberosNegotiate(ctx: RequestContext): Option[Either[Rejection, Token]] = clientToken(ctx).map(kerberosCore) 143 | 144 | private def initiateNegotiations: Either[Rejection, Token] = { 145 | log.debug("no negotiation header found, initiating negotiations") 146 | Left(AuthenticationFailedRejection(CredentialsMissing, HttpChallenge(challengeHeader().value, None))) 147 | } 148 | 149 | def apply(ctx: RequestContext): Either[Rejection, Token] = cookieToken(ctx).orElse(kerberosNegotiate(ctx)).getOrElse(initiateNegotiations) 150 | 151 | def setSpnegoCookie(token: Token): Directive0 = setCookie(HttpCookie(cookieName, tokens.serialize(token), domain = domain, path = path)) 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/main/scala/com/tresata/akka/http/spnego/SpnegoDirectives.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import com.typesafe.config.{Config, ConfigFactory} 4 | 5 | import akka.http.scaladsl.server.Directive1 6 | 7 | trait SpnegoDirectives { 8 | def spnegoAuthenticate(config: Config = ConfigFactory.load()): Directive1[Token] = SpnegoAuthenticator.spnegoAuthenticate(config) 9 | } 10 | 11 | object SpnegoDirectives extends SpnegoDirectives 12 | -------------------------------------------------------------------------------- /src/main/scala/com/tresata/akka/http/spnego/Token.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.charset.StandardCharsets.UTF_8 5 | import java.security.MessageDigest 6 | import scala.util.{Try, Success} 7 | 8 | import org.apache.commons.codec.binary.Base64 9 | 10 | case class TokenParseException(message: String) extends Exception(message) 11 | 12 | class Tokens(tokenValidity: Long, signatureSecret: Array[Byte]) { 13 | private def newExpiration: Long = System.currentTimeMillis + tokenValidity 14 | 15 | private[spnego] def sign(token: Token): String = { 16 | val md = MessageDigest.getInstance("SHA") 17 | md.update(token.principal.getBytes(UTF_8)) 18 | val bb = ByteBuffer.allocate(8) 19 | bb.putLong(token.expiration) 20 | md.update(bb.array) 21 | md.update(signatureSecret) 22 | new Base64(0).encodeToString(md.digest) 23 | } 24 | 25 | def create(principal: String): Token = Token(principal, newExpiration) 26 | 27 | def parse(tokenString: String): Token = tokenString.split("&") match { 28 | case Array(principal, expirationString, signature) => Try(expirationString.toLong) match { 29 | case Success(expiration) => { 30 | val token = Token(principal, expiration) 31 | if (sign(token) != signature) throw TokenParseException("incorrect signature") 32 | token 33 | } 34 | case _ => throw TokenParseException("expiration not a long") 35 | } 36 | case _ => throw TokenParseException("incorrect number of fields") 37 | } 38 | 39 | def serialize(token: Token): String = List(token.principal, token.expiration, sign(token)).mkString("&") 40 | } 41 | 42 | case class Token private[spnego] (principal: String, expiration: Long) { 43 | def expired: Boolean = System.currentTimeMillis > expiration 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/com/tresata/akka/http/spnego/TokenSpec.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import java.nio.charset.StandardCharsets.UTF_8 4 | import org.scalatest.funspec.AnyFunSpec 5 | 6 | class TokenSpec extends AnyFunSpec { 7 | describe("Token") { 8 | val tokens = new Tokens(3600 * 1000, "secret".getBytes(UTF_8)) 9 | 10 | val token = tokens.create("HTTP/someserver.example.com@EXAMPLE.COM") 11 | 12 | it("should not be expired immediately"){ 13 | assert(token.expired === false) 14 | } 15 | 16 | it("should rountrip serialization"){ 17 | assert(token === tokens.parse(tokens.serialize(token))) 18 | assert(token.expired === false) 19 | } 20 | 21 | it("should throw an exception when parsing a tokenString with an incorrect signature"){ 22 | intercept[TokenParseException] { 23 | tokens.parse(List(token.principal, token.expiration, tokens.sign(token) + "a").mkString("&")) 24 | } 25 | } 26 | 27 | it("should throw an exception when serializing an incomplete tokenString"){ 28 | intercept[TokenParseException] { 29 | tokens.parse("a&1") 30 | } 31 | } 32 | 33 | it("should throw an exception when serializing an illegal tokenString"){ 34 | intercept[TokenParseException] { 35 | tokens.parse("a&b&c") 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test-server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | tresata.akka.http.spnego { 2 | kerberos.principal = "HTTP/someserver@SOMEDOMAIN" 3 | kerberos.keytab = "/etc/http.keytab" 4 | kerberos.debug = true 5 | } 6 | 7 | akka { 8 | loglevel = DEBUG 9 | stdout-loglevel = DEBUG 10 | event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] 11 | actor { 12 | debug.unhandled = on 13 | } 14 | } 15 | 16 | akka.http.server { 17 | verbose-error-messages = on 18 | } 19 | -------------------------------------------------------------------------------- /test-server/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG,stdout 2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 3 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 4 | log4j.appender.stdout.layout.ConversionPattern=%c: %m%n 5 | 6 | #log4j.logger.com.tresata.akka.http.spnego=DEBUG 7 | -------------------------------------------------------------------------------- /test-server/src/main/resources/testKeystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tresata/akka-http-spnego/189f337825f8d83ce5abbce3ca1e9fc7e10c2b9b/test-server/src/main/resources/testKeystore -------------------------------------------------------------------------------- /test-server/src/main/scala/com/tresata/akka/http/spnego/Main.scala: -------------------------------------------------------------------------------- 1 | package com.tresata.akka.http.spnego 2 | 3 | import java.io.FileInputStream 4 | import java.security.{KeyStore, SecureRandom} 5 | import javax.net.ssl.{KeyManagerFactory, TrustManagerFactory, SSLContext} 6 | 7 | import akka.actor.ActorSystem 8 | import akka.stream.ActorMaterializer 9 | import akka.http.scaladsl.{ConnectionContext, Http} 10 | import akka.http.scaladsl.server.Directives._ 11 | 12 | import com.tresata.akka.http.spnego.SpnegoDirectives._ 13 | 14 | object Main extends App { 15 | implicit val system: ActorSystem = ActorSystem() 16 | implicit val materializer: ActorMaterializer = ActorMaterializer() 17 | 18 | val route = logRequestResult("debug") { 19 | spnegoAuthenticate(){ token => 20 | get{ 21 | path("ping") { 22 | complete(s"pong for user ${token.principal}") 23 | } 24 | } 25 | } 26 | } 27 | 28 | // very painful ssl stuff 29 | val keystore = System.getProperty("javax.net.ssl.keyStore") 30 | val password = System.getProperty("javax.net.ssl.keyStorePassword") 31 | val ks = KeyStore.getInstance("JKS") 32 | val fis = new FileInputStream(keystore) 33 | try ks.load(fis, password.toCharArray) 34 | finally fis.close() 35 | val kmf = KeyManagerFactory.getInstance("SunX509") 36 | kmf.init(ks, password.toCharArray) 37 | val tmf = TrustManagerFactory.getInstance("SunX509") 38 | tmf.init(ks) 39 | val sslContext = SSLContext.getInstance("TLS") 40 | sslContext.init(kmf.getKeyManagers, tmf.getTrustManagers, new SecureRandom) 41 | val connectionContext = ConnectionContext.httpsServer(sslContext) 42 | 43 | Http() 44 | .newServerAt("0.0.0.0", 12345) 45 | .enableHttps(connectionContext) 46 | .bind(route) 47 | } 48 | --------------------------------------------------------------------------------