Skip to content

Commit

Permalink
Merge pull request #18 from blogify-dev/dev
Browse files Browse the repository at this point in the history
Merge dev into master for PRX3
  • Loading branch information
ranile authored Oct 19, 2019
2 parents d270516 + 3d7417f commit 2737f15
Show file tree
Hide file tree
Showing 109 changed files with 3,004 additions and 1,075 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ FROM openjdk:10-jre

WORKDIR /var/server/

ADD build/dist/jar/blogify-alpha-0.0.1-all.jar .
ADD build/dist/jar/blogify-PRX2-all.jar .

EXPOSE 8080
EXPOSE 5005

CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "blogify-alpha-0.0.1-all.jar"]
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "blogify-PRX2-all.jar"]
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ plugins {
}

group = "blogify"
version = "alpha-0.0.1"
version = "PRX2"

application {
mainClassName = "io.ktor.server.netty.EngineMain"
Expand Down Expand Up @@ -69,6 +69,12 @@ dependencies {
compile("com.github.kittinunf.result:result:2.2.0")
compile("com.github.kittinunf.result:result-coroutines:2.2.0")

// JJWT

compile("io.jsonwebtoken:jjwt-api:0.10.7")
runtime("io.jsonwebtoken:jjwt-impl:0.10.7")
runtime("io.jsonwebtoken:jjwt-jackson:0.10.7")

}

kotlin.sourceSets["main"].kotlin.srcDirs("src")
Expand Down
3 changes: 2 additions & 1 deletion resources/logback.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{12} - %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{12} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
</configuration>
75 changes: 63 additions & 12 deletions src/blogify/backend/Application.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
package blogify.backend

import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation
import io.ktor.jackson.jackson
import io.ktor.routing.route
import io.ktor.routing.routing
import com.fasterxml.jackson.databind.module.SimpleModule

import com.andreapivetta.kolor.cyan

Expand All @@ -20,18 +14,35 @@ import blogify.backend.database.Comments
import blogify.backend.database.Users
import blogify.backend.routes.auth
import blogify.backend.database.handling.query
import blogify.backend.resources.models.Resource
import blogify.backend.util.SinglePageApplication

import io.ktor.application.call
import io.ktor.features.Compression
import io.ktor.features.GzipEncoder
import io.ktor.response.respondRedirect
import io.ktor.routing.get
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.CachingHeaders
import io.ktor.features.CallLogging
import io.ktor.features.ContentNegotiation
import io.ktor.features.DefaultHeaders
import io.ktor.http.CacheControl
import io.ktor.http.ContentType
import io.ktor.http.content.CachingOptions
import io.ktor.http.content.OutgoingContent
import io.ktor.jackson.jackson
import io.ktor.routing.route
import io.ktor.routing.routing

import org.jetbrains.exposed.sql.SchemaUtils

import kotlinx.coroutines.runBlocking

import org.slf4j.event.Level

const val version = "PRX2"
const val version = "PRX3"

const val asciiLogo = """
__ __ _ ____
Expand All @@ -58,18 +69,57 @@ fun Application.mainModule(@Suppress("UNUSED_PARAMETER") testing: Boolean = fals
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)

// Register a serializer for Resource.
// This will only affect pure Resource objects, so elements produced by the slicer are not affected,
// since those don't use Jackson for root serialization.

val resourceModule = SimpleModule()
resourceModule.addSerializer(Resource.ResourceIdSerializer)

registerModule(resourceModule)
}
}

// Intitialize call logging
// Initialize call logging

install(CallLogging) {
level = Level.TRACE
}

// Redirect every unknown route to SPA

install(SinglePageApplication) {
folderPath = "/frontend"
}

// Compression

install(Compression) {
encoder("gzip0", GzipEncoder)
}

// Default headers

install(DefaultHeaders) {
header("Server", "blogify-core PRX3")
header("X-Powered-By", "Ktor 1.2.3")
}

// Caching headers

install(CachingHeaders) {
options {
when (it.contentType?.withoutParameters()) {
ContentType.Text.JavaScript ->
CachingOptions(CacheControl.MaxAge(30 * 60))
ContentType.Application.Json ->
CachingOptions(CacheControl.MaxAge(60))
else -> null
}
}
}

// Initialize database

Database.init()
Expand All @@ -79,25 +129,26 @@ fun Application.mainModule(@Suppress("UNUSED_PARAMETER") testing: Boolean = fals
runBlocking { query {
SchemaUtils.create (
Articles,
Articles.Content,
Articles.Categories,
Users,
Comments,
Users.UserInfo
Comments
)
}}

// Initialize routes

routing {

route("/api") {
articles()
users()
auth()
}

get("/") {
call.respondRedirect("/home")
}

}

}
63 changes: 23 additions & 40 deletions src/blogify/backend/auth/handling/Handlers.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package blogify.backend.auth.handling

import blogify.backend.auth.jwt.validateJwt

import io.ktor.application.call
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
Expand All @@ -9,75 +11,56 @@ import io.ktor.response.respond
import blogify.backend.resources.User
import blogify.backend.routes.handling.CallPipeLineFunction
import blogify.backend.routes.handling.CallPipeline
import blogify.backend.routes.validTokens
import blogify.backend.util.BlogifyDsl
import blogify.backend.util.foldForOne
import blogify.backend.util.reason

/**
* Represents a predicate applied on a token.
*/
typealias AuthPredicate = suspend (token: String) -> Boolean

/**
* Represents a predicate applied on a [user][User].
*/
typealias UserAuthPredicate = suspend (user: User) -> Boolean

/**
* Wraps a [UserAuthPredicate] in an [AuthPredicate], allowing for methods to simply
* check against a [User] object instead of a token when using [authenticatedBy].
*
* @param token the token that should be used to provide the [User] object to [nextPredicate]
* @param nextPredicate the [predicate][UserAuthPredicate] that should be run on a [user][User]
*
* @return `false` if no users match the given [token] or if the wrapped predicate returns `false`.
*/
@BlogifyDsl
private suspend fun predicateOnUser(token: String, nextPredicate: UserAuthPredicate): Boolean {
return validTokens
.filterValues { it == token }
.keys.foldForOne (
one = { u -> nextPredicate.invoke(u); true },
multiple = { error("multiple tokens for user") },
none = { false }
)
}

/**
* Validates a token by making sure it authenticates a certain [User].
*
* @param user the user to whom the token must belong to
* @param mustBe the mustBe to whom the token must belong to
*/
fun isUser(user: User): AuthPredicate = { token ->
predicateOnUser(token) {
it == user
}
fun isUser(mustBe: User): UserAuthPredicate = { user ->
mustBe == user
}

/**
* Allows to wrap a call handler into a block that takes care of authentication using a given [predicate][AuthPredicate].
* Allows to wrap a call handler into a block that takes care of authentication using a given [predicate][UserAuthPredicate].
*
* For example, using [isUser] as a [predicate][AuthPredicate] will result in the block only being
* For example, using [isUser] as a [predicate][UserAuthPredicate] will result in the block only being
* executed if the provided [user][User] matches the authenticating user.
*
* @param predicate the predicate used as a check for authentication
* @param block the call handling block that is run if the check succeeds
*/
@BlogifyDsl
suspend fun CallPipeline.authenticatedBy(predicate: AuthPredicate, block: CallPipeLineFunction) {
suspend fun CallPipeline.runAuthenticated(predicate: UserAuthPredicate, block: CallPipeLineFunction) {
val header = call.request.header(HttpHeaders.Authorization) ?: run {
call.respond(HttpStatusCode.Unauthorized) // Header is missing
return
}

val token = header // Header validity procedure
.substringAfter("Bearer ", "none")
.takeIf { it != "none" && it.length == 86 /* 512 bit base64 token length */ }?.let { token -> // Token is provided
token // Propagate token value to token val
} ?: run { call.respond(HttpStatusCode.BadRequest); return } // Token is invalid or missing

if (predicate.invoke(token)) { // Check token against predicate
block.invoke(this, Unit)
} else call.respond(HttpStatusCode.Forbidden)
if (token == "none") {
call.respond(HttpStatusCode.BadRequest, reason("malforrmed token"))
return
}

validateJwt(call, token).fold (
success = { u ->
if (predicate.invoke(u)) { // Check token against predicate
block.invoke(this, Unit)
} else call.respond(HttpStatusCode.Forbidden)
}, failure = { ex ->
call.respond(HttpStatusCode.Forbidden, reason("invalid token - ${ex.javaClass.simpleName}"))
}
)

}
75 changes: 75 additions & 0 deletions src/blogify/backend/auth/jwt/Jwt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package blogify.backend.auth.jwt

import blogify.backend.resources.User
import blogify.backend.services.UserService
import blogify.backend.util.short
import blogify.backend.util.toUUID

import com.github.kittinunf.result.coroutines.SuspendableResult

import com.andreapivetta.kolor.green
import com.andreapivetta.kolor.red

import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import io.ktor.application.ApplicationCall

import org.slf4j.LoggerFactory

import java.util.Calendar
import java.util.Date

private val keyPair = Keys.keyPairFor(SignatureAlgorithm.ES512)

private val logger = LoggerFactory.getLogger("blogify-auth-token")

/**
* Creates a [Jws] for the specific [user].
*/
fun generateJWT(user: User) = Jwts
.builder()
.setSubject(user.uuid.toString())
.setIssuer("blogify")
.apply {
val cal = Calendar.getInstance()

cal.time = Date()
cal.add(Calendar.DAY_OF_MONTH, +7)

setExpiration(cal.time)
}
.signWith(keyPair.private).compact().also {
logger.debug("${"created token for user with id".green()} {${user.uuid.toString().take(8)}...}")
}

/**
* Validates a JWT, returning a [SuspendableResult] if that token authenticates a user, or an exception if the token is invalid
*/
suspend fun validateJwt(callContext: ApplicationCall, token: String): SuspendableResult<User, Exception> {
var jwsClaims: Jws<Claims>? = null

try {
jwsClaims = Jwts
.parser()
.setSigningKey(keyPair.public)
.requireIssuer("blogify")
.setAllowedClockSkewSeconds(1)
.parseClaimsJws(token)
} catch(e: JwtException) {
logger.debug("${"invalid token attempted".red()} - ${e.javaClass.simpleName.takeLastWhile { it != '.' }}")
e.printStackTrace()
return SuspendableResult.error(e)
} catch (e: Exception) {
logger.debug("${"unknown exception during token validation -".red()} - ${e.javaClass.simpleName.takeLastWhile { it != '.' }}")
e.printStackTrace()
}

val user = UserService.get(callContext, jwsClaims?.body?.subject?.toUUID() ?: error("malformed uuid in jwt"))
logger.debug("got valid JWT for user {${user.get().uuid.short()}...}".green())

return SuspendableResult.of { user.get() }
}
Loading

0 comments on commit 2737f15

Please sign in to comment.