CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-server-auth-jvm

Authentication and authorization plugin for Ktor server applications

Pending
Overview
Eval results
Files

jvm-features.mddocs/

JVM-Specific Authentication Features

JVM-only authentication features including Digest authentication and comprehensive OAuth 1.0a support. These features require JVM-specific cryptographic operations and are not available on other platforms.

Digest Authentication

HTTP Digest authentication implementation that provides more secure username/password authentication than Basic auth by avoiding transmission of plaintext passwords.

Configuration

fun AuthenticationConfig.digest(
    name: String? = null,
    configure: DigestAuthenticationProvider.Config.() -> Unit
)

@KtorDsl
class DigestAuthenticationProvider.Config(name: String?) : AuthenticationProvider.Config(name) {
    var realm: String
    var algorithmName: String
    var nonceManager: NonceManager
    fun digestProvider(digest: DigestProviderFunction)
    fun validate(body: suspend ApplicationCall.(DigestCredential) -> Any?)
    fun skipWhen(predicate: ApplicationCallPredicate)
}

Configuration Properties

  • realm: Authentication realm for WWW-Authenticate header
  • algorithmName: Message digest algorithm (typically "MD5")
  • nonceManager: Manages nonce generation and validation

Configuration Functions

  • digestProvider: Function that provides password digest for username/realm
  • validate: Validates digest credentials and returns principal
  • skipWhen: Predicate to skip authentication for certain calls

Basic Digest Authentication

install(Authentication) {
    digest("auth-digest") {
        realm = "Protected Area"
        algorithmName = "MD5"
        digestProvider { userName, realm ->
            // Provide digest for username in realm
            userService.getPasswordDigest(userName, realm)
        }
        validate { credentials ->
            // Additional validation after digest verification
            UserIdPrincipal(credentials.userName)
        }
    }
}

routing {
    authenticate("auth-digest") {
        get("/secure") {
            val principal = call.principal<UserIdPrincipal>()
            call.respond("Access granted to ${principal?.name}")
        }
    }
}

Advanced Digest Configuration

digest("custom-digest") {
    realm = "Secure API"
    algorithmName = "SHA-256" // Custom algorithm (client support varies)
    nonceManager = CustomNonceManager() // Custom nonce management
    
    digestProvider { userName, realm ->
        // Calculate HA1 = MD5(username:realm:password)
        val user = userService.getUser(userName)
        if (user != null) {
            MessageDigest.getInstance("MD5").digest(
                "$userName:$realm:${user.password}".toByteArray()
            )
        } else null
    }
    
    validate { credentials ->
        // Additional business logic validation
        val user = userService.getUser(credentials.userName)
        if (user?.isActive == true) {
            UserIdPrincipal(credentials.userName)
        } else null
    }
}

Digest Authentication API

Provider Class

class DigestAuthenticationProvider internal constructor(
    config: Config
) : AuthenticationProvider(config)

Credential Types

data class DigestCredential(
    val realm: String,
    val userName: String,
    val digestUri: String,
    val nonce: String,
    val opaque: String?,
    val nonceCount: String?,
    val algorithm: String?,
    val response: String,
    val cnonce: String?,
    val qop: String?
)

Function Types

typealias DigestProviderFunction = suspend (String, String) -> ByteArray?

Credential Extraction and Verification

fun ApplicationCall.digestAuthenticationCredentials(): DigestCredential?

fun HttpAuthHeader.Parameterized.toDigestCredential(): DigestCredential?

suspend fun DigestCredential.verifier(
    method: HttpMethod,
    digester: MessageDigest,
    userNameRealmPasswordDigest: suspend (String, String) -> ByteArray?
): Boolean

suspend fun DigestCredential.expectedDigest(
    method: HttpMethod,
    digester: MessageDigest,
    userNameRealmPasswordDigest: ByteArray
): ByteArray

Nonce Management

Nonce Manager Interface

interface NonceManager {
    suspend fun newNonce(): String
    suspend fun verifyNonce(nonce: String): Boolean
}

object GenerateOnlyNonceManager : NonceManager {
    override suspend fun newNonce(): String
    override suspend fun verifyNonce(nonce: String): Boolean
}

Custom Nonce Manager

class DatabaseNonceManager : NonceManager {
    override suspend fun newNonce(): String {
        val nonce = generateSecureNonce()
        // Store nonce in database with expiration
        nonceRepository.store(nonce, Clock.System.now() + 5.minutes)
        return nonce
    }
    
    override suspend fun verifyNonce(nonce: String): Boolean {
        return nonceRepository.isValid(nonce)
    }
}

OAuth 1.0a Support (JVM Only)

Complete OAuth 1.0a implementation with signature generation and verification.

OAuth 1.0a Configuration

OAuth 1.0a server settings for legacy providers:

data class OAuth1aServerSettings(
    val name: String,
    val requestTokenUrl: String,
    val authorizeUrl: String,
    val accessTokenUrl: String,
    val consumerKey: String,
    val consumerSecret: String
) : OAuthServerSettings()

OAuth 1.0a Flow Implementation

val twitterOAuth1a = OAuthServerSettings.OAuth1aServerSettings(
    name = "twitter",
    requestTokenUrl = "https://api.twitter.com/oauth/request_token",
    authorizeUrl = "https://api.twitter.com/oauth/authorize", 
    accessTokenUrl = "https://api.twitter.com/oauth/access_token",
    consumerKey = System.getenv("TWITTER_CONSUMER_KEY"),
    consumerSecret = System.getenv("TWITTER_CONSUMER_SECRET")
)

install(Authentication) {
    oauth("twitter-oauth1a") {
        client = HttpClient()
        providerLookup = { twitterOAuth1a }
        urlProvider = { url -> redirectUrl(url, "/auth/twitter/callback") }
    }
}

routing {
    authenticate("twitter-oauth1a") {
        get("/auth/twitter") {
            // Initiates OAuth 1.0a flow
        }
        
        get("/auth/twitter/callback") {
            val principal = call.principal<OAuthAccessTokenResponse.OAuth1a>()
            if (principal != null) {
                // Use OAuth 1.0a tokens to make API calls
                val userProfile = getTwitterProfile(principal.token, principal.tokenSecret)
                call.sessions.set(UserSession(userProfile.id))
                call.respondRedirect("/dashboard")
            } else {
                call.respondRedirect("/login?error=oauth")
            }
        }
    }
}

OAuth 1.0a Token Types

data class OAuthAccessTokenResponse.OAuth1a(
    val token: String,
    val tokenSecret: String,
    val extraParameters: Parameters = Parameters.Empty
) : OAuthAccessTokenResponse()

data class OAuthCallback.TokenPair(
    val token: String, 
    val tokenSecret: String
) : OAuthCallback()

OAuth 1.0a Exceptions

sealed class OAuth1aException : Exception() {
    class MissingTokenException(message: String) : OAuth1aException()
}

Full OAuth Procedure Support

The JVM platform provides complete OAuth implementation including the OAuth procedure class:

class OAuthProcedure {
    suspend fun requestToken(): OAuthCallback.TokenPair
    suspend fun authorizeUrl(requestToken: OAuthCallback.TokenPair): String
    suspend fun accessToken(authorizedToken: OAuthCallback.TokenPair): OAuthAccessTokenResponse.OAuth1a
}

Platform-Specific Cryptographic Features

Digest Algorithm Support

digest("sha256-digest") {
    algorithmName = "SHA-256" // JVM supports multiple algorithms
    realm = "SHA-256 Protected"
    digestProvider { userName, realm ->
        MessageDigest.getInstance("SHA-256").digest(
            "$userName:$realm:${getPassword(userName)}".toByteArray()
        )
    }
}

Secure Random Generation

class SecureNonceManager : NonceManager {
    private val secureRandom = SecureRandom()
    
    override suspend fun newNonce(): String {
        val bytes = ByteArray(16)
        secureRandom.nextBytes(bytes)
        return bytes.encodeBase64()
    }
}

Integration Examples

Digest with Database

digest("database-digest") {
    realm = "Database Protected"
    digestProvider { userName, realm ->
        transaction {
            Users.select { Users.username eq userName }
                .singleOrNull()
                ?.let { user ->
                    // Pre-computed digest stored in database
                    user[Users.digestHA1]
                }
        }
    }
}

OAuth 1.0a with Custom Signature

class CustomOAuth1aClient(
    private val consumerKey: String,
    private val consumerSecret: String
) {
    suspend fun makeSignedRequest(
        url: String,
        token: String,
        tokenSecret: String,
        parameters: Map<String, String> = emptyMap()
    ): HttpResponse {
        val oauthParams = generateOAuthParams(url, parameters, token)
        val signature = generateSignature(url, oauthParams, tokenSecret)
        
        return httpClient.get(url) {
            header("Authorization", buildOAuthHeader(oauthParams, signature))
            parameters.forEach { (key, value) ->
                parameter(key, value)
            }
        }
    }
}

Security Considerations

Digest Authentication Security

  • More secure than Basic auth (no plaintext password transmission)
  • Vulnerable to rainbow table attacks if using weak hashing
  • Requires secure nonce management to prevent replay attacks
  • Client support may be limited compared to Basic/Bearer auth
  • Use strong digest algorithms when client support allows

OAuth 1.0a Security

  • Requires proper signature generation and verification
  • More complex than OAuth 2.0 but provides built-in request signing
  • Protect consumer secrets and token secrets securely
  • Implement proper timestamp and nonce validation
  • Handle signature verification failures gracefully

Install with Tessl CLI

npx tessl i tessl/maven-io-ktor--ktor-server-auth-jvm

docs

basic-auth.md

bearer-auth.md

form-session-auth.md

index.md

jvm-features.md

oauth.md

tile.json