Authentication and authorization plugin for Ktor server applications
—
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.
HTTP Digest authentication implementation that provides more secure username/password authentication than Basic auth by avoiding transmission of plaintext passwords.
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)
}realm: Authentication realm for WWW-Authenticate headeralgorithmName: Message digest algorithm (typically "MD5")nonceManager: Manages nonce generation and validationdigestProvider: Function that provides password digest for username/realmvalidate: Validates digest credentials and returns principalskipWhen: Predicate to skip authentication for certain callsinstall(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}")
}
}
}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
}
}class DigestAuthenticationProvider internal constructor(
config: Config
) : AuthenticationProvider(config)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?
)typealias DigestProviderFunction = suspend (String, String) -> ByteArray?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
): ByteArrayinterface 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
}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)
}
}Complete OAuth 1.0a implementation with signature generation and verification.
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()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")
}
}
}
}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()sealed class OAuth1aException : Exception() {
class MissingTokenException(message: String) : OAuth1aException()
}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
}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()
)
}
}class SecureNonceManager : NonceManager {
private val secureRandom = SecureRandom()
override suspend fun newNonce(): String {
val bytes = ByteArray(16)
secureRandom.nextBytes(bytes)
return bytes.encodeBase64()
}
}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]
}
}
}
}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)
}
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-server-auth-jvm