Skip to content

Commit

Permalink
Support self-signed long-lived session tokens
Browse files Browse the repository at this point in the history
Closes #18
  • Loading branch information
Brutus5000 committed Dec 29, 2023
1 parent 4376e9f commit e9ef556
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 15 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin")
implementation("io.quarkus:quarkus-jdbc-mariadb")
implementation("io.quarkus:quarkus-kotlin")
implementation("io.quarkus:quarkus-oidc")
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-health")
implementation("io.quarkus:quarkus-rest-client-reactive-jackson")
Expand Down
75 changes: 75 additions & 0 deletions ids.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
11787b16-a348-463e-8272-691320e74aa9
1614911b-3aeb-4757-ae36-2f5c0126391c
1614b8a7-8cbb-42ef-a073-bca10b2328ba
161576d6-b236-4a2b-b959-bbc92bf1bd3f
1618d757-5202-4db0-be76-fe197c13f00c
1619ab54-b7a1-4c85-8602-cf2168107ef4
161d19bb-b9b9-4ec0-bf3a-e891bf81fe36
161da926-a2ac-4ed3-a02f-bd4bee6dbee7
161e71c4-7e3d-4600-801e-585ffd0a8022
162131c3-704c-4a83-b7d9-fff6de99cebd
1ae1afd1-21f5-4900-b772-eb029e8f6a61
1ae2e0cf-df8e-4c27-9517-fa3c7a988bc0
1fd5c803-7ecd-4fd3-8973-f70141c9d00b
1fd6719e-569f-4de7-a34d-d1bef1df26b2
1fdbda5a-6bf2-409f-a2ee-c949aa57b36f
1fe13b96-1a64-4d6c-96ec-74a9844f5b2d
1fe23b48-219b-46ad-bd64-f8ccbf91565f
29841d94-0776-43cc-816b-66cc5abd8085
298b4b40-9193-48f2-a9b8-6097ba99285a
2e0b087a-3d5d-4f7d-a618-feede52d4d41
2e0c9ef7-a44f-42f9-962a-d11e1dac8d17
2e10e0dc-8c69-4418-9e3a-7100322e191b
2e137eb7-b146-4d6d-ab8e-5153529e4c33
2e144b3b-0e6e-44c0-8fda-3e818770c011
2e16b7cf-114d-4b8b-b2cd-6f770150336a
2e1b2960-6745-4e0b-a1ec-4fadf8ebaab7
2e1c457c-5546-4e33-8f80-10923abe9755
32b670f7-ae32-418f-8a37-024b9eec457f
32b74935-551f-484c-9719-a887f4635a71
32bd3b83-29fc-4182-9a5a-ec85628e0057
32bea65d-e387-4e66-821a-17e71610a45a
32bf9b99-f71c-430f-ab34-ea79b4c53e9d
32c007a7-ee2c-4b3e-b205-6310ab61dd7a
381c0312-81a0-4ba9-a34f-3d6a3de55468
381ca2a5-6221-442a-8279-f9e9de501516
3cccc9cf-13bb-4776-a809-f739ffdc3df2
3ccd8ab3-963d-43ac-90be-baef4433e617
3ce01084-fd05-41de-9ff3-bd97b862d747
3ce4e910-022c-419a-a97b-4412602c026b
467da129-120e-4700-84a2-c89aa7af4290
467ff6e9-e55a-47a4-8cae-0f8d09c3299b
4680bc21-0710-4a7b-99d4-bf438456d545
4684a63b-27f5-4650-878b-cb679fb865f5
468657e4-a236-4b8e-82cf-9ddfbbaee928
46866620-47b7-42fa-90dc-95b9ae310bad
46869fc8-7de9-46b8-beee-c6fccc60b644
4686de20-4072-4674-af36-c3fb6cb22935
468af2b1-7efe-4a5e-85bc-84c1ad41a6ea
4fc5d4ce-3662-46c6-8ca1-6eefaa4e236b
4fc60636-d948-4c14-af56-55e7bb6fecb1
4fc6b83c-6f55-4378-8919-898036866e98
4fc8d96a-9eb7-4424-938a-7410e1b99af5
552de312-f3ae-412c-97d0-feeaf6395670
5535228e-6fa2-4e4c-9ae5-815c59a437e9
553a365f-310d-4d09-ae32-4a0141ddce4f
553c9ee0-9919-4939-a372-43226388cc01
553e7304-61a6-4ee1-9f6a-ed0b7064b03e
59f7fb2a-02cf-4b12-bd01-0dedadf495f3
59fc6c9b-9347-4d13-8f83-ef087527c1a1
59feb30d-840a-46fa-9d64-96b8a85e19eb
59ffc8ed-d420-4f4c-ade2-9771ca51b423
5f23c9cc-6f83-4d9a-ad25-a765729b23ac
5f2b15a9-759e-4387-8e9a-95492f07ca2e
5f363fb8-7609-4b5b-b4f9-c4a95ae91971
5f392730-2c4a-444b-9c8b-e0083e8fe4fb
5f3ee721-e07b-4cf4-85ce-32fd7ae24ac7
5f420463-0e08-4038-9b20-beaf015ecb9e
6455e9ef-b8f6-4635-a8b7-062abc5ea5f0
773d3f36-5b1e-49d1-90e8-875617d4240d
7742a7b1-d4f9-405a-aba0-8835b35a4e8a
86304a97-acf2-42f9-a0ae-66118f913fb8
8630dd54-6377-40fd-9b7e-15f7f9f90971
86363187-a893-4979-9971-495c72ede2c2
86376d38-769d-44d9-86ba-f3ee58c977dd
86393fb3-c273-4d43-bcbc-60d128a2c2a6
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import io.smallrye.config.ConfigMapping

@ConfigMapping(prefix = "faf")
interface FafProperties {
fun selfUrl(): String

/**
* Must start with a letter, otherwise potential conflicts in Xirsys!
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.faforever.icebreaker.security

import com.faforever.icebreaker.config.FafProperties
import com.fasterxml.jackson.databind.ObjectMapper
import io.quarkus.oidc.TenantResolver
import io.vertx.core.http.HttpHeaders.AUTHORIZATION
import io.vertx.ext.web.RoutingContext
import jakarta.enterprise.context.ApplicationScoped

/**
* This application supports 2 JWT providers:
*
* * "Official" FAF access token from Ory Hydra (hydra.faforever.com)
* * Self-signed JWTs inside this service that are used for extended session token.
*
* This tenant resolver selects the right provider based on the issuer of the JWT.
*/
@ApplicationScoped
class CustomTenantResolver(
private val fafProperties: FafProperties,
private val objectMapper: ObjectMapper,
) : TenantResolver {

override fun resolve(context: RoutingContext): String? =
context.request().getHeader(AUTHORIZATION)
?.takeIf { it.startsWith("Bearer ") }
?.let {
val rawToken = it.substring(7)
val body = java.util.Base64.getDecoder().decode(rawToken.split(".")[1])
val json = objectMapper.readTree(body)

json["iss"]?.textValue()
}
?.takeIf { it == fafProperties.selfUrl() }
?.let { "self-tenant" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor
import io.quarkus.security.runtime.QuarkusSecurityIdentity
import io.smallrye.mutiny.Uni
import jakarta.enterprise.context.ApplicationScoped
import jakarta.json.JsonNumber
import jakarta.json.JsonString
import org.eclipse.microprofile.jwt.JsonWebToken

Expand All @@ -24,7 +25,7 @@ class FafPermissionsAugmentor : SecurityIdentityAugmentor {
when (val principal = identity.principal) {
is JsonWebToken -> {
val roles = principal.claim<Map<String, Any>>("ext")
.map { it["roles"] as List<JsonString> }
.map<List<JsonString>> { it["roles"] as? List<JsonString> }
.map { it.map { jsonString -> jsonString.string } }
.map { it.toSet() }
.orElse(setOf())
Expand All @@ -33,6 +34,12 @@ class FafPermissionsAugmentor : SecurityIdentityAugmentor {
.map { it.map { jsonString -> jsonString.string }.toSet() }
.orElse(setOf())

principal.claim<Map<String, Any>>("ext")
.map<JsonNumber> { it["gameId"] as? JsonNumber }
.ifPresent {
builder.addAttribute("gameId", it.longValue())
}

builder.addPermissionChecker { requiredPermission ->
val hasRole = roles.contains(requiredPermission.name)
val hasScopes = requiredPermission.actions.split(",").all { scopes.contains(it) }
Expand Down
38 changes: 38 additions & 0 deletions src/main/kotlin/com/faforever/icebreaker/service/SessionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import com.faforever.icebreaker.persistence.IceSessionEntity
import com.faforever.icebreaker.persistence.IceSessionRepository
import com.faforever.icebreaker.util.AsyncRunner
import io.quarkus.scheduler.Scheduled
import io.quarkus.security.ForbiddenException
import io.quarkus.security.UnauthorizedException
import io.quarkus.security.identity.SecurityIdentity
import io.smallrye.jwt.build.Jwt
import jakarta.enterprise.inject.Instance
import jakarta.inject.Singleton
import jakarta.transaction.Transactional
import org.eclipse.microprofile.jwt.JsonWebToken
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Instant
Expand All @@ -20,12 +25,45 @@ class SessionService(
sessionHandlers: Instance<SessionHandler>,
private val fafProperties: FafProperties,
private val iceSessionRepository: IceSessionRepository,
private val securityIdentity: SecurityIdentity,
) {
private val activeSessionHandlers = sessionHandlers.filter { it.active }

fun buildToken(gameId: Long): String {
val userId = when (val principal = securityIdentity?.principal) {
null -> throw UnauthorizedException("No principal available")
is JsonWebToken -> principal.subject.toInt()
else -> throw IllegalStateException("Unexpected principal type: ${principal.javaClass} ($principal)")
}

return Jwt.subject(userId.toString())
.claim(
"ext",
mapOf(
"roles" to listOf("USER"),
"gameId" to gameId,
),
)
.claim("scp", listOf("lobby"))
.issuer(fafProperties.selfUrl())
.audience(fafProperties.selfUrl())
.expiresAt(Instant.now().plus(fafProperties.maxSessionLifeTimeHours(), ChronoUnit.HOURS))
.sign()
}

@Deprecated(
message = "Only remaining for java-ice-adapter compatibility",
replaceWith = ReplaceWith("getSession"),
)
fun getServers(): List<Server> = activeSessionHandlers.flatMap { it.getIceServers() }

fun getSession(gameId: Long): Session {
// For compatibility reasons right now we only check on mismatch because general FAF JWT are still allowed
// but have no implicit gameId attached
securityIdentity.attributes["gameId"]?.takeIf { it != gameId }?.run {
throw ForbiddenException("Not authorized to join game $gameId")
}

val sessionId = "game/$gameId"

val servers = activeSessionHandlers.flatMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,37 @@ package com.faforever.icebreaker.web

import com.faforever.icebreaker.service.Session
import com.faforever.icebreaker.service.SessionService
import io.quarkus.runtime.annotations.RegisterForReflection
import io.quarkus.security.PermissionsAllowed
import jakarta.inject.Singleton
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
import org.jboss.resteasy.reactive.RestPath

@Path("/session")
@Singleton
class SessionController(private val sessionService: SessionService) {
class SessionController(
private val sessionService: SessionService,
) {

@RegisterForReflection
data class TokenRequest(val gameId: Long)

@RegisterForReflection
data class TokenResponse(val jwt: String)

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/token")
fun buildToken(tokenRequest: TokenRequest): TokenResponse =
sessionService.buildToken(tokenRequest.gameId)
.let { TokenResponse(it) }

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/game/{gameId}")
Expand Down
91 changes: 78 additions & 13 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,29 @@ quarkus:
physical-naming-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
flyway:
migrate-at-start: true
mp:
jwt:
verify:
issuer: ${HYDRA_TOKEN_ISSUER:http://faf-ory-hydra:4444/}
key-format: JWKS
publickey:
location: ${HYDRA_JWKS_URL:http://localhost:4444/.well-known/jwks.json}
oidc:
auth-server-url: ${HYDRA_URL:http://localhost:4444}
# A tenant for our self-signed JWTs
# (also requires the CustomTenantResolver)
self-tenant:
# There is no .well-known/openid-configuration
auth-server-url: ${SELF_URL:https://ice.faforever.com}
discovery-enabled: false
token:
# Hard coded JWT settings, as there is no JWKS
signature-algorithm: ${JWT_ALGORITHM:RS256}
# No Quarkus, there is really no JWKS! Stop looking for it.
forced-jwk-refresh-interval: 0S
public-key: ${JWT_PUBLIC_KEY_PATH}
http:
auth:
permission:
authenticated:
paths: "/*"
policy: authenticated

faf:
self-url: ${SELF_URL:https://ice.faforever.com}
environment: ${ENVIRONMENT:dev}
token-lifetime-seconds: 86400 # 24h because of lobbies/games/reconnects can happen for a long time
max-session-life-time-hours: 24
Expand All @@ -31,12 +45,63 @@ xirsys:
ident: ${XIRSYS_IDENT}
secret: ${XIRSYS_SECRET}
channel-namespace: "faf"

smallrye:
jwt:
sign:
key: ${JWT_PRIVATE_KEY_PATH}
"%dev":
quarkus:
log:
category:
"org.hibernate.SQL":
smallrye:
jwt:
sign:
# Just a random private key for testing
key: |-
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDXsCsl9W0vnW2k
5GaNOVoZ6LPFYu60Y1Cd4ERRXvt8KzKTm2HHZeLKd77OLeIHR4RvJ2Q76SFwfDBM
35F5eEx1mjPua2ljxbObsgz/bA9yBwO1RugpNOe+GoGUhPyZvmmZwqRnnQsT/SHV
ZvRq7ej6k+KkJf09IIOxfWrGUj8SajW3iEpkuKdNpjp1dRnJdZAZ8mV1LgnwHCAf
osL3t3+PElBSxnRQNW9iYVwB9wQAWK+aivx5warhuCeyKVtDaR0x96bOUTaKL4i/
Uihn0CElGt5ZA907wHa6/N4Z8ssXjY+/vizYB2VYxuAG/MVkkbwWUUTGjzEEX6Ww
h5icvVm1AgMBAAECggEAAZYYGyVc8ja0MbxETNGZKgueFtuNaeI5G5AksHyEWPtw
WcmQxIipTFfpHVcVDHyoKrEdeZtTVaJ0MHyMc1pBJbRGoYBEvCkeEw0SL2a6Dlqi
2lh1KKhs8+b6AP+hY/gUir71upVbGYCJGSqyrX6mcgFYb2CgJizxCwMjH+ZG9Hm0
CkGeh4g0VDOWmx4uCChXSyoaPzD4yTJts/EOpSD61KqS+cNcnRD8PVUxwwSH+4DY
ZSuaAUC/kFvD4qQq2lY/eia2CQi2R1Ff2TCxcbNZ34yW8IR1UBdrOPo3orQK5vSf
iT5++MYJmTZJ8/QxY5M1nZqiyJEjTvaBQNGv8abKWQKBgQDxd/5lJkc13x8jPFJm
EnmPvxrJaYk3MLW3dtxz1HtjHDQAvCmjXy7Ss13WhLJv9nHJDtQlSRr+l+7eNPTP
QtiwDsqv9COfbPbvH2qcNJuNoINQ2YSKYvR0j+QlMz2dHroWEyXL4oyOfXAJ3ZrU
lyWn/a2BD3uiJAj4p8YzJgfgnwKBgQDkqwGC6AMLPbVmhCMnUd+cxFMkYymdi8R4
ZXMkjJiMLAOt8tkp8T0nqxC/zMfD0jnPKw1R9MP7XlM/tonLeAM/P8GUMwJnTCTc
PvP1JxkvMG3do+7y9AbLyJsNZDkbYj1wLzvZYUrXQV/HKU4balDj3QVI6yr+W6ha
idlsMDYBKwKBgBeuF9GdlmAvGGOhN8dwymERcbQM2HsEGN38FxR44vzOOD9WNJMj
83iQRISUENewCGqaPK3HZJFRHwjFkrh8qrlhSflFbPTmf7TllNPqyNJzykz0d+4G
VEjWD56iTsmIyOD/UbaT6grTPFiLVfLBO90koI5GkW5OMF8KPQKpGR6rAoGAU5NQ
1RiZbDVcpKBs/MUG1pRG0wjPP/7Ci0KBB/2/D5RSr/QPfS3nrSTv1ToyVRbz/Az/
LFIqgyghgyrjSBOQFEDoLpNKMJj66+iyX4qvwLiRny14eyHHjhm+2fEkkiagz+zj
kfrmULBbIj6thoWgFPhGIzWYnCjB6n1xkwI36ssCgYACiZNHvqld4Om2IChCjIV6
UPNLUDOvr7V1qsEy+y0dp2RQH9Es121n/v30GYfsUUYmH35CQYR0aOEqU17Qm2V7
1auC1ZD9UeE9dy2LpW635uYf16D5FejAcmxyf/MRSBBnvFauGdS2vZ7Pf05u9Zpw
i8UgZE7+lTYKv7+4ujmgHw==
-----END PRIVATE KEY-----
quarkus:
oidc:
self-tenant:
public-key: |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA17ArJfVtL51tpORmjTla
GeizxWLutGNQneBEUV77fCsyk5thx2Xiyne+zi3iB0eEbydkO+khcHwwTN+ReXhM
dZoz7mtpY8Wzm7IM/2wPcgcDtUboKTTnvhqBlIT8mb5pmcKkZ50LE/0h1Wb0au3o
+pPipCX9PSCDsX1qxlI/Emo1t4hKZLinTaY6dXUZyXWQGfJldS4J8BwgH6LC97d/
jxJQUsZ0UDVvYmFcAfcEAFivmor8ecGq4bgnsilbQ2kdMfemzlE2ii+Iv1IoZ9Ah
JRreWQPdO8B2uvzeGfLLF42Pv74s2AdlWMbgBvzFZJG8FlFExo8xBF+lsIeYnL1Z
tQIDAQAB
-----END PUBLIC KEY-----
log:
level: DEBUG
category:
"org.hibernate.SQL":
level: DEBUG
"com.faforever":
level: DEBUG
"com.faforever":
"io.quarkus":
level: DEBUG

0 comments on commit e9ef556

Please sign in to comment.