Aether provides a modular authentication system that works across REST and gRPC.
The AuthMiddleware orchestrates the authentication process. It iterates through a list of registered AuthProviders.
class AuthMiddleware(
private val providers: List<AuthProvider>
) : Middleware
Implement this interface to support different authentication schemes.
interface AuthProvider {
suspend fun authenticate(exchange: Exchange): AuthResult
}
sealed class AuthResult {
data class Success(val principal: Principal) : AuthResult()
object Failure : AuthResult()
object Skipped : AuthResult() // Provider doesn't handle this request
}
Represents the authenticated entity (usually a user).
interface Principal {
val id: String
val name: String
val roles: Set<String>
}
BasicAuthProviderImplements HTTP Basic Auth.
BasicAuthProvider { username, password ->
// Verify credentials against DB
if (username == "admin" && password == "secret") {
UserPrincipal(id = "1", name = "admin", roles = setOf("admin"))
} else {
null
}
}
BearerAuthProviderImplements Bearer Token authentication (e.g., JWT).
BearerAuthProvider { token ->
// Verify JWT
jwtService.verify(token)
}
SessionAuthProviderAuthenticates a user based on their active session.
SessionAuthProvider { session ->
val userId = session["userId"]
if (userId != null) {
UserPrincipal(id = userId, ...)
} else {
null
}
}
Aether provides a built-in RBAC system allowing granular control over user permissions using Groups and Permissions.
Your user entity should extend AbstractUser to gain built-in RBAC capabilities.
class User : AbstractUser<User>() {
// ... custom fields
}
You can check if a user has a specific permission, either directly assigned or inherited from a group.
if (user.hasPermission("blog.create_post")) {
// Allowed
}
app.action_resource).RBAC is often used in checking access rights within handlers or middleware after authentication has established the principal.
UserContext propagates the authenticated principal through Kotlin's coroutine context, enabling unified authentication for REST and gRPC.
// In any suspend function (REST handler, gRPC method, etc.)
suspend fun handleRequest() {
// Get current user (nullable)
val user: Principal? = currentUser()
// Require authenticated user (throws if not authenticated)
val user: Principal = requireUser()
// Check authentication status
if (isAuthenticated()) {
// User is logged in
}
// Check roles
if (hasRole("admin")) {
// User has admin role
}
}
The AuthMiddleware automatically wraps authenticated requests with UserContext:
// Internal implementation
when (val result = provider.authenticate(exchange)) {
is AuthResult.Success -> {
withContext(UserContext(result.principal)) {
next() // Handler runs with UserContext available
}
}
// ...
}
This allows any code running within the request to access the authenticated user without passing it explicitly.
AuthStrategy provides protocol-agnostic authentication that works for both REST (via headers) and gRPC (via metadata).
interface AuthStrategy {
suspend fun authenticate(credential: String): AuthResult
suspend fun authenticateOrNoCredentials(credential: String?): AuthResult
}
For JWT and other bearer tokens:
val strategy = BearerTokenStrategy { token ->
// Verify and decode the token
jwtService.verify(token)?.let { claims ->
UserPrincipal(
id = claims.subject,
name = claims["name"] as String,
roles = claims["roles"] as Set<String>
)
}
}
// Extract token from Authorization header
val token = strategy.extractToken("Bearer eyJhbGc...") // Returns "eyJhbGc..."
// Authenticate from header directly
val result = strategy.authenticateFromHeader("Bearer eyJhbGc...")
For API key authentication:
val strategy = ApiKeyStrategy { apiKey ->
apiKeyRepository.findByKey(apiKey)?.let { key ->
ApiKeyPrincipal(
id = key.id,
name = key.name,
roles = key.scopes.toSet()
)
}
}
For HTTP Basic authentication:
val strategy = BasicAuthStrategy { username, password ->
userService.verifyCredentials(username, password)?.let { user ->
UserPrincipal(id = user.id, name = user.name, roles = user.roles)
}
}
Combines multiple strategies (tries each in order):
val strategy = CompositeAuthStrategy(
listOf(
bearerStrategy, // Try JWT first
apiKeyStrategy, // Then API key
basicStrategy // Finally basic auth
)
)
// Returns Success if any strategy succeeds
// Returns NoCredentials if all return NoCredentials
// Returns Failure if any returns Failure
grpc {
intercept { call, next ->
val authHeader = call.metadata["authorization"]
val result = bearerStrategy.authenticateFromHeader(authHeader)
when (result) {
is AuthResult.Success -> {
withContext(UserContext(result.principal)) {
next(call)
}
}
is AuthResult.NoCredentials -> {
// Allow unauthenticated access or throw
next(call)
}
is AuthResult.Failure -> {
throw GrpcException.unauthenticated("Invalid credentials")
}
}
}
}
