From 5542eede277bef656b2bbbac91415b8c7ef58072 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Thu, 22 Aug 2019 23:23:25 +0200 Subject: [PATCH] feature #9: Add routes for login --- build.gradle.kts | 12 ++++--- gradle.properties | 2 ++ src/main/kotlin/fr/dcproject/Application.kt | 15 ++++++++ src/main/kotlin/fr/dcproject/Configuration.kt | 35 +++++++++++++++++++ src/main/kotlin/fr/dcproject/Module.kt | 2 ++ src/main/kotlin/fr/dcproject/entity/User.kt | 4 ++- .../kotlin/fr/dcproject/repository/User.kt | 19 ++++++++++ src/main/kotlin/fr/dcproject/routes/Auth.kt | 29 +++++++++++++++ src/main/kotlin/fr/dcproject/routes/Paths.kt | 2 ++ 9 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/fr/dcproject/repository/User.kt create mode 100644 src/main/kotlin/fr/dcproject/routes/Auth.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3a18def..c231a33 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,8 @@ val kotlin_version: String by project val logback_version: String by project val koinVersion: String by project val postgresjson_version: String by project +val jackson_version: String by project +val cucumber_version: String by project plugins { application @@ -39,11 +41,13 @@ dependencies { implementation("io.ktor:ktor-auth:$ktor_version") implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("io.ktor:ktor-gson:$ktor_version") + implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("org.koin:koin-ktor:$koinVersion") implementation("io.ktor:ktor-jackson:$ktor_version") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.9") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version") implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") + implementation("com.auth0:java-jwt:3.8.2") implementation("fr.postgresjson:postgresjson:$postgresjson_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version") @@ -53,6 +57,6 @@ dependencies { testImplementation("io.mockk:mockk:1.9") testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") testImplementation("org.amshove.kluent:kluent:1.4") - testImplementation("io.cucumber:cucumber-java8:4.3.1") - testImplementation("io.cucumber:cucumber-junit:4.3.1") + testImplementation("io.cucumber:cucumber-java8:$cucumber_version") + testImplementation("io.cucumber:cucumber-junit:$cucumber_version") } diff --git a/gradle.properties b/gradle.properties index cd137c7..448427d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,5 @@ kotlin_version=1.3.40 logback_version=1.2.1 postgresjson_version=0.1 koinVersion=2.0.1 +jackson_version=2.9.9 +cucumber_version=4.3.1 diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 5a5616d..26a95e4 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -9,11 +9,13 @@ import com.fasterxml.jackson.datatype.joda.JodaModule import fr.dcproject.entity.Article import fr.dcproject.entity.Citizen import fr.dcproject.entity.Constitution +import fr.dcproject.entity.User import fr.dcproject.routes.* import fr.postgresjson.migration.Migrations import io.ktor.application.Application import io.ktor.application.install import io.ktor.auth.Authentication +import io.ktor.auth.jwt.jwt import io.ktor.features.AutoHeadResponse import io.ktor.features.CallLogging import io.ktor.features.ContentNegotiation @@ -93,6 +95,18 @@ fun Application.module() { } install(Authentication) { + /** + * Setup the JWT authentication to be used in [Routing]. + * If the token is valid, the corresponding [User] is fetched from the database. + * The [User] can then be accessed in each [ApplicationCall]. + */ + jwt { + verifier(JwtConfig.verifier) + realm = "dc-project.fr" + validate { + it.payload.getClaim("id").asInt()?.let { get() } + } + } } install(AutoHeadResponse) @@ -115,6 +129,7 @@ fun Application.module() { install(Routing) { article(get()) + auth(get()) citizen(get()) constitution(get()) followArticle(get()) diff --git a/src/main/kotlin/fr/dcproject/Configuration.kt b/src/main/kotlin/fr/dcproject/Configuration.kt index 2e6faaa..388ac03 100644 --- a/src/main/kotlin/fr/dcproject/Configuration.kt +++ b/src/main/kotlin/fr/dcproject/Configuration.kt @@ -1,7 +1,12 @@ package fr.dcproject +import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm import com.typesafe.config.ConfigFactory +import fr.dcproject.entity.User import java.io.File +import java.util.* class Config { private var config = ConfigFactory.load() @@ -14,3 +19,33 @@ class Config { var password: String = config.getString("db.password") val port: Int = config.getInt("db.port") } + +object JwtConfig { + + private const val secret = "zAP5MBA4B4Ijz0MZaS48" + private const val issuer = "dc-project.fr" + private const val validityInMs = 36_000_00 * 10 // 10 hours + // TODO change to RSA512 + private val algorithm = Algorithm.HMAC512(secret) + + val verifier: JWTVerifier = JWT + .require(algorithm) + .withIssuer(issuer) + .build() + + /** + * Produce a token for this combination of User and Account + */ + fun makeToken(user: User): String = JWT.create() + .withSubject("Authentication") + .withIssuer(issuer) + .withClaim("id", user.id.toString()) + .withExpiresAt(getExpiration()) + .sign(algorithm) + + /** + * Calculate the expiration Date based on current time + the given validity + */ + private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs) + +} \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/Module.kt b/src/main/kotlin/fr/dcproject/Module.kt index 02dae72..1dd834a 100644 --- a/src/main/kotlin/fr/dcproject/Module.kt +++ b/src/main/kotlin/fr/dcproject/Module.kt @@ -10,6 +10,7 @@ import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Constitution as ConstitutionRepository import fr.dcproject.repository.FollowArticle as FollowArticleRepository import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository +import fr.dcproject.repository.User as UserRepository val config = Config() @@ -26,6 +27,7 @@ val Module = module { ).createRequester() } // TODO: create generic declaration + single { UserRepository(get()) } single { ArticleRepository(get()) } single { CitizenRepository(get()) } single { ConstitutionRepository(get()) } diff --git a/src/main/kotlin/fr/dcproject/entity/User.kt b/src/main/kotlin/fr/dcproject/entity/User.kt index b622f9c..f0f9a5b 100644 --- a/src/main/kotlin/fr/dcproject/entity/User.kt +++ b/src/main/kotlin/fr/dcproject/entity/User.kt @@ -1,6 +1,7 @@ package fr.dcproject.entity import fr.postgresjson.entity.* +import io.ktor.auth.Principal import org.joda.time.DateTime import java.util.* @@ -11,4 +12,5 @@ class User( var plainPassword: String? ) : UuidEntity(id), EntityCreatedAt by EntityCreatedAtImp(), - EntityUpdatedAt by EntityUpdatedAtImp() + EntityUpdatedAt by EntityUpdatedAtImp(), + Principal diff --git a/src/main/kotlin/fr/dcproject/repository/User.kt b/src/main/kotlin/fr/dcproject/repository/User.kt new file mode 100644 index 0000000..255d660 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/repository/User.kt @@ -0,0 +1,19 @@ +package fr.dcproject.repository + +import fr.postgresjson.connexion.Requester +import fr.postgresjson.repository.RepositoryI +import io.ktor.auth.UserPasswordCredential +import fr.dcproject.entity.User as UserEntity + +class User(override var requester: Requester) : RepositoryI { + override val entityName = UserEntity::class + + fun findByCredentials(credentials: UserPasswordCredential): UserEntity? { + return requester + .getFunction("check_user") + .selectOne( + "username" to credentials.name, + "plain_password" to credentials.password + ) + } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Auth.kt b/src/main/kotlin/fr/dcproject/routes/Auth.kt new file mode 100644 index 0000000..3dacd84 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/routes/Auth.kt @@ -0,0 +1,29 @@ +package fr.dcproject.routes + +import Paths +import com.fasterxml.jackson.databind.exc.MismatchedInputException +import fr.dcproject.JwtConfig +import io.ktor.application.call +import io.ktor.auth.UserPasswordCredential +import io.ktor.features.BadRequestException +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.post +import io.ktor.request.receive +import io.ktor.response.respondText +import io.ktor.routing.Route +import io.ktor.util.KtorExperimentalAPI +import fr.dcproject.repository.User as UserRepository + +@KtorExperimentalLocationsAPI +@KtorExperimentalAPI +fun Route.auth(repo: UserRepository) { + post { + try { + val credentials = call.receive() + val user = repo.findByCredentials(credentials) ?: throw BadRequestException("Username not exist or password is wrong") + call.respondText(JwtConfig.makeToken(user)) + } catch (e: MismatchedInputException) { + throw BadRequestException("You must be send name and password to the request") + } + } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Paths.kt b/src/main/kotlin/fr/dcproject/routes/Paths.kt index f4bf6b4..316a008 100644 --- a/src/main/kotlin/fr/dcproject/routes/Paths.kt +++ b/src/main/kotlin/fr/dcproject/routes/Paths.kt @@ -7,6 +7,8 @@ import io.ktor.locations.Location @KtorExperimentalLocationsAPI object Paths { + @Location("/login") class LoginRequest + @Location("/articles") class ArticlesRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: Direction? = null, val search: String? = null) { val page: Int = if (page < 1) 1 else page val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit