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 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")
}

View File

@@ -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

View File

@@ -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<User>() }
}
}
}
install(AutoHeadResponse)
@@ -115,6 +129,7 @@ fun Application.module() {
install(Routing) {
article(get())
auth(get())
citizen(get())
constitution(get())
followArticle(get())

View File

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

View File

@@ -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()) }

View File

@@ -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

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
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