-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementing a more maintainable version of annotation based authenti…
…cation
- Loading branch information
1 parent
6387ef4
commit e600214
Showing
19 changed files
with
271 additions
and
17 deletions.
There are no files selected for viewing
89 changes: 89 additions & 0 deletions
89
cam/src/main/kotlin/org/dreamexposure/discal/cam/business/SecurityService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package org.dreamexposure.discal.cam.business | ||
|
||
import org.dreamexposure.discal.core.business.ApiKeyService | ||
import org.dreamexposure.discal.core.business.SessionService | ||
import org.dreamexposure.discal.core.config.Config | ||
import org.dreamexposure.discal.core.extensions.isExpiredTtl | ||
import org.dreamexposure.discal.core.`object`.new.security.Scope | ||
import org.dreamexposure.discal.core.`object`.new.security.TokenType | ||
import org.springframework.stereotype.Component | ||
|
||
@Component | ||
class SecurityService( | ||
private val sessionService: SessionService, | ||
private val apiKeyService: ApiKeyService, | ||
) { | ||
suspend fun authenticateToken(token: String): Boolean { | ||
val schema = getSchema(token) | ||
val tokenStr = token.removePrefix(schema.schema) | ||
|
||
return when (schema) { | ||
TokenType.BEARER -> authenticateUserToken(tokenStr) | ||
TokenType.APP -> authenticateAppToken(tokenStr) | ||
TokenType.INTERNAL -> authenticateInternalToken(tokenStr) | ||
else -> false | ||
} | ||
} | ||
|
||
suspend fun validateTokenSchema(token: String, allowedSchemas: List<TokenType>): Boolean { | ||
if (allowedSchemas.isEmpty()) return true // No schemas required | ||
val schema = getSchema(token) | ||
|
||
return allowedSchemas.contains(schema) | ||
} | ||
|
||
suspend fun authorizeToken(token: String, requiredScopes: List<Scope>): Boolean { | ||
if (requiredScopes.isEmpty()) return true // No scopes required | ||
|
||
val schema = getSchema(token) | ||
val tokenStr = token.removePrefix(schema.schema) | ||
|
||
val scopes = when (schema) { | ||
TokenType.BEARER -> getScopesForUserToken(tokenStr) | ||
TokenType.APP -> getScopesForAppToken(tokenStr) | ||
TokenType.INTERNAL -> getScopesForInternalToken() | ||
else -> return false | ||
} | ||
|
||
return scopes.containsAll(requiredScopes) | ||
} | ||
|
||
|
||
// Authentication based on token type | ||
private suspend fun authenticateUserToken(token: String): Boolean { | ||
val session = sessionService.getSession(token) ?: return false | ||
|
||
return !session.expiresAt.isExpiredTtl() | ||
} | ||
|
||
private suspend fun authenticateAppToken(token: String): Boolean { | ||
val key = apiKeyService.getKey(token) ?: return false | ||
|
||
return !key.blocked | ||
} | ||
|
||
private fun authenticateInternalToken(token: String): Boolean { | ||
return Config.SECRET_DISCAL_API_KEY.getString() == token | ||
} | ||
|
||
// Fetching scopes for tokens | ||
private suspend fun getScopesForUserToken(token: String): List<Scope> { | ||
return sessionService.getSession(token)?.scopes ?: emptyList() | ||
} | ||
|
||
private suspend fun getScopesForAppToken(token: String): List<Scope> { | ||
return apiKeyService.getKey(token)?.scopes ?: emptyList() | ||
} | ||
|
||
private fun getScopesForInternalToken(): List<Scope> = Scope.entries.toList() | ||
|
||
// Various other stuff | ||
private fun getSchema(token: String): TokenType { | ||
return when { | ||
token.startsWith(TokenType.BEARER.schema) -> TokenType.BEARER | ||
token.startsWith(TokenType.APP.schema) -> TokenType.APP | ||
token.startsWith(TokenType.INTERNAL.schema) -> TokenType.INTERNAL | ||
else -> TokenType.NONE | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
cam/src/main/kotlin/org/dreamexposure/discal/cam/security/SecurityWebFilter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package org.dreamexposure.discal.cam.security | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import kotlinx.coroutines.reactive.awaitFirst | ||
import kotlinx.coroutines.reactive.awaitFirstOrNull | ||
import kotlinx.coroutines.reactor.mono | ||
import org.dreamexposure.discal.cam.business.SecurityService | ||
import org.dreamexposure.discal.core.annotations.SecurityRequirement | ||
import org.dreamexposure.discal.core.extensions.spring.writeJsonString | ||
import org.dreamexposure.discal.core.`object`.rest.ErrorResponse | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.method.HandlerMethod | ||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping | ||
import org.springframework.web.server.ServerWebExchange | ||
import org.springframework.web.server.WebFilter | ||
import org.springframework.web.server.WebFilterChain | ||
import reactor.core.publisher.Mono | ||
|
||
@Component | ||
class SecurityWebFilter( | ||
private val securityService: SecurityService, | ||
private val handlerMapping: RequestMappingHandlerMapping, | ||
private val objectMapper: ObjectMapper, | ||
) : WebFilter { | ||
|
||
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { | ||
return mono { | ||
doSecurityFilter(exchange, chain) | ||
}.then(chain.filter(exchange)) | ||
} | ||
|
||
suspend fun doSecurityFilter(exchange: ServerWebExchange, chain: WebFilterChain) { | ||
val handlerMethod = handlerMapping.getHandler(exchange) | ||
.cast(HandlerMethod::class.java) | ||
.awaitFirst() | ||
|
||
if (!handlerMethod.hasMethodAnnotation(SecurityRequirement::class.java)) { | ||
throw IllegalStateException("No SecurityRequirement annotation!") | ||
} | ||
|
||
val authAnnotation = handlerMethod.getMethodAnnotation(SecurityRequirement::class.java)!! | ||
val authHeader = exchange.request.headers.getOrEmpty("Authorization").firstOrNull() | ||
|
||
|
||
if (authAnnotation.disableSecurity) return | ||
|
||
if (authHeader == null) { | ||
exchange.response.statusCode = HttpStatus.UNAUTHORIZED | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("Missing Authorization header")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
if (authHeader.equals("teapot", ignoreCase = true)) { | ||
exchange.response.statusCode = HttpStatus.I_AM_A_TEAPOT | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("I'm a teapot")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
if (!securityService.authenticateToken(authHeader)) { | ||
exchange.response.statusCode = HttpStatus.UNAUTHORIZED | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("Unauthenticated")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
if (!securityService.validateTokenSchema(authHeader, authAnnotation.schemas.toList())) { | ||
exchange.response.statusCode = HttpStatus.UNAUTHORIZED | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("Unsupported schema")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
if (!securityService.authorizeToken(authHeader, authAnnotation.scopes.toList())) { | ||
exchange.response.statusCode = HttpStatus.FORBIDDEN | ||
exchange.response.writeJsonString( | ||
objectMapper.writeValueAsString(ErrorResponse("Access denied")) | ||
).awaitFirstOrNull() | ||
return | ||
} | ||
|
||
// If we made it to the end, everything is good to go. | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
core/src/main/kotlin/org/dreamexposure/discal/core/annotations/SecurityRequirement.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.dreamexposure.discal.core.annotations | ||
|
||
import org.dreamexposure.discal.core.`object`.new.security.Scope | ||
import org.dreamexposure.discal.core.`object`.new.security.TokenType | ||
|
||
@Retention(AnnotationRetention.RUNTIME) | ||
@Target(AnnotationTarget.FUNCTION) | ||
annotation class SecurityRequirement( | ||
val schemas: Array<TokenType> = [], // Default to allowing any token kind | ||
val scopes: Array<Scope>, | ||
val disableSecurity: Boolean = false, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,5 @@ data class ApiData( | |
val apiKey: String, | ||
val blocked: Boolean, | ||
val timeIssued: Long, | ||
val scopes: String, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
core/src/main/kotlin/org/dreamexposure/discal/core/extensions/spring/ServerHttpResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.dreamexposure.discal.core.extensions.spring | ||
|
||
import org.springframework.http.MediaType | ||
import org.springframework.http.server.reactive.ServerHttpResponse | ||
import reactor.core.publisher.Mono | ||
|
||
fun ServerHttpResponse.writeJsonString(json: String): Mono<Void> { | ||
val factory = bufferFactory() | ||
val buffer = factory.wrap(json.toByteArray()) | ||
|
||
headers.contentType = MediaType.APPLICATION_JSON | ||
return writeWith(Mono.just(buffer)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
core/src/main/kotlin/org/dreamexposure/discal/core/object/new/security/Scope.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package org.dreamexposure.discal.core.`object`.new.security | ||
|
||
enum class Scope { | ||
CALENDAR_TOKEN_READ, | ||
|
||
OAUTH2_DISCORD, | ||
; | ||
|
||
companion object { | ||
fun defaultWebsiteLoginScopes() = listOf( | ||
OAUTH2_DISCORD, | ||
) | ||
|
||
fun defaultBasicAppScopes() = listOf<Scope>() | ||
} | ||
} |
Oops, something went wrong.