feature #9: Add routes for login

This commit is contained in:
2019-08-22 23:23:25 +02:00
parent 4e9f737e00
commit 5542eede27
9 changed files with 115 additions and 5 deletions

View File

@@ -5,6 +5,8 @@ val kotlin_version: String by project
val logback_version: String by project val logback_version: String by project
val koinVersion: String by project val koinVersion: String by project
val postgresjson_version: String by project val postgresjson_version: String by project
val jackson_version: String by project
val cucumber_version: String by project
plugins { plugins {
application application
@@ -39,11 +41,13 @@ dependencies {
implementation("io.ktor:ktor-auth:$ktor_version") implementation("io.ktor:ktor-auth:$ktor_version")
implementation("io.ktor:ktor-auth-jwt:$ktor_version") implementation("io.ktor:ktor-auth-jwt:$ktor_version")
implementation("io.ktor:ktor-gson:$ktor_version") implementation("io.ktor:ktor-gson:$ktor_version")
implementation("io.ktor:ktor-auth-jwt:$ktor_version")
implementation("org.koin:koin-ktor:$koinVersion") implementation("org.koin:koin-ktor:$koinVersion")
implementation("io.ktor:ktor-jackson:$ktor_version") implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.9") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.9") implementation("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version")
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
implementation("com.auth0:java-jwt:3.8.2")
implementation("fr.postgresjson:postgresjson:$postgresjson_version") implementation("fr.postgresjson:postgresjson:$postgresjson_version")
testImplementation("io.ktor:ktor-server-tests:$ktor_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version")
@@ -53,6 +57,6 @@ dependencies {
testImplementation("io.mockk:mockk:1.9") testImplementation("io.mockk:mockk:1.9")
testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") testImplementation("org.junit.jupiter:junit-jupiter:5.5.0")
testImplementation("org.amshove.kluent:kluent:1.4") testImplementation("org.amshove.kluent:kluent:1.4")
testImplementation("io.cucumber:cucumber-java8:4.3.1") testImplementation("io.cucumber:cucumber-java8:$cucumber_version")
testImplementation("io.cucumber:cucumber-junit:4.3.1") testImplementation("io.cucumber:cucumber-junit:$cucumber_version")
} }

View File

@@ -4,3 +4,5 @@ kotlin_version=1.3.40
logback_version=1.2.1 logback_version=1.2.1
postgresjson_version=0.1 postgresjson_version=0.1
koinVersion=2.0.1 koinVersion=2.0.1
jackson_version=2.9.9
cucumber_version=4.3.1

View File

@@ -9,11 +9,13 @@ import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.entity.Article import fr.dcproject.entity.Article
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.Constitution import fr.dcproject.entity.Constitution
import fr.dcproject.entity.User
import fr.dcproject.routes.* import fr.dcproject.routes.*
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.auth.jwt.jwt
import io.ktor.features.AutoHeadResponse import io.ktor.features.AutoHeadResponse
import io.ktor.features.CallLogging import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
@@ -93,6 +95,18 @@ fun Application.module() {
} }
install(Authentication) { 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<User>() }
}
}
} }
install(AutoHeadResponse) install(AutoHeadResponse)
@@ -115,6 +129,7 @@ fun Application.module() {
install(Routing) { install(Routing) {
article(get()) article(get())
auth(get())
citizen(get()) citizen(get())
constitution(get()) constitution(get())
followArticle(get()) followArticle(get())

View File

@@ -1,7 +1,12 @@
package fr.dcproject 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 com.typesafe.config.ConfigFactory
import fr.dcproject.entity.User
import java.io.File import java.io.File
import java.util.*
class Config { class Config {
private var config = ConfigFactory.load() private var config = ConfigFactory.load()
@@ -14,3 +19,33 @@ class Config {
var password: String = config.getString("db.password") var password: String = config.getString("db.password")
val port: Int = config.getInt("db.port") 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)
}

View File

@@ -10,6 +10,7 @@ import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository import fr.dcproject.repository.Constitution as ConstitutionRepository
import fr.dcproject.repository.FollowArticle as FollowArticleRepository import fr.dcproject.repository.FollowArticle as FollowArticleRepository
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
import fr.dcproject.repository.User as UserRepository
val config = Config() val config = Config()
@@ -26,6 +27,7 @@ val Module = module {
).createRequester() } ).createRequester() }
// TODO: create generic declaration // TODO: create generic declaration
single { UserRepository(get()) }
single { ArticleRepository(get()) } single { ArticleRepository(get()) }
single { CitizenRepository(get()) } single { CitizenRepository(get()) }
single { ConstitutionRepository(get()) } single { ConstitutionRepository(get()) }

View File

@@ -1,6 +1,7 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.* import fr.postgresjson.entity.*
import io.ktor.auth.Principal
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
@@ -11,4 +12,5 @@ class User(
var plainPassword: String? var plainPassword: String?
) : UuidEntity(id), ) : UuidEntity(id),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp() EntityUpdatedAt by EntityUpdatedAtImp(),
Principal

View File

@@ -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<UserEntity> {
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
)
}
}

View File

@@ -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 <Paths.LoginRequest> {
try {
val credentials = call.receive<UserPasswordCredential>()
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")
}
}
}

View File

@@ -7,6 +7,8 @@ import io.ktor.locations.Location
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object Paths { 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) { @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 page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit