Skip to content

Commit

Permalink
refactor(backend): Use github OAuth token
Browse files Browse the repository at this point in the history
* Github oauth mitigates the github rate limit problem
* Rewrote all tests to use only requests and cookies
* Removed jwt and auth sessions in favor of using cookies
  * This breaks front dev flow, and needs to be fixed.
  • Loading branch information
Grohden committed Aug 5, 2020
1 parent c6b67e6 commit a6103bb
Show file tree
Hide file tree
Showing 25 changed files with 850 additions and 844 deletions.
3 changes: 2 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project is written in kotlin using ktor and exposed.

* JDK
* Env variables set, see `.env.example` on root folder
* Github personal access token (env) for development only

### Run server

Expand All @@ -34,4 +35,4 @@ Once you have correct env variables on your path run:
* `./gradlew test`

If you get a `BUILD SUCESSFUL` it means all current tests
are passing.
are passing.
8 changes: 1 addition & 7 deletions backend/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ ktor {
modules = [ com.grohden.repotagger.ApplicationKt.module ]
}

environment = prod
environment = dev
environment = ${?TAGGER_ENV}
}

jwt {
domain = "https://jwt-provider-domain/"
audience = "jwt-audience"
realm = "repo tagger app"
}
8 changes: 0 additions & 8 deletions backend/src/com/grohden/repotagger/AliasDeclarations.kt

This file was deleted.

2 changes: 0 additions & 2 deletions backend/src/com/grohden/repotagger/ApiDeclarations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,5 @@ suspend fun ApplicationCall.respondError(
* Error messages exhibited by facades/api
*/
enum class FacadeError(val message: String) {
INVALID_PAYLOAD("invalid payload"),
USER_NAME_TAKEN("User name already taken"),
GITHUB_NAME_NOT_FOUND("Github api returned error, user name may not exist")
}
108 changes: 70 additions & 38 deletions backend/src/com/grohden/repotagger/Application.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.grohden.repotagger

import com.grohden.repotagger.api.account
import com.grohden.repotagger.api.repository
import com.grohden.repotagger.api.session
import com.grohden.repotagger.api.userTag
import com.grohden.repotagger.dao.DAOFacade
import com.grohden.repotagger.dao.DAOFacadeDatabase
import com.grohden.repotagger.github.api.GithubClient
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.application.*
import io.ktor.auth.Authentication
import io.ktor.auth.jwt.jwt
import io.ktor.auth.OAuthServerSettings
import io.ktor.auth.oauth
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.json.GsonSerializer
Expand All @@ -22,16 +22,22 @@ import io.ktor.gson.gson
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.defaultResource
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.http.content.*
import io.ktor.request.host
import io.ktor.request.path
import io.ktor.request.port
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.netty.EngineMain
import io.ktor.sessions.SessionStorageMemory
import io.ktor.sessions.SessionTransportTransformerMessageAuthentication
import io.ktor.sessions.Sessions
import io.ktor.sessions.cookie
import io.ktor.util.hex
import org.jetbrains.exposed.sql.Database
import org.slf4j.event.Level
import java.io.File

/**
* Pool of JDBC connections used.
Expand All @@ -50,6 +56,8 @@ fun main(args: Array<String>) {
EngineMain.main(args)
}

@Suppress("EXPERIMENTAL_API_USAGE")
val secretSignKey = hex(EnvProvider.hashKeySecret)

@Suppress("EXPERIMENTAL_API_USAGE")
fun Application.getEnv(entry: String) =
Expand Down Expand Up @@ -113,14 +121,31 @@ fun Application.module() {
moduleWithDependencies(dao = dao)
}

fun Application.moduleWithDependencies(dao: DAOFacade) {
val client = makeClient()
val github = GithubClient(client)
private fun ApplicationCall.redirectUrl(path: String): String {
val defaultPort = if (request.origin.scheme == "http") 80 else 443
val hostPort = request.host() + request.port().let { port ->
if (port == defaultPort) "" else ":$port"
}
val protocol = request.origin.scheme
return "$protocol://$hostPort$path"
}


install(DefaultHeaders) {
header(HttpHeaders.Server, "") // OWASP recommended
fun Application.moduleWithDependencies(dao: DAOFacade) {
val httpClient = makeClient()
val githubClient = GithubClient(httpClient)

install(Sessions) {
cookie<TaggerSessionUser>(
TAGGER_SESSION_COOKIE,
SessionStorageMemory()
) {
transform(SessionTransportTransformerMessageAuthentication(secretSignKey))
}
}

install(DefaultHeaders)

install(ConditionalHeaders)

if (isDev || isTesting) {
Expand All @@ -136,25 +161,35 @@ fun Application.moduleWithDependencies(dao: DAOFacade) {
method(HttpMethod.Patch)
header(HttpHeaders.Authorization)
header(HttpHeaders.ContentType)
header(HttpHeaders.Cookie)
header(HttpHeaders.SetCookie)
header(HttpHeaders.AccessControlAllowCredentials)
header(HttpHeaders.AccessControlAllowOrigin)
allowCredentials = true
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
allowSameOrigin = true
anyHost()
}
}

install(DataConversion)

val jwtIssuer = getEnv("jwt.domain")
val jwtAudience = getEnv("jwt.audience")
val jwtRealm = getEnv("jwt.realm")
val jwtProvider = JwtProvider(jwtIssuer, jwtAudience)
val githubOAuthProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "github",
authorizeUrl = "https://github.com/login/oauth/authorize",
accessTokenUrl = "https://github.com/login/oauth/access_token",
clientId = EnvProvider.githubClientId ?: "",
clientSecret = EnvProvider.githubClientSecret ?: "",
defaultScopes = listOf("read:user", "public_repo")
)

/**
* Sets on internal http client the provider rules
*/
install(Authentication) {
jwt {
realm = jwtRealm
verifier(jwtProvider.verifier)
validate {
it.payload.getClaim("id").asInt()?.let(dao::findUserById)
}
oauth("github") {
client = httpClient
providerLookup = { githubOAuthProvider }
urlProvider = { redirectUrl("/login") }
}
}

Expand All @@ -164,6 +199,12 @@ fun Application.moduleWithDependencies(dao: DAOFacade) {
gson()
}

install(StatusPages) {
exception<NoSessionException> {
call.respond(HttpStatusCode.Unauthorized)
}
}

routing {
if (isDev || isTesting) {
trace { application.log.trace(it.buildText()) }
Expand All @@ -175,23 +216,14 @@ fun Application.moduleWithDependencies(dao: DAOFacade) {
}

route("api") {
account(dao, github, jwtProvider)
repository(dao, github)
userTag(dao, github)
}

install(StatusPages) {
exception<AuthenticationException> {
call.respond(HttpStatusCode.Unauthorized)
}
exception<AuthorizationException> {
call.respond(HttpStatusCode.Forbidden)
}
session(
usePersonalToken = isDev || isTesting,
githubClient = githubClient,
githubOAuthProvider = githubOAuthProvider
)
repository(dao, githubClient)
userTag(githubClient, dao)
}
}
}


class AuthenticationException : RuntimeException()
class AuthorizationException : RuntimeException()

7 changes: 3 additions & 4 deletions backend/src/com/grohden/repotagger/EnvProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ object EnvProvider {
private val env by lazy { System.getenv() }

val hashKeySecret by lazy { env["HASH_KEY_SECRET"]!! }
val jwtKeySecret by lazy { env["JWT_KEY_SECRET"]!! }
val githubClientId by lazy { env["GITHUB_CLIENT_ID"] }
val githubClientSecret by lazy { env["GITHUB_CLIENT_SECRET"] }
val personalAccessToken by lazy { env["PERSONAL_ACCESS_TOKEN"] }

fun validate() {
assert(env["HASH_KEY_SECRET"]?.isNotBlank() == true) {
"HASH_KEY_SECRET variable must not be null or blank"
}
assert(env["JWT_KEY_SECRET"]?.isNotBlank() == true) {
"JWT_KEY_SECRET variable must not be null or blank"
}
}
}
38 changes: 0 additions & 38 deletions backend/src/com/grohden/repotagger/JwtProvider.kt

This file was deleted.

41 changes: 0 additions & 41 deletions backend/src/com/grohden/repotagger/PasswordHash.kt

This file was deleted.

29 changes: 29 additions & 0 deletions backend/src/com/grohden/repotagger/SessionDeclarations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.grohden.repotagger

import io.ktor.application.ApplicationCall
import io.ktor.sessions.get
import io.ktor.sessions.sessions

const val TAGGER_SESSION_COOKIE = "SESSION_ID"

class NoSessionException : RuntimeException()


/**
* Base session data for the app
*/
class TaggerSessionUser(
val githubUserId: Int,
val name: String,
val token: String
)

/**
* Requires a session, if no session is
* present [NoSessionException] is thrown
*/
fun ApplicationCall.requireSession(): TaggerSessionUser {
val session = sessions.get<TaggerSessionUser>()

return session ?: throw NoSessionException()
}
Loading

0 comments on commit a6103bb

Please sign in to comment.