OAuth with KeyCloak in Ktor :它应该像这样工作吗?

tet*_*oba 5 authentication oauth-2.0 kotlin keycloak ktor

我尝试在 Ktor Web 服务器中通过 Keycloak 设置有效的 Oauth2 授权。预期的流程是从 Web 服务器向 keycloak 发送请求并登录给定的 UI,然后 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}")
         }
     }
    
    Run Code Online (Sandbox Code Playgroud)
  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" }
     }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 我用内置的 HTTP 请求创建了这个 /token 路由,这个路由设法获取了令牌,但感觉就像是黑客攻击。

    get("/token"){
     var grantType = "authorization_code"
     val code = call.request.queryParameters["code"]
     val requestBody = "grant_type=${grantType}&" +
             "client_id=${keycloakProvider.clientId}&" +
             "client_secret=${keycloakProvider.clientSecret}&" +
             "code=${code.toString()}&" +
             "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()}")
    }
    
    Run Code Online (Sandbox Code Playgroud)

TL;DR:我可以通过 Keycloak 登录,但是尝试获取 access_token 却给了我 401。ktor 中的身份验证函数也应该处理这个问题吗?

Ale*_*man 6

第一个问题的答案:如果该路由对应于 lambda 中返回的重定向 URI,则它将用于两者urlProvider

整体流程如下:

  1. 用户authenticate在浏览器中打开 http://localhost:7777/login ( 下的任何路由)
  2. Ktor 进行重定向以authorizeUrl传递必要的参数
  3. 用户通过 Keycloak UI 登录
  4. Keycloak 将用户重定向到urlProviderlambda 提供的重定向 URI,传递获取访问令牌所需的参数
  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)
}
Run Code Online (Sandbox Code Playgroud)