OAuth с KeyCloak в Ktor: это должно так работать?

#kotlin #authentication #oauth-2.0 #keycloak #ktor

#kotlin #аутентификация #oauth-2.0 #keycloak #ktor

Вопрос:

Я попытался настроить рабочую авторизацию Oauth2 через Keycloak на веб-сервере Ktor. Ожидаемым потоком будет отправка запроса с веб-сервера на keycloak и вход в данный пользовательский интерфейс, затем Keycloak отправляет обратно код, который можно использовать для получения токена. Как здесь

Сначала я сделал это на основе примеров в документации Ktor. Oauth Все работало нормально, пока не дошло до того, что я должен был получить токен, затем он просто выдал мне HTTP статус 401. Несмотря на то, что команда curl работает правильно. Затем я попробовал пример проекта, который я нашел на GitHub, мне удалось заставить его работать, создав свой собственный HTTP-запрос и отправив его на сервер Keycloak для получения токена, но должен ли он работать так?

У меня есть несколько вопросов по этому поводу.

  1. Предполагается ли, что эта функция обрабатывает как авторизацию, так и получение токена?
      authenticate(keycloakOAuth) {
         get("/oauth") {
             val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
    
             call.respondText("Access Token = ${principal?.accessToken}")
         }
     }
     
  2. Я думаю, что моя конфигурация правильная, поскольку я могу получить авторизацию, но не токен.
     const val KEYCLOAK_ADDRESS = "**"
    
    val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings(
    name = "keycloak",
    authorizeUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/auth",
    accessTokenUrl = "$KEYCLOAK_ADDRESS/auth/realms/production/protocol/openid-connect/token",
    clientId = "**",
    clientSecret = "**",
    accessTokenRequiresBasicAuth = false,
    requestMethod = HttpMethod.Post, // must POST to token endpoint
    defaultScopes = listOf("roles")
    )
    const val keycloakOAuth = "keycloakOAuth"
    
     install(Authentication) {
         oauth(keycloakOAuth) {
         client = HttpClient(Apache)
         providerLookup = { keycloakProvider }
         urlProvider = { "http://localhost:8080/token" }
     }
    }
     
  3. Существует маршрут /token, который я создал с помощью встроенного HTTP-запроса, этому удается получить токен, но это похоже на взлом.
     get("/token"){
     var grantType = "authorization_code"
     val code = call.request.queryParameters["code"]
     val requestBody = "grant_type=${grantType}amp;"  
             "client_id=${keycloakProvider.clientId}amp;"  
             "client_secret=${keycloakProvider.clientSecret}amp;"  
             "code=${code.toString()}amp;"  
             "redirect_uri=http://localhost:8080/token"
    
     val tokenResponse = httpClient.post<HttpResponse>(keycloakProvider.accessTokenUrl) {
         headers {
             append("Content-Type","application/x-www-form-urlencoded")
         }
         body = requestBody
     }
     call.respondText("Access Token = ${tokenResponse.readText()}")
    }
     

TL; DR: Я могу войти в систему через Keycloak нормально, но попытка получить access_token дает мне 401. Должна ли функция аутентификации в ktor обрабатывать и это?

Ответ №1:

Ответ на ваш первый вопрос: он будет использоваться для обоих, если этот маршрут соответствует URI перенаправления, возвращенному в urlProvider лямбда.

Общий процесс выглядит следующим образом:

  1. Пользователь открывает http://localhost:7777/login (любой маршрут в разделе authenticate ) в браузере
  2. Ktor перенаправляет на authorizeUrl передачу необходимых параметров
  3. Пользователь входит в систему через пользовательский интерфейс Keycloak
  4. Keycloak перенаправляет пользователя на URI перенаправления, предоставляемый urlProvider параметрами передачи lambda, необходимыми для получения токена доступа
  5. Ktor отправляет запрос на URL-адрес токена и выполняет обработчик маршрутизации, соответствующий URI перенаправления (http://localhost:7777/callback в приведенном примере).
  6. В обработчике у вас есть доступ к OAuthAccessTokenResponse объекту, который имеет свойства для токена доступа, токена обновления и любых других параметров, возвращаемых из Keycloak.

Вот код для рабочего примера:

 val provider = OAuthServerSettings.OAuth2ServerSettings(
    name = "keycloak",
    authorizeUrl = "http://localhost:8080/auth/realms/master/protocol/openid-connect/auth",
    accessTokenUrl = "http://localhost:8080/auth/realms/$realm/protocol/openid-connect/token",
    clientId = clientId,
    clientSecret = clientSecret,
    requestMethod = HttpMethod.Post // The GET HTTP method is not supported for this provider
)

fun main() {
    embeddedServer(Netty, port = 7777) {
        install(Authentication) {
            oauth("keycloak_oauth") {
                client = HttpClient(Apache)
                providerLookup = { provider }
                // The URL should match "Valid Redirect URIs" pattern in Keycloak client settings
                urlProvider = { "http://localhost:7777/callback" }
            }
        }

        routing {
            authenticate("keycloak_oauth") {
                get("login") {
                    // The user will be redirected to authorizeUrl first
                }

                route("/callback") {
                    // This handler will be executed after making a request to a provider's token URL.
                    handle {
                        val principal = call.authentication.principal<OAuthAccessTokenResponse>()

                        if (principal != null) {
                            val response = principal as OAuthAccessTokenResponse.OAuth2
                            call.respondText { "Access token: ${response.accessToken}" }
                        } else {
                            call.respondText { "NO principal" }
                        }
                    }
                }
            }
        }
    }.start(wait = false)
}