mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-18 08:53:57 +00:00
Compare commits
4 Commits
v1.2.0
...
v1.3.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d85ce17f6 | ||
|
|
c26e129bda | ||
|
|
84ea5e4a41 | ||
|
|
491533d3f4 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
|||||||
|
# [1.3.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.1...v1.3.0-dev.2) (2024-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Expire token relative to current date time instead of just time ([c26e129](https://github.com/ReVanced/revanced-api/commit/c26e129bda09345761f291917f026c13e89a2572))
|
||||||
|
|
||||||
|
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0-dev.1) (2024-09-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add missing parameter and response documentation ([491533d](https://github.com/ReVanced/revanced-api/commit/491533d3f44ccd716eee80123d0875a05eb9435b))
|
||||||
|
|
||||||
# [1.2.0](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0) (2024-09-06)
|
# [1.2.0](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0) (2024-09-06)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching = true
|
org.gradle.caching = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 1.2.0
|
version = 1.3.0-dev.2
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import app.revanced.api.configuration.repository.GitHubBackendRepository
|
|||||||
import app.revanced.api.configuration.services.*
|
import app.revanced.api.configuration.services.*
|
||||||
import app.revanced.api.configuration.services.AnnouncementService
|
import app.revanced.api.configuration.services.AnnouncementService
|
||||||
import app.revanced.api.configuration.services.ApiService
|
import app.revanced.api.configuration.services.ApiService
|
||||||
import app.revanced.api.configuration.services.AuthService
|
import app.revanced.api.configuration.services.AuthenticationService
|
||||||
import app.revanced.api.configuration.services.OldApiService
|
import app.revanced.api.configuration.services.OldApiService
|
||||||
import app.revanced.api.configuration.services.PatchesService
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
import com.akuleshov7.ktoml.Toml
|
import com.akuleshov7.ktoml.Toml
|
||||||
@@ -116,11 +116,11 @@ fun Application.configureDependencies(
|
|||||||
|
|
||||||
val jwtSecret = dotenv["JWT_SECRET"]
|
val jwtSecret = dotenv["JWT_SECRET"]
|
||||||
val issuer = dotenv["JWT_ISSUER"]
|
val issuer = dotenv["JWT_ISSUER"]
|
||||||
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
|
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toLong()
|
||||||
|
|
||||||
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
|
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
|
||||||
|
|
||||||
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
val configuration = get<ConfigurationRepository>()
|
val configuration = get<ConfigurationRepository>()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.api.configuration
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.core.metadata.MethodInfo
|
||||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.http.content.*
|
import io.ktor.http.content.*
|
||||||
@@ -40,3 +41,11 @@ internal fun Route.staticFiles(
|
|||||||
extensions("json")
|
extensions("json")
|
||||||
},
|
},
|
||||||
) = staticFiles(remotePath, dir.toFile(), null, block)
|
) = staticFiles(remotePath, dir.toFile(), null, block)
|
||||||
|
|
||||||
|
internal fun MethodInfo.Builder<*>.canRespondUnauthorized() {
|
||||||
|
canRespond {
|
||||||
|
responseCode(HttpStatusCode.Unauthorized)
|
||||||
|
description("Unauthorized")
|
||||||
|
responseType<Unit>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
package app.revanced.api.configuration
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
import app.revanced.api.configuration.services.AuthService
|
import app.revanced.api.configuration.services.AuthenticationService
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
fun Application.configureSecurity() {
|
fun Application.configureSecurity() {
|
||||||
get<AuthService>().configureSecurity(this)
|
val authenticationService = get<AuthenticationService>()
|
||||||
|
|
||||||
|
install(Authentication) {
|
||||||
|
with(authenticationService) {
|
||||||
|
jwt()
|
||||||
|
digest()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.api.configuration.routes
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.canRespondUnauthorized
|
||||||
import app.revanced.api.configuration.installCache
|
import app.revanced.api.configuration.installCache
|
||||||
import app.revanced.api.configuration.installNotarizedRoute
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
import app.revanced.api.configuration.respondOrNotFound
|
import app.revanced.api.configuration.respondOrNotFound
|
||||||
@@ -8,10 +9,7 @@ import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
|
|||||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||||
import app.revanced.api.configuration.services.AnnouncementService
|
import app.revanced.api.configuration.services.AnnouncementService
|
||||||
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
import io.bkbn.kompendium.core.metadata.*
|
||||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
|
||||||
import io.bkbn.kompendium.core.metadata.PatchInfo
|
|
||||||
import io.bkbn.kompendium.core.metadata.PostInfo
|
|
||||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
import io.bkbn.kompendium.oas.payload.Parameter
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
@@ -138,9 +136,19 @@ internal fun Route.announcementsRoute() = route("announcements") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val authHeaderParameter = Parameter(
|
||||||
|
name = "Authorization",
|
||||||
|
`in` = Parameter.Location.header,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
required = true,
|
||||||
|
examples = mapOf("Bearer authentication" to Parameter.Example("Bearer abc123")),
|
||||||
|
)
|
||||||
|
|
||||||
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("Announcements")
|
tags = setOf("Announcements")
|
||||||
|
|
||||||
|
parameters = listOf(authHeaderParameter)
|
||||||
|
|
||||||
post = PostInfo.builder {
|
post = PostInfo.builder {
|
||||||
description("Create a new announcement")
|
description("Create a new announcement")
|
||||||
summary("Create announcement")
|
summary("Create announcement")
|
||||||
@@ -153,6 +161,7 @@ private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRout
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<Unit>()
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +248,7 @@ private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotari
|
|||||||
description = "The date and time the announcement to be archived",
|
description = "The date and time the announcement to be archived",
|
||||||
required = false,
|
required = false,
|
||||||
),
|
),
|
||||||
|
authHeaderParameter,
|
||||||
)
|
)
|
||||||
|
|
||||||
post = PostInfo.builder {
|
post = PostInfo.builder {
|
||||||
@@ -249,6 +259,7 @@ private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotari
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<Unit>()
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +274,7 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
|
|||||||
description = "The id of the announcement to unarchive",
|
description = "The id of the announcement to unarchive",
|
||||||
required = true,
|
required = true,
|
||||||
),
|
),
|
||||||
|
authHeaderParameter,
|
||||||
)
|
)
|
||||||
|
|
||||||
post = PostInfo.builder {
|
post = PostInfo.builder {
|
||||||
@@ -273,6 +285,7 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<Unit>()
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +300,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
|
|||||||
description = "The id of the announcement to update",
|
description = "The id of the announcement to update",
|
||||||
required = true,
|
required = true,
|
||||||
),
|
),
|
||||||
|
authHeaderParameter,
|
||||||
)
|
)
|
||||||
|
|
||||||
patch = PatchInfo.builder {
|
patch = PatchInfo.builder {
|
||||||
@@ -301,6 +315,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<Unit>()
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
|
|
||||||
delete = DeleteInfo.builder {
|
delete = DeleteInfo.builder {
|
||||||
@@ -311,6 +326,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<Unit>()
|
responseType<Unit>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import app.revanced.api.configuration.*
|
|||||||
import app.revanced.api.configuration.installCache
|
import app.revanced.api.configuration.installCache
|
||||||
import app.revanced.api.configuration.installNoCache
|
import app.revanced.api.configuration.installNoCache
|
||||||
import app.revanced.api.configuration.installNotarizedRoute
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
import app.revanced.api.configuration.respondOrNotFound
|
import app.revanced.api.configuration.respondOrNotFound
|
||||||
import app.revanced.api.configuration.schema.APIAbout
|
import app.revanced.api.configuration.schema.APIAbout
|
||||||
import app.revanced.api.configuration.schema.APIContributable
|
import app.revanced.api.configuration.schema.APIContributable
|
||||||
import app.revanced.api.configuration.schema.APIMember
|
import app.revanced.api.configuration.schema.APIMember
|
||||||
import app.revanced.api.configuration.schema.APIRateLimit
|
import app.revanced.api.configuration.schema.APIRateLimit
|
||||||
import app.revanced.api.configuration.services.ApiService
|
import app.revanced.api.configuration.services.ApiService
|
||||||
import app.revanced.api.configuration.services.AuthService
|
import app.revanced.api.configuration.services.AuthenticationService
|
||||||
import io.bkbn.kompendium.core.metadata.*
|
import io.bkbn.kompendium.core.metadata.*
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
@@ -23,7 +26,7 @@ import org.koin.ktor.ext.get as koinGet
|
|||||||
|
|
||||||
internal fun Route.apiRoute() {
|
internal fun Route.apiRoute() {
|
||||||
val apiService = koinGet<ApiService>()
|
val apiService = koinGet<ApiService>()
|
||||||
val authService = koinGet<AuthService>()
|
val authenticationService = koinGet<AuthenticationService>()
|
||||||
|
|
||||||
rateLimit(RateLimitName("strong")) {
|
rateLimit(RateLimitName("strong")) {
|
||||||
authenticate("auth-digest") {
|
authenticate("auth-digest") {
|
||||||
@@ -31,7 +34,7 @@ internal fun Route.apiRoute() {
|
|||||||
installTokenRouteDocumentation()
|
installTokenRouteDocumentation()
|
||||||
|
|
||||||
get {
|
get {
|
||||||
call.respond(authService.newToken())
|
call.respond(authenticationService.newToken())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,16 +168,38 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
||||||
|
val configuration = koinGet<ConfigurationRepository>()
|
||||||
|
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
description("Get a new authorization token")
|
description("Get a new authorization token")
|
||||||
summary("Get authorization token")
|
summary("Get authorization token")
|
||||||
|
parameters(
|
||||||
|
Parameter(
|
||||||
|
name = "Authorization",
|
||||||
|
`in` = Parameter.Location.header,
|
||||||
|
schema = TypeDefinition.STRING,
|
||||||
|
required = true,
|
||||||
|
examples = mapOf(
|
||||||
|
"Digest access authentication" to Parameter.Example(
|
||||||
|
value = "Digest " +
|
||||||
|
"username=\"ReVanced\", " +
|
||||||
|
"realm=\"ReVanced\", " +
|
||||||
|
"nonce=\"abc123\", " +
|
||||||
|
"uri=\"/v${configuration.apiVersion}/token\", " +
|
||||||
|
"algorithm=SHA-256, " +
|
||||||
|
"response=\"yxz456\"",
|
||||||
|
),
|
||||||
|
), // Provide an example for the header
|
||||||
|
),
|
||||||
|
)
|
||||||
response {
|
response {
|
||||||
description("The authorization token")
|
description("The authorization token")
|
||||||
mediaTypes("application/json")
|
mediaTypes("application/json")
|
||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<String>()
|
responseType<String>()
|
||||||
}
|
}
|
||||||
|
canRespondUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package app.revanced.api.configuration.services
|
|
||||||
|
|
||||||
import com.auth0.jwt.JWT
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
|
||||||
import io.ktor.server.application.*
|
|
||||||
import io.ktor.server.auth.*
|
|
||||||
import io.ktor.server.auth.jwt.*
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.text.HexFormat
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
internal class AuthService private constructor(
|
|
||||||
private val issuer: String,
|
|
||||||
private val validityInMin: Int,
|
|
||||||
private val jwtSecret: String,
|
|
||||||
private val authSHA256Digest: ByteArray,
|
|
||||||
) {
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this(
|
|
||||||
issuer,
|
|
||||||
validityInMin,
|
|
||||||
jwtSecret,
|
|
||||||
authSHA256DigestString.hexToByteArray(HexFormat.Default),
|
|
||||||
)
|
|
||||||
|
|
||||||
val configureSecurity: Application.() -> Unit = {
|
|
||||||
install(Authentication) {
|
|
||||||
jwt("jwt") {
|
|
||||||
realm = "ReVanced"
|
|
||||||
|
|
||||||
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
digest("auth-digest") {
|
|
||||||
realm = "ReVanced"
|
|
||||||
algorithmName = "SHA-256"
|
|
||||||
|
|
||||||
digestProvider { _, _ ->
|
|
||||||
authSHA256Digest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newToken(): String = JWT.create()
|
|
||||||
.withIssuer(issuer)
|
|
||||||
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
|
|
||||||
.sign(Algorithm.HMAC256(jwtSecret))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.auth.jwt.*
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.text.HexFormat
|
||||||
|
|
||||||
|
internal class AuthenticationService private constructor(
|
||||||
|
private val issuer: String,
|
||||||
|
private val validityInMin: Long,
|
||||||
|
private val jwtSecret: String,
|
||||||
|
private val authSHA256Digest: ByteArray,
|
||||||
|
) {
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
constructor(issuer: String, validityInMin: Long, jwtSecret: String, authSHA256DigestString: String) : this(
|
||||||
|
issuer,
|
||||||
|
validityInMin,
|
||||||
|
jwtSecret,
|
||||||
|
authSHA256DigestString.hexToByteArray(HexFormat.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AuthenticationConfig.jwt() {
|
||||||
|
jwt("jwt") {
|
||||||
|
realm = "ReVanced"
|
||||||
|
|
||||||
|
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AuthenticationConfig.digest() {
|
||||||
|
digest("auth-digest") {
|
||||||
|
realm = "ReVanced"
|
||||||
|
algorithmName = "SHA-256"
|
||||||
|
|
||||||
|
digestProvider { _, _ ->
|
||||||
|
authSHA256Digest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newToken(): String {
|
||||||
|
val issuedAt = Instant.now()
|
||||||
|
|
||||||
|
return JWT.create()
|
||||||
|
.withIssuer(issuer)
|
||||||
|
.withIssuedAt(issuedAt)
|
||||||
|
.withExpiresAt(issuedAt.plus(validityInMin, ChronoUnit.MINUTES))
|
||||||
|
.sign(Algorithm.HMAC256(jwtSecret))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user