Merge pull request 'LMS-27 Auth0 Integration' (#3) from FEAT-auth0-Integration into main
Reviewed-on: https://gitea.com/NickKalar/LMS-APIs/pulls/3
This commit is contained in:
@@ -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<String>) {
|
||||
@@ -20,6 +26,11 @@ fun main(args: Array<String>) {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
9
src/main/kotlin/model/User.kt
Normal file
9
src/main/kotlin/model/User.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package codes.kalar.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val name: String,
|
||||
val password: String,
|
||||
)
|
||||
@@ -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<NewCollectionItem>()
|
||||
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<NewCollectionItem>()
|
||||
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<CollectionItem>()
|
||||
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<CollectionItem>()
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
src/main/kotlin/routes/LoginRoutes.kt
Normal file
47
src/main/kotlin/routes/LoginRoutes.kt
Normal file
@@ -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<User>()
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user