Authentication and authorization plugin for Ktor server applications
—
OAuth 1.0a and OAuth 2.0 authentication support for integrating with third-party authentication providers like Google, GitHub, Twitter, and custom OAuth servers. This includes authorization code flows, token validation, and user profile retrieval.
OAuth 2.0 authorization code flow implementation for modern OAuth providers.
fun AuthenticationConfig.oauth(
name: String? = null,
configure: OAuthAuthenticationProvider.Config.() -> Unit
)
@KtorDsl
class OAuthAuthenticationProvider.Config(name: String?) : AuthenticationProvider.Config(name) {
lateinit var client: HttpClient
lateinit var providerLookup: ApplicationCall.() -> OAuthServerSettings?
lateinit var urlProvider: ApplicationCall.(OAuthServerSettings) -> String
fun skipWhen(predicate: ApplicationCallPredicate)
}enum class OAuthVersion {
V10a, V20
}sealed class OAuthServerSettings {
data class OAuth1aServerSettings(
val name: String,
val requestTokenUrl: String,
val authorizeUrl: String,
val accessTokenUrl: String,
val consumerKey: String,
val consumerSecret: String
) : OAuthServerSettings()
data class OAuth2ServerSettings(
val name: String,
val authorizeUrl: String,
val accessTokenUrl: String,
val requestMethod: HttpMethod = HttpMethod.Post,
val clientId: String,
val clientSecret: String,
val defaultScopes: List<String> = emptyList(),
val extraAuthParameters: List<Pair<String, String>> = emptyList(),
val extraTokenParameters: List<Pair<String, String>> = emptyList(),
val accessTokenRequiresBasicAuth: Boolean = false,
val onStateCreated: ((call: ApplicationCall, state: String) -> Unit)? = null,
val authorizeUrlInterceptor: ((URLBuilder, ApplicationCall) -> Unit)? = null,
val passParamsInURL: Boolean = false,
val nonceManager: NonceManager = GenerateOnlyNonceManager
) : OAuthServerSettings()
}val googleOAuthProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://www.googleapis.com/oauth2/v4/token",
clientId = "your-google-client-id",
clientSecret = "your-google-client-secret",
defaultScopes = listOf("openid", "profile", "email")
)
install(Authentication) {
oauth("auth-oauth-google") {
client = HttpClient(Apache)
providerLookup = { googleOAuthProvider }
urlProvider = { url ->
redirectUrl(url, "/callback")
}
}
}
routing {
authenticate("auth-oauth-google") {
get("/login") {
// Redirects to OAuth provider
}
get("/callback") {
val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
if (principal != null) {
// Use access token to get user info
val userInfo = getUserInfo(principal.accessToken)
call.sessions.set(UserSession(userInfo.id))
call.respondRedirect("/dashboard")
} else {
call.respondRedirect("/login?error=oauth")
}
}
}
}OAuth 1.0a implementation for legacy providers like Twitter API v1.1.
data class OAuth1aServerSettings(
val name: String,
val requestTokenUrl: String,
val authorizeUrl: String,
val accessTokenUrl: String,
val consumerKey: String,
val consumerSecret: String
) : OAuthServerSettings()val twitterOAuthProvider = 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 = "your-twitter-consumer-key",
consumerSecret = "your-twitter-consumer-secret"
)
install(Authentication) {
oauth("auth-oauth-twitter") {
client = HttpClient(Apache)
providerLookup = { twitterOAuthProvider }
urlProvider = { url ->
redirectUrl(url, "/twitter-callback")
}
}
}sealed class OAuthCallback {
data class TokenSingle(val token: String, val state: String) : OAuthCallback()
data class TokenPair(val token: String, val tokenSecret: String) : OAuthCallback()
data class Error(val error: String, val errorDescription: String?) : OAuthCallback()
}sealed class OAuthAccessTokenResponse {
data class OAuth2(
val accessToken: String,
val tokenType: String,
val expiresIn: Long?,
val refreshToken: String?,
val extraParameters: Parameters = Parameters.Empty
) : OAuthAccessTokenResponse()
data class OAuth1a(
val token: String,
val tokenSecret: String,
val extraParameters: Parameters = Parameters.Empty
) : OAuthAccessTokenResponse()
}oauth("custom-state") {
client = HttpClient(CIO)
providerLookup = {
googleOAuthProvider.copy(
onStateCreated = { call, state ->
// Store state with additional context
call.sessions.set(OAuthState(state, call.request.uri))
}
)
}
urlProvider = { url -> redirectUrl(url, "/callback") }
}install(Authentication) {
oauth("google") {
client = httpClient
providerLookup = { googleProvider }
urlProvider = { redirectUrl(it, "/auth/google/callback") }
}
oauth("github") {
client = httpClient
providerLookup = { githubProvider }
urlProvider = { redirectUrl(it, "/auth/github/callback") }
}
}
routing {
get("/auth/{provider}") {
val provider = call.parameters["provider"]
authenticate(provider) {
// Redirect to OAuth provider
}
}
}suspend fun refreshOAuth2Token(refreshToken: String): OAuthAccessTokenResponse.OAuth2? {
return try {
httpClient.submitForm(
url = "https://oauth2.googleapis.com/token",
formParameters = parameters {
append("grant_type", "refresh_token")
append("refresh_token", refreshToken)
append("client_id", clientId)
append("client_secret", clientSecret)
}
).body()
} catch (e: Exception) {
null
}
}suspend fun verifyWithOAuth2(
client: HttpClient,
settings: OAuthServerSettings.OAuth2ServerSettings,
callbackResponse: OAuthCallback.TokenSingle,
configure: HttpRequestBuilder.() -> Unit = {}
): OAuthAccessTokenResponse.OAuth2
suspend fun verifyWithOAuth2(
credential: UserPasswordCredential,
client: HttpClient,
settings: OAuthServerSettings.OAuth2ServerSettings
): OAuthAccessTokenResponse.OAuth2suspend fun authenticateWithResourceOwner(
username: String,
password: String
): OAuthAccessTokenResponse.OAuth2? {
return verifyWithOAuth2(
client = httpClient,
settings = oauth2Settings,
callbackResponse = OAuthCallback.TokenSingle(
token = "", // Not used in resource owner flow
state = ""
)
) {
parameter("grant_type", "password")
parameter("username", username)
parameter("password", password)
}
}sealed class OAuth2Exception : Exception() {
class InvalidGrant(message: String) : OAuth2Exception()
class InvalidNonce(message: String) : OAuth2Exception()
class MissingAccessToken(message: String) : OAuth2Exception()
class UnsupportedGrantType(message: String) : OAuth2Exception()
class UnknownException(message: String, cause: Throwable?) : OAuth2Exception()
}
sealed class OAuth1aException : Exception() {
class MissingTokenException(message: String) : OAuth1aException()
}object OAuthGrantTypes {
const val AuthorizationCode = "authorization_code"
const val RefreshToken = "refresh_token"
const val Password = "password"
const val ClientCredentials = "client_credentials"
}object OAuth2RequestParameters {
const val ClientId = "client_id"
const val ClientSecret = "client_secret"
const val Code = "code"
const val GrantType = "grant_type"
const val RedirectUri = "redirect_uri"
const val ResponseType = "response_type"
const val Scope = "scope"
const val State = "state"
}
object OAuth2ResponseParameters {
const val AccessToken = "access_token"
const val TokenType = "token_type"
const val ExpiresIn = "expires_in"
const val RefreshToken = "refresh_token"
const val Scope = "scope"
const val State = "state"
const val Error = "error"
const val ErrorDescription = "error_description"
}Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-server-auth-jvm