#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 для получения токена, но должен ли он работать так?
У меня есть несколько вопросов по этому поводу.
- Предполагается ли, что эта функция обрабатывает как авторизацию, так и получение токена?
authenticate(keycloakOAuth) { get("/oauth") { val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>() call.respondText("Access Token = ${principal?.accessToken}") } }
- Я думаю, что моя конфигурация правильная, поскольку я могу получить авторизацию, но не токен.
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" } } }
- Существует маршрут /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
лямбда.
Общий процесс выглядит следующим образом:
- Пользователь открывает http://localhost:7777/login (любой маршрут в разделе
authenticate
) в браузере - Ktor перенаправляет на
authorizeUrl
передачу необходимых параметров - Пользователь входит в систему через пользовательский интерфейс Keycloak
- Keycloak перенаправляет пользователя на URI перенаправления, предоставляемый
urlProvider
параметрами передачи lambda, необходимыми для получения токена доступа - Ktor отправляет запрос на URL-адрес токена и выполняет обработчик маршрутизации, соответствующий URI перенаправления (http://localhost:7777/callback в приведенном примере).
- В обработчике у вас есть доступ к
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)
}