Skip to content

Commit

Permalink
feat: add authorization logic
Browse files Browse the repository at this point in the history
  • Loading branch information
OpenSrcerer committed Feb 13, 2024
1 parent 587246c commit 88d5848
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 46 deletions.
24 changes: 15 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ services:
paddy-bridge:
aliases:
- paddy.auth.io
# build: .
image: ghcr.io/opensrcerer/paddy-auth:master
build: .
# image: ghcr.io/opensrcerer/paddy-auth:master
restart: on-failure
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/auth/v1/health"]
interval: 5s
timeout: 30s
retries: 3
start_period: 10s
ports:
- "80:80"
env_file:
- ".env"
- ".env"

emqx1:
image: emqx:5.5.0
container_name: emqx1
restart: on-failure
networks:
paddy-bridge:
aliases:
- node1.emqx.io
ports:
- "1883:1883"
- "18083:18083"
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
package online.danielstefani.paddy.security
package online.danielstefani.paddy.authentication

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 online.danielstefani.paddy.db.device.DeviceRepository
import online.danielstefani.paddy.security.JwtService
import org.jboss.resteasy.reactive.RestPath
import java.util.*


@Path("/")
class JwksController(
class DeviceAuthenticationController(
private val jwtService: JwtService,
private val deviceRepository: DeviceRepository
) {

/*
Return a JWKS to authenticate MQTT clients.
*/
@Produces(MediaType.APPLICATION_JSON)
@Path("/jwks")
@GET
fun getJwks(): String {
return jwtService.makeJwks()
}

@Path("/jwt")
/*
Mint a new JWT. Should be called only by the backend
as these JWTs have permissions to connect to all topics.
*/
@Path("/admin-jwt")
@GET
fun getJwt(): String {
return jwtService.makeJwt()
return jwtService.makeJwt("paddy-backend")
}

@Path("/create-device/{serial}")
@POST
fun createDevice(@RestPath serial: String?): Map<String, String> {
val jwt = jwtService.makeJwt()
val deviceSerial = if (serial.isNullOrEmpty()) UUID.randomUUID().toString() else serial
fun createDevice(@RestPath serial: String): Map<String, String> {
val deviceSerial = serial.ifEmpty { UUID.randomUUID().toString() }
val jwt = jwtService.makeJwt(deviceSerial)

deviceRepository.createDevice(deviceSerial, jwt)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package online.danielstefani.paddy.authorization

import io.quarkus.logging.Log
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.impl.jose.JWT
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
import online.danielstefani.paddy.authorization.dto.AuthorizationRequestDto
import online.danielstefani.paddy.authorization.dto.AuthorizationResultDto
import org.jboss.resteasy.reactive.RestResponse

import online.danielstefani.paddy.authorization.dto.AuthorizationResultDto.Companion.forbid
import online.danielstefani.paddy.authorization.dto.AuthorizationResultDto.Companion.allow

@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
class AuthorizationController {

@Path("/verify")
@POST
fun getJwt(authDto: AuthorizationRequestDto): RestResponse<AuthorizationResultDto> {

// Parse JWT
val jwt: JsonObject? =
try {
JWT.parse(authDto.username)
} catch (ex: Exception) {
Log.debug("Received invalid JWT: <${authDto.username}>.")
null
}
if (jwt == null) return forbid()

val sub = jwt.getJsonObject("payload").getString("sub")

// Special case: Check if the token is for the backend
if (sub.equals("paddy-backend")) {
Log.debug("Received Paddy Backend JWT: <${authDto.username}>.")
return allow()
}

// Check if the topic matches the sub -> if no match 403
return if (topicMatchSub(authDto.topic, sub)) allow() else forbid()
}

/*
Topics are expected to be in some-topic-here/test/abc/SUB-XXXX.
This function extracts the SUB-XXXX part and checks it against the sub in the claim.
*/
private fun topicMatchSub(topic: String, sub: String): Boolean {
val topicSerial: String? = with(topic.split("/")) {
if (this.isEmpty()) null
else this.last()
}
return topicSerial.equals(sub)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package online.danielstefani.paddy.authorization.dto

/*
This payload will be sent by EMQX to verify
whether an actor has access to a topic.
This step happens after the actor's authenticity has been verified.
https://www.emqx.io/docs/en/latest/access-control/authz/http.html
*/
data class AuthorizationRequestDto(
val username: String, // Expected to be a JWT
val topic: String // Topic that client wants to access
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package online.danielstefani.paddy.authorization.dto

import com.fasterxml.jackson.annotation.JsonValue
import org.jboss.resteasy.reactive.RestResponse
import org.jboss.resteasy.reactive.RestResponse.*
import java.util.*

class AuthorizationResultDto private constructor(
val result: DeviceAuthorizationResult
) {
companion object {
fun allow(): RestResponse<AuthorizationResultDto> {
return ResponseBuilder.ok(AuthorizationResultDto(DeviceAuthorizationResult.ALLOW))
.status(Status.OK)
.build()
}

fun forbid(): RestResponse<AuthorizationResultDto> {
return ResponseBuilder.ok(AuthorizationResultDto(DeviceAuthorizationResult.DENY))
.status(Status.OK)
.build()
}

fun ignore(): RestResponse<AuthorizationResultDto> {
return ResponseBuilder.ok(AuthorizationResultDto(DeviceAuthorizationResult.IGNORE))
.status(Status.OK)
.build()
}
}

enum class DeviceAuthorizationResult {
ALLOW, DENY, IGNORE;

@JsonValue
fun toLowerCase(): String {
return toString().lowercase(Locale.getDefault())
}
}
}
63 changes: 33 additions & 30 deletions src/main/kotlin/online/danielstefani/paddy/security/JwtService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,16 @@ import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPublicKeySpec
import java.time.Instant
import java.util.Base64
import java.util.*

@ApplicationScoped
class JwtService(
private val jwksConfiguration: JwksConfiguration
) {
fun makeJwks(): String {
val rsaPublicKey = makeKeyPair().second as RSAPublicKey

val jwksResponse = java.lang.String.format(
"""
{
"keys": [{
"kty": "%s",
"kid": "1",
"n": "%s",
"e": "%s",
"alg": "RS256",
"use": "sig"
}]
}
""".trimIndent()
.replace("\n", "")
.replace(" ", ""),
rsaPublicKey.algorithm,
Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()),
Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray())
)

return jwksResponse
}

fun makeJwt(jwtLifetimeSeconds: Long = 31540000L): String {
fun makeJwt(
sub: String,
jwtLifetimeSeconds: Long = 31540000
): String {
val (privateKey, _) = makeKeyPair()

val jwtHeader: String = Base64.getUrlEncoder().withoutPadding().encodeToString(
Expand All @@ -57,11 +34,11 @@ class JwtService(

val jwtPayloadTemplate = """
{
"sub": "daniel.stefani",
"sub": "$sub",
"iss": "https://danielstefani.online",
"iat": %s,
"exp": %s,
"aud": "MQTT Clients"
"aud": "Paddy MQTT Broker Clients"
}
""".trimIndent().replace(" ", "").replace("\n", "")
Expand All @@ -88,6 +65,32 @@ class JwtService(
return "$jwtContent.$jwtSignature"
}

fun makeJwks(): String {
val rsaPublicKey = makeKeyPair().second as RSAPublicKey

val jwksResponse = java.lang.String.format(
"""
{
"keys": [{
"kty": "%s",
"kid": "1",
"n": "%s",
"e": "%s",
"alg": "RS256",
"use": "sig"
}]
}
""".trimIndent()
.replace("\n", "")
.replace(" ", ""),
rsaPublicKey.algorithm,
Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()),
Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray())
)

return jwksResponse
}

private fun makeKeyPair(): Pair<PrivateKey, PublicKey> {
val keyFactory = KeyFactory.getInstance("RSA")

Expand Down

0 comments on commit 88d5848

Please sign in to comment.