From 8061c15b04c6d9801661af8a95a9c668349ce393 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sun, 26 Apr 2020 00:49:42 +0200 Subject: [PATCH] Add token renewal backend --- api/http/test.http | 16 +++++ api/src/auth/AuthenticationModule.kt | 4 +- api/src/auth/SimpleJWT.kt | 9 +-- .../extensions/ApplicationCallExtensions.kt | 12 ++-- api/src/features/ConfigurationFeature.kt | 8 ++- api/src/routing/AuthController.kt | 70 +++++++++++++++++++ api/src/routing/LoginController.kt | 46 ------------ api/src/routing/Routes.kt | 2 +- api/src/routing/UserController.kt | 1 - api/src/services/UserService.kt | 8 ++- 10 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 api/src/routing/AuthController.kt delete mode 100644 api/src/routing/LoginController.kt diff --git a/api/http/test.http b/api/http/test.http index 9a8f0f9..6675bf1 100644 --- a/api/http/test.http +++ b/api/http/test.http @@ -9,11 +9,27 @@ Content-Type: application/json > {% client.global.set("token", response.body.token); +client.global.set("refreshToken", response.body.refreshToken); client.test("Request executed successfully", function() { 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 http://localhost:8081/notes Authorization: Bearer {{token}} diff --git a/api/src/auth/AuthenticationModule.kt b/api/src/auth/AuthenticationModule.kt index 454ea22..30b7c7f 100644 --- a/api/src/auth/AuthenticationModule.kt +++ b/api/src/auth/AuthenticationModule.kt @@ -9,10 +9,10 @@ import org.kodein.di.generic.instance fun Application.authenticationModule() { install(Authentication) { jwt { - val simpleJwt by kodein.instance() + val simpleJwt by kodein.instance(tag = "auth") verifier(simpleJwt.verifier) validate { - UserIdPrincipal(it.payload.getClaim("name").asString()) + UserIdPrincipal(it.payload.getClaim("email").asString()) } } } diff --git a/api/src/auth/SimpleJWT.kt b/api/src/auth/SimpleJWT.kt index 173a5c1..0d7b0ad 100644 --- a/api/src/auth/SimpleJWT.kt +++ b/api/src/auth/SimpleJWT.kt @@ -4,14 +4,15 @@ import com.auth0.jwt.JWT import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm import java.util.* +import java.util.concurrent.TimeUnit -class SimpleJWT(secret: String) { - private val validityInMs = 36_000_00 * 1 +class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) { + private val validityInMs = TimeUnit.MILLISECONDS.convert(validity, unit) private val algorithm = Algorithm.HMAC256(secret) val verifier: JWTVerifier = JWT.require(algorithm).build() - fun sign(name: String): String = JWT.create() - .withClaim("name", name) + fun sign(email: String): String = JWT.create() + .withClaim("email", email) .withExpiresAt(getExpiration()) .sign(algorithm) diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index a8a3f3b..88a93dc 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -17,16 +17,18 @@ suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { respond(status, status.description) } +/** + * @return the user email for the currently authenticated user + */ +fun ApplicationCall.userEmail() = principal()!!.name + /** * @return the userId for the currently authenticated user */ -fun ApplicationCall.userId(): Int { - val email = principal()!!.name - return userService.getUserId(email)!! -} +fun ApplicationCall.userId() = userService.getUserId(userEmail())!! class NoteCreate(val title: String, val tags: List) suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive() -suspend fun ApplicationCall.receiveNotePatch() : FullNotePatchDTO = receive() \ No newline at end of file +suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive() \ No newline at end of file diff --git a/api/src/features/ConfigurationFeature.kt b/api/src/features/ConfigurationFeature.kt index 8632233..67aae98 100644 --- a/api/src/features/ConfigurationFeature.kt +++ b/api/src/features/ConfigurationFeature.kt @@ -7,6 +7,7 @@ import io.ktor.application.* import org.kodein.di.Kodein import org.kodein.di.generic.bind import org.kodein.di.generic.instance +import java.util.concurrent.TimeUnit import javax.sql.DataSource /** @@ -29,10 +30,13 @@ fun Application.configurationFeature() { 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") { bind() with instance(dataSource) - bind() with instance(simpleJwt) + bind(tag = "auth") with instance(authSimpleJwt) + bind(tag = "refresh") with instance(refreshSimpleJwt) } } \ No newline at end of file diff --git a/api/src/routing/AuthController.kt b/api/src/routing/AuthController.kt new file mode 100644 index 0000000..8945e60 --- /dev/null +++ b/api/src/routing/AuthController.kt @@ -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("auth") + val refreshSimpleJwt by kodein.instance("refresh") + val userService by kodein.instance() + + post("/user/login") { + val credential = call.receive() + + 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 + + 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()!!.name + val info = userService.getUserInfo(email) + if (info != null) call.respond(mapOf("user" to info)) + else call.respondStatus(HttpStatusCode.Unauthorized) + } + } + +} \ No newline at end of file diff --git a/api/src/routing/LoginController.kt b/api/src/routing/LoginController.kt deleted file mode 100644 index 78c7be2..0000000 --- a/api/src/routing/LoginController.kt +++ /dev/null @@ -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() - val userService by kodein.instance() - - data class TokenResponse(val token: String) - - post("/user/login") { - val credential = call.receive() - - 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()!!.name - val info = userService.getUserInfo(email) - if (info != null) call.respond(mapOf("user" to info)) - else call.respondStatus(HttpStatusCode.Unauthorized) - } - } - -} \ No newline at end of file diff --git a/api/src/routing/Routes.kt b/api/src/routing/Routes.kt index 4ed1e22..6844529 100644 --- a/api/src/routing/Routes.kt +++ b/api/src/routing/Routes.kt @@ -5,7 +5,7 @@ import org.kodein.di.Kodein fun Routing.registerRoutes(kodein: Kodein) { user(kodein) - login(kodein) + auth(kodein) notes(kodein) title(kodein) tags(kodein) diff --git a/api/src/routing/UserController.kt b/api/src/routing/UserController.kt index aeef679..f8f94e3 100644 --- a/api/src/routing/UserController.kt +++ b/api/src/routing/UserController.kt @@ -34,7 +34,6 @@ fun Routing.user(kodein: Kodein) { } authenticate { - put { val user = call.receive() diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index 75a6063..b0334fd 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -44,7 +44,13 @@ class UserService(override val kodein: Kodein) : KodeinAware { return db.from(Users) .select(Users.id) .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 }