diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt index 43d91bb..6538438 100644 --- a/src/main/kotlin/Application.kt +++ b/src/main/kotlin/Application.kt @@ -1,12 +1,18 @@ package codes.kalar +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.jwt.JWTPrincipal +import io.ktor.server.auth.jwt.jwt import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.response.respond import kotlinx.serialization.json.Json fun main(args: Array) { @@ -20,6 +26,11 @@ fun main(args: Array) { } fun Application.module() { + val secret = environment.config.property("jwt.secret").getString() + val issuer = environment.config.property("jwt.issuer").getString() + val audience = environment.config.property("jwt.audience").getString() + val myRealm = environment.config.property("jwt.realm").getString() + install(ContentNegotiation) { json(Json { prettyPrint = true @@ -27,6 +38,28 @@ fun Application.module() { }) } + install(Authentication) { + jwt("auth-jwt") { + realm = myRealm + verifier( + JWT + .require(Algorithm.HMAC256(secret)) + .withAudience(audience) + .withIssuer(issuer) + .build()) + validate { credential -> + if (credential.payload.getClaim("name").asString() != "") { + JWTPrincipal(credential.payload) + } else { + null + } + } + challenge { defaultScheme, realm -> + call.respond(HttpStatusCode.Unauthorized, "${defaultScheme}, $realm Token is not valid or has expired") + } + } + } + configureHTTP() configureSecurity() configureSerialization() diff --git a/src/main/kotlin/Routing.kt b/src/main/kotlin/Routing.kt index 82536d6..b86683e 100644 --- a/src/main/kotlin/Routing.kt +++ b/src/main/kotlin/Routing.kt @@ -2,6 +2,7 @@ package codes.kalar import codes.kalar.routes.configureCollectionItemRoutes import codes.kalar.routes.configureLibraryRoutes +import codes.kalar.routes.configureLoginRoutes import codes.kalar.routes.configurePatronRoutes import codes.kalar.routes.configureStaffRoutes import io.ktor.http.* @@ -14,6 +15,7 @@ import java.sql.Connection fun Application.configureRouting() { val dbConnection: Connection = connectToPostgres() + configureLoginRoutes(dbConnection) configureCollectionItemRoutes(dbConnection) configurePatronRoutes(dbConnection) configureLibraryRoutes(dbConnection) @@ -24,12 +26,8 @@ fun Application.configureRouting() { call.respondText("Hello World!", status = HttpStatusCode.OK) } - get("/authenticate") { - call.respondText(System.getenv("JWT_DOMAIN") ?: "You seem to be missing something") - } - swaggerUI(path = "swagger", swaggerFile = "src/main/resources/openapi/documentation.yaml") { - version="5.27.1" + version = "5.27.1" } } } diff --git a/src/main/kotlin/Security.kt b/src/main/kotlin/Security.kt index 636f74c..049dd68 100644 --- a/src/main/kotlin/Security.kt +++ b/src/main/kotlin/Security.kt @@ -1,28 +1,16 @@ package codes.kalar -import java.io.File -import java.util.Properties import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.defaultheaders.* -import io.ktor.server.plugins.swagger.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import java.sql.Connection -import java.sql.DriverManager fun Application.configureSecurity() { // Please read the jwt property from the config file if you are using EngineMain val jwtAudience = environment.config.property("jwt.audience").toString() val jwtDomain = environment.config.property("jwt.domain").toString() - val jwtRealm = "ktor sample app" + val jwtRealm = environment.config.property("jwt.realm").toString() val jwtSecret = environment.config.property("jwt.secret").toString() authentication { diff --git a/src/main/kotlin/model/User.kt b/src/main/kotlin/model/User.kt new file mode 100644 index 0000000..9d75b05 --- /dev/null +++ b/src/main/kotlin/model/User.kt @@ -0,0 +1,9 @@ +package codes.kalar.model + +import kotlinx.serialization.Serializable + +@Serializable +data class User( + val name: String, + val password: String, +) \ No newline at end of file diff --git a/src/main/kotlin/routes/CollectionItemRoutes.kt b/src/main/kotlin/routes/CollectionItemRoutes.kt index bb6b9c7..4fb321e 100644 --- a/src/main/kotlin/routes/CollectionItemRoutes.kt +++ b/src/main/kotlin/routes/CollectionItemRoutes.kt @@ -7,6 +7,7 @@ import codes.kalar.exception.DbElementNotFoundException import codes.kalar.model.NewCollectionItem import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.auth.authenticate import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -41,49 +42,51 @@ fun Application.configureCollectionItemRoutes(dbConnection: Connection) { } } - post("/items") { - try { - val item = call.receive() - val id = itemService.create(item) - call.respondText("Adding ${item.title} to database with the id of $id", status=HttpStatusCode.OK) - } catch (cause: DbElementInsertionException) { - call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") - } catch (cause: ContentTransformationException) { - call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.") + authenticate("auth-jwt") { + post("/items") { + try { + val item = call.receive() + val id = itemService.create(item) + call.respondText("Adding ${item.title} to database with the id of $id", status = HttpStatusCode.OK) + } catch (cause: DbElementInsertionException) { + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") + } catch (cause: ContentTransformationException) { + call.respond(HttpStatusCode.BadRequest, "Bad Arguments. Must pass a valid CollectionItem object.") + } + } - } - - patch("/items") { - try { - val inputItem = call.receive() - itemService.readById(inputItem.id) - itemService.update(inputItem) - call.respondText("Updated ${inputItem.title} to database.", status=HttpStatusCode.OK) - } catch (cause: DbElementNotFoundException) { - log.error(cause.message) - call.respond(HttpStatusCode.NotFound, cause.message ?: "Could not find item in database.") - } catch (cause: DbElementInsertionException) { - log.error(cause.message) - call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") - } catch (cause: ContentTransformationException) { - log.error(cause.message) - call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") + patch("/items") { + try { + val inputItem = call.receive() + itemService.readById(inputItem.id) + itemService.update(inputItem) + call.respondText("Updated ${inputItem.title} to database.", status = HttpStatusCode.OK) + } catch (cause: DbElementNotFoundException) { + log.error(cause.message) + call.respond(HttpStatusCode.NotFound, cause.message ?: "Could not find item in database.") + } catch (cause: DbElementInsertionException) { + log.error(cause.message) + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") + } catch (cause: ContentTransformationException) { + log.error(cause.message) + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") + } } - } - delete("/items/{id}") { - try { - val id = call.parameters["id"]!!.toLong() - log.info("Deleting item with id=$id") - itemService.delete(id) - call.respondText(":(", status = HttpStatusCode.OK) - } catch (cause: DbElementNotFoundException) { - log.error(cause.message, cause) - call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") - } catch (cause: NumberFormatException) { - log.error(cause.message, cause) - call.respond(HttpStatusCode.BadRequest, cause.message ?: "Invalid ID format") + delete("/items/{id}") { + try { + val id = call.parameters["id"]!!.toLong() + log.info("Deleting item with id=$id") + itemService.delete(id) + call.respondText(":(", status = HttpStatusCode.OK) + } catch (cause: DbElementNotFoundException) { + log.error(cause.message, cause) + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Bad Arguments") + } catch (cause: NumberFormatException) { + log.error(cause.message, cause) + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Invalid ID format") + } } } } diff --git a/src/main/kotlin/routes/LoginRoutes.kt b/src/main/kotlin/routes/LoginRoutes.kt new file mode 100644 index 0000000..f4e58f5 --- /dev/null +++ b/src/main/kotlin/routes/LoginRoutes.kt @@ -0,0 +1,47 @@ +package codes.kalar.routes + +import codes.kalar.exception.DbElementNotFoundException +import codes.kalar.model.User +import codes.kalar.service.PatronService +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.* +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import java.sql.Connection +import java.util.Date + +fun Application.configureLoginRoutes(dbConnection: Connection) { + val secret = environment.config.property("jwt.secret").getString() + val issuer = environment.config.property("jwt.issuer").getString() + val audience = environment.config.property("jwt.audience").getString() + + val patronService = PatronService(dbConnection) + routing { + post("/login") { + try { + val user = call.receive() + val name = user.name + val password = user.password + + if (patronService.loginPatronByLoginUsername(name, password)) { + val token = JWT.create() + .withAudience(audience) + .withIssuer(issuer) + .withClaim("name", name) + .withExpiresAt(Date(System.currentTimeMillis() + 160000)) + .sign(Algorithm.HMAC256(secret)) + call.respond(hashMapOf("token" to token)) + } else { + call.respond(HttpStatusCode.Unauthorized, "Invalid login") + } + } catch (cause: DbElementNotFoundException) { + call.respond(HttpStatusCode.BadRequest, cause.message ?: "Something went wrong") + } + + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/service/PatronService.kt b/src/main/kotlin/service/PatronService.kt index 3ad3c60..95bc5fa 100644 --- a/src/main/kotlin/service/PatronService.kt +++ b/src/main/kotlin/service/PatronService.kt @@ -21,7 +21,7 @@ class PatronService(private val connection: Connection) { private const val ARCHIVE_PATRON_BY_ID = "UPDATE patron set is_archived = true WHERE id = ?" } - suspend fun create(newPatron: NewPatron): Long { + fun create(newPatron: NewPatron): Long { val statement = connection.prepareStatement(INSERT_PATRON, Statement.RETURN_GENERATED_KEYS) statement.setString(1, newPatron.name) statement.setBoolean(2, true) @@ -42,7 +42,7 @@ class PatronService(private val connection: Connection) { } } - suspend fun readPatronById(id: Long): Patron { + fun readPatronById(id: Long): Patron { try { val statement = connection.prepareStatement(SELECT_PATRON_BY_ID, Statement.RETURN_GENERATED_KEYS) statement.setLong(1, id) @@ -58,7 +58,7 @@ class PatronService(private val connection: Connection) { } } - suspend fun readPatronByLoginUsername(username: String): Patron { + fun readPatronByLoginUsername(username: String): Patron { try { val statement = connection.prepareStatement(SELECT_PATRON_BY_LOGIN_USERNAME) statement.setString(1, username) @@ -74,13 +74,13 @@ class PatronService(private val connection: Connection) { } } - suspend fun loginPatronByLoginUsername(username: String, password: String): Boolean { + fun loginPatronByLoginUsername(username: String, password: String): Boolean { try { val statement = connection.prepareStatement(SELECT_PATRON_BY_LOGIN_USERNAME) statement.setString(1, username) val resultSet = statement.executeQuery() return if (resultSet.next()) { - resultSet.getString("password") == password + resultSet.getString("password") == password && !resultSet.getBoolean("is_archived") } else { throw SQLException() } @@ -89,7 +89,7 @@ class PatronService(private val connection: Connection) { } } - suspend fun update(patron: Patron): Boolean { + fun update(patron: Patron): Boolean { val statement = connection.prepareStatement(UPDATE_PATRON_BY_ID) statement.setString(1, patron.name) statement.setBoolean(2, patron.hasGoodStanding) @@ -106,7 +106,7 @@ class PatronService(private val connection: Connection) { } } - suspend fun delete(id: Long) { + fun delete(id: Long) { val statement = connection.prepareStatement(ARCHIVE_PATRON_BY_ID) statement.setLong(1, id) try {