Merge branch 'feature/token-renewal'

This commit is contained in:
Hubert Van De Walle 2020-04-26 02:10:34 +02:00
commit 5799949e00
10 changed files with 114 additions and 62 deletions

View File

@ -9,11 +9,27 @@ Content-Type: application/json
> {% > {%
client.global.set("token", response.body.token); client.global.set("token", response.body.token);
client.global.set("refreshToken", response.body.refreshToken);
client.test("Request executed successfully", function() { client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200"); client.assert(response.status === 200, "Response status is not 200");
}); });
%} %}
### Refresh token
POST http://localhost:8081/user/refresh_token
Content-Type: application/json
{
"refreshToken": "{{refreshToken}}"
}
> {%
client.test("Request executed successfully", function() {
client.global.set("token", response.body.token);
client.assert(response.status === 200, "Response status is not 200");
});
%}
### Get notes ### Get notes
GET http://localhost:8081/notes GET http://localhost:8081/notes
Authorization: Bearer {{token}} Authorization: Bearer {{token}}

View File

@ -9,10 +9,10 @@ import org.kodein.di.generic.instance
fun Application.authenticationModule() { fun Application.authenticationModule() {
install(Authentication) { install(Authentication) {
jwt { jwt {
val simpleJwt by kodein.instance<SimpleJWT>() val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
verifier(simpleJwt.verifier) verifier(simpleJwt.verifier)
validate { validate {
UserIdPrincipal(it.payload.getClaim("name").asString()) UserIdPrincipal(it.payload.getClaim("email").asString())
} }
} }
} }

View File

@ -4,14 +4,15 @@ import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
class SimpleJWT(secret: String) { class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
private val validityInMs = 36_000_00 * 1 private val validityInMs = TimeUnit.MILLISECONDS.convert(validity, unit)
private val algorithm = Algorithm.HMAC256(secret) private val algorithm = Algorithm.HMAC256(secret)
val verifier: JWTVerifier = JWT.require(algorithm).build() val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(name: String): String = JWT.create() fun sign(email: String): String = JWT.create()
.withClaim("name", name) .withClaim("email", email)
.withExpiresAt(getExpiration()) .withExpiresAt(getExpiration())
.sign(algorithm) .sign(algorithm)

View File

@ -17,16 +17,18 @@ suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, status.description) respond(status, status.description)
} }
/**
* @return the user email for the currently authenticated user
*/
fun ApplicationCall.userEmail() = principal<UserIdPrincipal>()!!.name
/** /**
* @return the userId for the currently authenticated user * @return the userId for the currently authenticated user
*/ */
fun ApplicationCall.userId(): Int { fun ApplicationCall.userId() = userService.getUserId(userEmail())!!
val email = principal<UserIdPrincipal>()!!.name
return userService.getUserId(email)!!
}
class NoteCreate(val title: String, val tags: List<String>) class NoteCreate(val title: String, val tags: List<String>)
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive() suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
suspend fun ApplicationCall.receiveNotePatch() : FullNotePatchDTO = receive() suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive()

View File

@ -7,6 +7,7 @@ import io.ktor.application.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.bind import org.kodein.di.generic.bind
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import java.util.concurrent.TimeUnit
import javax.sql.DataSource import javax.sql.DataSource
/** /**
@ -29,10 +30,13 @@ fun Application.configurationFeature() {
HikariDataSource(hikariConfig) HikariDataSource(hikariConfig)
} }
val simpleJwt = SimpleJWT(environment.config.property("jwt.secret").getString()) val jwtSecret = environment.config.property("jwt.secret").getString()
val authSimpleJwt = SimpleJWT(jwtSecret, 1, TimeUnit.HOURS)
val refreshSimpleJwt = SimpleJWT(jwtSecret, 7, TimeUnit.DAYS)
configurationModule = Kodein.Module("Configuration") { configurationModule = Kodein.Module("Configuration") {
bind<DataSource>() with instance(dataSource) bind<DataSource>() with instance(dataSource)
bind<SimpleJWT>() with instance(simpleJwt) bind<SimpleJWT>(tag = "auth") with instance(authSimpleJwt)
bind<SimpleJWT>(tag = "refresh") with instance(refreshSimpleJwt)
} }
} }

View File

@ -0,0 +1,70 @@
package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.services.UserService
import com.auth0.jwt.exceptions.JWTVerificationException
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
data class RefreshToken(val refreshToken: String)
data class DualToken(val token: String, val refreshToken: String)
fun Routing.auth(kodein: Kodein) {
val authSimpleJwt by kodein.instance<SimpleJWT>("auth")
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
val userService by kodein.instance<UserService>()
post("/user/login") {
val credential = call.receive<UsernamePasswordCredential>()
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
if (!BCrypt.checkpw(credential.password, password)) {
return@post call.respondStatus(HttpStatusCode.Unauthorized)
}
val response = DualToken(
token = authSimpleJwt.sign(email),
refreshToken = refreshSimpleJwt.sign(email)
)
return@post call.respond(response)
}
post("/user/refresh_token") {
val token = call.receive<RefreshToken>().refreshToken
val email = try {
val decodedJWT = refreshSimpleJwt.verifier.verify(token)
decodedJWT.getClaim("email").asString()
} catch (e: JWTVerificationException) {
return@post call.respondStatus(HttpStatusCode.Unauthorized)
}
val response = DualToken(
token = authSimpleJwt.sign(email),
refreshToken = refreshSimpleJwt.sign(email)
)
return@post call.respond(response)
}
authenticate {
get("/user/me") {
// retrieve email from token
val email = call.principal<UserIdPrincipal>()!!.name
val info = userService.getUserInfo(email)
if (info != null) call.respond(mapOf("user" to info))
else call.respondStatus(HttpStatusCode.Unauthorized)
}
}
}

View File

@ -1,46 +0,0 @@
package be.vandewalleh.routing
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.services.UserService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
fun Routing.login(kodein: Kodein) {
val simpleJwt by kodein.instance<SimpleJWT>()
val userService by kodein.instance<UserService>()
data class TokenResponse(val token: String)
post("/user/login") {
val credential = call.receive<UsernamePasswordCredential>()
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
?: return@post call.respond(HttpStatusCode.Unauthorized)
if (!BCrypt.checkpw(credential.password, password)) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
return@post call.respond(TokenResponse(simpleJwt.sign(email)))
}
authenticate {
get("/user/me") {
// retrieve email from token
val email = call.principal<UserIdPrincipal>()!!.name
val info = userService.getUserInfo(email)
if (info != null) call.respond(mapOf("user" to info))
else call.respondStatus(HttpStatusCode.Unauthorized)
}
}
}

View File

@ -5,7 +5,7 @@ import org.kodein.di.Kodein
fun Routing.registerRoutes(kodein: Kodein) { fun Routing.registerRoutes(kodein: Kodein) {
user(kodein) user(kodein)
login(kodein) auth(kodein)
notes(kodein) notes(kodein)
title(kodein) title(kodein)
tags(kodein) tags(kodein)

View File

@ -34,7 +34,6 @@ fun Routing.user(kodein: Kodein) {
} }
authenticate { authenticate {
put { put {
val user = call.receive<UserDto>() val user = call.receive<UserDto>()

View File

@ -44,7 +44,13 @@ class UserService(override val kodein: Kodein) : KodeinAware {
return db.from(Users) return db.from(Users)
.select(Users.id) .select(Users.id)
.where { (Users.username eq username) or (Users.email eq email) } .where { (Users.username eq username) or (Users.email eq email) }
.limit(0, 1) .firstOrNull() != null
}
fun userExists(userId: Int): Boolean {
return db.from(Users)
.select(Users.id)
.where { Users.id eq userId }
.firstOrNull() != null .firstOrNull() != null
} }