Skip to content

Commit

Permalink
Merge pull request #1679 from dedis/work-be2-jharaxus-authentication-…
Browse files Browse the repository at this point in the history
…message-response-handling

Properly handle authentication message to complete popcha authentication
  • Loading branch information
K1li4nL authored Feb 10, 2024
2 parents d5a7fe3 + 10bed69 commit cc54846
Show file tree
Hide file tree
Showing 38 changed files with 923 additions and 105 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,10 @@ jobs:
run: sbt scalafmtCheckAll

- name: Run unit tests
run: sbt -Dscala.config="src/main/scala/ch/epfl/pop/config" -Dtest clean coverage test
run: |
cd ./src/security/ && ./generateKeys.sh -test && cd ../.. # Generate test keys
sbt -Dscala.config="src/main/scala/ch/epfl/pop/config" -Dscala.security="src/security" -Dtest clean coverage test
rm -rf ./src/security/test/ # Remove test keys
- name: Report coverage
run: sbt coverageReport
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/karate_be2-scala.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ jobs:

- name: Assemble server jar file
working-directory: ./be2-scala
run: sbt assembly

run: |
sbt assembly
cd ./src/security/ && ./generateKeys.sh # Generate keys
- name: Run Karate server-client tests
id: tests_client
continue-on-error: true
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Cryptographic keys:
*.pem
*.der

# Dependency directories (remove the comment below to include it)

# vendor/
Expand Down
24 changes: 21 additions & 3 deletions be2-scala/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ There are two main possible ways of running the project :
1. Import the project using your editor
2. Modify the default Run configuration 'Server', to include the following __VM option__:

__```-Dscala.config=src/main/scala/ch/epfl/pop/config```__ (click on **Modify options** and tick **Add VM options** if VM options box does not appear initially)
__```-Dscala.config=src/main/scala/ch/epfl/pop/config -Dscala.security=src/security```__ (click on **Modify options** and tick **Add VM options** if VM options box does not appear initially)

![](docs/images/intellij-vm.png)

Expand All @@ -33,7 +33,7 @@ Here are a few points that students often forget when setting up IntelliJ:

### Option 2: SBT

Using `sbt -Dscala.config="path/to/config/file" run`.
Using `sbt -Dscala.config="path/to/config/file" -Dscala.security="src/security" run`.

There is a default configuration ready to use in
`src/main/scala/ch/epfl/pop/config` which contains an __application.config__
Expand All @@ -55,16 +55,31 @@ ch_epfl_pop_Server {
Consequently, from **be2-scala/** folder run the following:

```bash
sbt -Dscala.config="src/main/scala/ch/epfl/pop/config" run
sbt -Dscala.config="src/main/scala/ch/epfl/pop/config" -Dscala.security="src/security" run
```

---

## Security keys

The scala server needs security keys to run properly. Their location must be specified using the `-Dscala.security` flag.

By default, the folder is `src/security` and the script `src/security/generateKeys.sh` can be used to generate fresh keys.
> Go to the folder `src/security` and run `./generateKeys.sh`, or directly run `(cd ./src/security/ && ./generateKeys.sh)` from the current directory.
Security keys are also needed to run the tests. The process is the same except that tests expect to find the keys in a `test` folder (for example `src/security/test`), so run the script `generateKeys.sh` with the argument `-test` to take that into account. You don't need to generate new keys every time you run the tests.
> Go to the folder `src/security` and run `./generateKeys.sh -test`, or directly run `(cd ./src/security/ && ./generateKeys.sh -test)` from the current directory before running the tests.
Note that the script `generateKeys.sh` requires [**openssl**](https://www.openssl.org/) to be available on the system.

---

## Preprocessor flags

We introduced two custom [preprocessor flags](https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html), one of which you already encountered:

- Config file location (**mandatory**): location of the config file on the system with respect to the be2-scala folder
- Security keys location (**mandatory**): location of the security keys on the system with respect to the be2-scala folder
- Database auto-cleanup (optional). By adding the `-Dclean` flag, the database will be recreated everytime the server starts running

---
Expand All @@ -80,6 +95,9 @@ The project relies on several sbt dependencies (external libraries) :
- encryption : [**kyber**](https://github.com/dedis/kyber) to encrypt and decrypt messages of an election
- testing : [**scalatest**](https://www.scalatest.org/) for unit tests
- Json schema validator : [**networknt**](https://github.com/networknt/json-schema-validator) for Json schema validation
- Json Web Tokens (jwt) : [java-jwt](https://github.com/auth0/java-jwt) to generate and sign jwt
- Qrcode : [**zxing**](https://github.com/zxing/zxing) to generate Qrcodes
- security keys : [**openssl**](https://www.openssl.org/) only used in `src/security/generateKeys.sh` to generate a pair of RSA keys

---

Expand Down
4 changes: 4 additions & 0 deletions be2-scala/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ libraryDependencies ++= Seq(

"ch.qos.logback" % "logback-classic" % "1.1.3" % Runtime, // Akka logging library
"com.typesafe.akka" %% "akka-testkit" % AkkaVersion % Test, // Akka actor test kit (akka actor testing library)
"com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion, // Akka stream test kit
"com.typesafe.akka" %% "akka-http-testkit" % AkkaHttpVersion // Akka http test kit
)

Expand Down Expand Up @@ -172,4 +173,7 @@ libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
// QRCODE generation library
libraryDependencies += "com.google.zxing" % "core" % "3.5.1"

// JWT library
libraryDependencies += "com.auth0" % "java-jwt" % "4.4.0"

conflictManager := ConflictManager.latestCompatible
19 changes: 14 additions & 5 deletions be2-scala/src/main/scala/ch/epfl/pop/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{RequestContext, RouteResult}
import akka.pattern.AskableActorRef
import akka.util.Timeout
import ch.epfl.pop.authentication.GetRequestHandler
import ch.epfl.pop.authentication.{GetRequestHandler, PopchaWebSocketResponseHandler}
import ch.epfl.pop.config.RuntimeEnvironment
import ch.epfl.pop.config.RuntimeEnvironment.{ownAuthAddress, ownClientAddress, ownServerAddress, serverConf}
import ch.epfl.pop.config.RuntimeEnvironment._
import ch.epfl.pop.decentralized.{ConnectionMediator, HeartbeatGenerator, Monitor}
import ch.epfl.pop.pubsub.{MessageRegistry, PubSubMediator, PublishSubscribe}
import ch.epfl.pop.storage.DbActor
import ch.epfl.pop.storage.{DbActor, SecurityModuleActor}
import org.iq80.leveldb.Options

import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -44,12 +44,14 @@ object Server {

val messageRegistry: MessageRegistry = MessageRegistry()
val pubSubMediatorRef: ActorRef = system.actorOf(PubSubMediator.props, "PubSubMediator")

val dbActorRef: AskableActorRef = system.actorOf(Props(DbActor(pubSubMediatorRef, messageRegistry)), "DbActor")
val securityModuleActorRef: AskableActorRef = system.actorOf(Props(SecurityModuleActor(RuntimeEnvironment.securityPath)))

// Create necessary actors for server-server communications
val heartbeatGenRef: ActorRef = system.actorOf(HeartbeatGenerator.props(dbActorRef))
val monitorRef: ActorRef = system.actorOf(Monitor.props(heartbeatGenRef))
val connectionMediatorRef: ActorRef = system.actorOf(ConnectionMediator.props(monitorRef, pubSubMediatorRef, dbActorRef, messageRegistry))
val connectionMediatorRef: ActorRef = system.actorOf(ConnectionMediator.props(monitorRef, pubSubMediatorRef, dbActorRef, securityModuleActorRef, messageRegistry))

// Setup routes
def publishSubscribeRoute: RequestContext => Future[RouteResult] = {
Expand All @@ -58,6 +60,7 @@ object Server {
PublishSubscribe.buildGraph(
pubSubMediatorRef,
dbActorRef,
securityModuleActorRef,
messageRegistry,
monitorRef,
connectionMediatorRef,
Expand All @@ -69,6 +72,7 @@ object Server {
PublishSubscribe.buildGraph(
pubSubMediatorRef,
dbActorRef,
securityModuleActorRef,
messageRegistry,
monitorRef,
connectionMediatorRef,
Expand All @@ -78,9 +82,12 @@ object Server {
}
}

def getRequestsRoute = GetRequestHandler.buildRoutes(serverConf)
def getRequestsRoute = GetRequestHandler.buildRoutes(serverConf, securityModuleActorRef)

def authenticateWsResponseRoute = PopchaWebSocketResponseHandler.buildRoute(serverConf)(system)

def allRoutes = concat(
authenticateWsResponseRoute,
getRequestsRoute,
publishSubscribeRoute
)
Expand All @@ -94,6 +101,8 @@ object Server {
println(f"[Client] ch.epfl.pop.Server online at $ownClientAddress")
println(f"[Server] ch.epfl.pop.Server online at $ownServerAddress")
println(f"[Server] ch.epfl.pop.Server auth server online at $ownAuthAddress")
println(f"[Server] ch.epfl.pop.Server auth ws server online at $ownResponseAddress")
println(f"[Server] ch.epfl.pop.Server public key available at $ownPublicKeyAddress")

case Failure(_) =>
logger.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object Authenticate {
val params = RequestParameters(response_type, client_id, redirect_uri, scope, state, response_mode, login_hint, nonce)
verifyParameters(params) match {
case Left(error -> errorDescription) => complete(authenticationFailure(error, errorDescription, state))
case Right(_) => complete(generateChallenge(request))
case Right(_) => complete(generateChallenge(request, redirect_uri, login_hint, client_id, nonce))
}
}
}
Expand All @@ -57,8 +57,8 @@ object Authenticate {
}
}

private def generateChallenge(request: HttpRequest): HttpResponse = {
val challengeEntity = QRCodeChallengeGenerator.generateChallengeContent(request.uri.toString())
private def generateChallenge(request: HttpRequest, redirectUri: String, laoId: String, clientId: String, nonce: String): HttpResponse = {
val challengeEntity = QRCodeChallengeGenerator.generateChallengeContent(request.uri.toString(), redirectUri, laoId, clientId, nonce)
HttpResponse(status = StatusCodes.OK, entity = challengeEntity)
}

Expand All @@ -72,7 +72,7 @@ object Authenticate {
}

private def verifyResponseType(response_type: String): VerificationState = {
val expectedResponseType = "id_token token"
val expectedResponseType = "id_token"
if (response_type == expectedResponseType)
Right(())
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
package ch.epfl.pop.authentication

import akka.http.scaladsl.model._
import akka.http.scaladsl.server
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.server.Directives.path
import akka.http.scaladsl.server.Directives.{complete, pathPrefix}
import akka.pattern.AskableActorRef
import akka.util.Timeout
import ch.epfl.pop.config.ServerConf
import ch.epfl.pop.storage.SecurityModuleActor.{ReadRsaPublicKeyPem, ReadRsaPublicKeyPemAck}

import java.util.concurrent.TimeUnit
import scala.concurrent.Await
import scala.util.Success

/** Object to handle the http-get requests supported by the server
*/
object GetRequestHandler {

def buildRoutes(config: ServerConf): server.Route = {
// Implicit for system actors
implicit val timeout: Timeout = Timeout(1, TimeUnit.SECONDS)

/** Build routes to handle the http-get requests supported by the server
* @param config
* server configuration to use
* @param securityModuleActorRef
* security module to use for secret keys
* @return
* a route to handle get requests
*/
def buildRoutes(config: ServerConf, securityModuleActorRef: AskableActorRef): server.Route = {
Directives.get {
buildPathRoute(config.authenticationPath, Authenticate.buildRoute())
Directives.concat(
buildPathRoute(config.authenticationPath, Authenticate.buildRoute()),
buildPathRoute(config.publicKeyEndpoint, fetchPublicKey(securityModuleActorRef))
)
}
}

private def buildPathRoute(pathName: String, route: server.Route): server.Route = {
path(pathName) {
pathPrefix(pathName) {
route
}
}

private def fetchPublicKey(securityModuleActorRef: AskableActorRef): server.Route = {
complete {
Await.ready(securityModuleActorRef ? ReadRsaPublicKeyPem(), timeout.duration).value match {
case Some(Success(ReadRsaPublicKeyPemAck(publicKey))) => requestSuccess(publicKey)
case Some(reply) => requestFailure("Server error", reply.toString)
case None => requestFailure("Server error", "No response received from DB")
}
}
}

private def requestFailure(error: String, errorDescription: String): HttpResponse = {
HttpResponse(status = StatusCodes.OK)
.addAttribute(AttributeKey("error"), error)
.addAttribute(AttributeKey("error_description"), errorDescription)
}

private def requestSuccess(response: String) = {
HttpResponse(status = StatusCodes.OK, entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, response))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package ch.epfl.pop.authentication

import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.model.ws.{Message, TextMessage}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.stream.{CompletionStrategy, OverflowStrategy}
import akka.{Done, NotUsed}
import ch.epfl.pop.config.ServerConf

import scala.concurrent.{Future, Promise}

/** This websocket handler receives the JWT from the PopchaHandler via websocket and forwards it to the client webpage connected via websocket as well.
*/
object PopchaWebSocketResponseHandler {

private val LISTENER_BUFFER_SIZE: Int = 256

private val socketsConnected: collection.mutable.Map[(String, String, String), ActorRef] = collection.mutable.Map()

/** Builds a route to handle the websocket requests received
* @param config
* server configuration to use
* @param system
* actor system to use to spawn actors
* @return
* a route that handles the server's response websocket messages
*/
def buildRoute(config: ServerConf)(implicit system: ActorSystem): Route = {
path(config.responseEndpoint / Segment / "authentication" / Segment / Segment) {
(laoId: String, clientId: String, nonce: String) =>
handleWebSocketMessages(handleMessage(laoId, clientId, nonce))
}
}

/** Handle a message received using its socket id (laoId, clientId, nonce):
* - If it is the first connection on this socket id, the connection is handled as a listener (waiting for a message).
* - If it is the second connection on this socket id, the connection is handled as a server sending messages to the listener waiting.
*/
private def handleMessage(laoId: String, clientId: String, nonce: String)(implicit system: ActorSystem): Flow[Message, Message, Any] = {
val socketId = (laoId, clientId, nonce)
socketsConnected.get(socketId) match {
case Some(listener) => handleMessageAsServer(listener, socketId)
case None => handleMessageAsListener(socketId)
}
}

private def handleMessageAsListener(socketId: (String, String, String)): Flow[Message, Message, Any] = {
val dummySink = Sink.ignore
val source: Source[TextMessage, NotUsed] = Source
.actorRef(
{
case Done => CompletionStrategy.immediately
},
PartialFunction.empty,
bufferSize = LISTENER_BUFFER_SIZE,
overflowStrategy = OverflowStrategy.dropBuffer
)
.mapMaterializedValue(wsHandle => {
socketsConnected.put(socketId, wsHandle)
NotUsed
})
Flow.fromSinkAndSourceCoupled(dummySink, source)
}

private def handleMessageAsServer(listenerRef: ActorRef, socketId: (String, String, String)): Flow[Message, Message, Any] = {
val sink: Sink[Message, Future[Done]] =
Sink.foreach {
case message: TextMessage.Strict =>
listenerRef ! message
listenerRef ! Done
socketsConnected.remove(socketId)
case _ => // ignore other message types
}
val dummySource: Source[Message, Promise[Option[Nothing]]] = Source.maybe
Flow.fromSinkAndSourceCoupled(sink, dummySource)
}
}
Loading

0 comments on commit cc54846

Please sign in to comment.