From 8ab2b8c6f17eca921cbdccdf69c22aa0c83f2269 Mon Sep 17 00:00:00 2001 From: Marco Fracassi Date: Mon, 30 Dec 2019 12:14:41 +0100 Subject: [PATCH] Add basic authentication --- README.md | 4 +- build.gradle | 2 +- src/main/kotlin/daikon/BasicAuthentication.kt | 53 ++++++++++++++++ src/main/kotlin/daikon/HttpServer.kt | 12 ++++ .../daikon/BasicAuthenticationHttpTest.kt | 61 +++++++++++++++++++ 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/daikon/BasicAuthentication.kt create mode 100644 src/test/kotlin/daikon/BasicAuthenticationHttpTest.kt diff --git a/README.md b/README.md index 01e63f8..e29c3f3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ repositories { ``` - Add the dependency ``` -implementation 'com.github.DaikonWeb:daikon:0.6.3' +implementation 'com.github.DaikonWeb:daikon:0.6.4' ``` ### Maven @@ -40,7 +40,7 @@ implementation 'com.github.DaikonWeb:daikon:0.6.3' com.github.DaikonWeb daikon - 0.6.3 + 0.6.4 ``` diff --git a/build.gradle b/build.gradle index 4c28c2a..a24e38a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { } group = 'com.github.DaikonWeb' -version '0.6.3' +version '0.6.4' repositories { mavenCentral() diff --git a/src/main/kotlin/daikon/BasicAuthentication.kt b/src/main/kotlin/daikon/BasicAuthentication.kt new file mode 100644 index 0000000..fb218f0 --- /dev/null +++ b/src/main/kotlin/daikon/BasicAuthentication.kt @@ -0,0 +1,53 @@ +package daikon + +import daikon.RequestFlow.halt +import org.eclipse.jetty.http.HttpHeader.AUTHORIZATION +import org.eclipse.jetty.http.HttpHeader.WWW_AUTHENTICATE +import java.nio.charset.StandardCharsets.UTF_8 +import java.util.* +import javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED + +class BasicAuthentication { + + private val credentials = mutableListOf() + + fun addUser(username: String, password: String) { + credentials.add(Credential(username, password)) + } + + fun validate(req: Request, res: Response, realm: String) { + try { + val credential = credential(req.header(AUTHORIZATION.asString())) + + if (isForbidden(credential)) { + unauthorized(res, realm) + } + } catch (t: Throwable) { + unauthorized(res, realm) + } + + } + + private fun unauthorized(res: Response, realm: String) { + res.header( + WWW_AUTHENTICATE.asString(), + """Basic realm="$realm", charset="UTF-8"""" + ) + halt(res, SC_UNAUTHORIZED) + } + + private fun isForbidden(credential: Credential): Boolean { + return credentials.none { it == credential } + } + + private fun credential(header: String): Credential { + val credentials = String( + Base64.getDecoder().decode(header.replace("Basic ", "", true)), + UTF_8 + ).split(":") + + return Credential(credentials[0], credentials[1]) + } +} + +data class Credential(val username: String, val password: String) diff --git a/src/main/kotlin/daikon/HttpServer.kt b/src/main/kotlin/daikon/HttpServer.kt index 391fc4e..fcaa2c5 100644 --- a/src/main/kotlin/daikon/HttpServer.kt +++ b/src/main/kotlin/daikon/HttpServer.kt @@ -10,6 +10,7 @@ import org.eclipse.jetty.util.resource.Resource import java.time.LocalDateTime.now import java.time.temporal.ChronoUnit.MILLIS + class HttpServer(private val port: Int = 4545, initializeActions: HttpServer.() -> Unit = {}) : AutoCloseable { private val routes = Routing() @@ -19,6 +20,7 @@ class HttpServer(private val port: Int = 4545, initializeActions: HttpServer.() private val beforeStopActions = mutableListOf<(Context) -> Unit>() private val basePath = mutableListOf("") private val context= ServerContext(port) + private val basicAuth = BasicAuthentication() init { initializeActions() @@ -149,6 +151,16 @@ class HttpServer(private val port: Int = 4545, initializeActions: HttpServer.() return this } + fun basicAuthUser(username: String, password: String): HttpServer { + basicAuth.addUser(username, password) + return this + } + + fun basicAuth(path: String, realm: String = "default"): HttpServer { + before(path) { req, res -> basicAuth.validate(req, res, realm) } + return this + } + private fun add(method: Method, path: String, action: RouteAction) { routes.add(Route(method, joinPaths(path), action)) } diff --git a/src/test/kotlin/daikon/BasicAuthenticationHttpTest.kt b/src/test/kotlin/daikon/BasicAuthenticationHttpTest.kt new file mode 100644 index 0000000..a191bde --- /dev/null +++ b/src/test/kotlin/daikon/BasicAuthenticationHttpTest.kt @@ -0,0 +1,61 @@ +package daikon + +import daikon.Localhost.get +import khttp.structures.authorization.BasicAuthorization +import org.assertj.core.api.Assertions.assertThat +import org.eclipse.jetty.http.HttpStatus.OK_200 +import org.eclipse.jetty.http.HttpStatus.UNAUTHORIZED_401 +import org.junit.jupiter.api.Test + +class BasicAuthenticationHttpTest { + + @Test + fun routing() { + HttpServer() + .get("/") { _, res -> res.status(OK_200)} + .basicAuth("/foo", "realm") + .basicAuth("/bar*") + .basicAuth("/baz/:name") + .start().use { + assertThat(get("/").statusCode).isEqualTo(OK_200) + assertThat(get("/foo").statusCode).isEqualTo(UNAUTHORIZED_401) + assertThat(get("/bar/baz").statusCode).isEqualTo(UNAUTHORIZED_401) + assertThat(get("/baz/alex").statusCode).isEqualTo(UNAUTHORIZED_401) + } + } + + @Test + fun `authenticate user`() { + HttpServer() + .basicAuthUser("Marco", "secret") + .basicAuth("/") + .get("/") { _, res -> res.status(OK_200)} + .start().use { + assertThat(get("/").statusCode).isEqualTo(UNAUTHORIZED_401) + assertThat(get("/", auth = BasicAuthorization("Marco", "secret")).statusCode).isEqualTo(OK_200) + } + } + + @Test + fun `wrong credential`() { + HttpServer() + .basicAuthUser("Marco", "secret") + .basicAuth("/") + .get("/") { _, res -> res.status(OK_200)} + .start().use { + assertThat(get("/", auth = BasicAuthorization("Marco", "wrong")).statusCode).isEqualTo(UNAUTHORIZED_401) + assertThat(get("/", auth = BasicAuthorization("wrong", "secret")).statusCode).isEqualTo(UNAUTHORIZED_401) + } + } + + @Test + fun `supports utf-8`() { + HttpServer() + .basicAuthUser("ìù", "èéàò") + .basicAuth("/") + .get("/") { _, res -> res.status(OK_200)} + .start().use { + assertThat(get("/", auth = BasicAuthorization("ìù", "èéàò")).statusCode).isEqualTo(OK_200) + } + } +} \ No newline at end of file