Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OAuth with KeyCloak in Ktor : Is it supposed to work like this?

I tried to set up a working Oauth2 authorization via Keycloak in a Ktor web server. The expected flow would be sending a request from the web server to keycloak and logging in on the given UI, then Keycloak sends back a code that can be used to receive a token. Like here

First I did it based on the examples in Ktor's documentation. Oauth It worked fine until it got to the point where I had to receive the token, then it just gave me HTTP status 401. Even though the curl command works properly. Then I tried an example project I found on GitHub , I managed to make it work by building my own HTTP request and sending it to the Keycloak server to receive the token, but is it supposed to work like this?

I have multiple questions regarding this.

  1. Is this function supposed to handle both authorization and getting the token?

     authenticate(keycloakOAuth) {
         get("/oauth") {
             val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
    
             call.respondText("Access Token = ${principal?.accessToken}")
         }
     }
    
  2. I think my configuration is correct, since I can receive the authorization, just not the token.

    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. There is this /token route I made with a built HTTP request, this one manages to get the token, but it feels like a hack.

    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()}")
    }
    

TL;DR: I can log in via Keycloak fine, but trying to get an access_token gives me 401. Is the authenticate function in ktor supposed to handle that too?

like image 908
tetsuroba Avatar asked Feb 25 '21 08:02

tetsuroba


People also ask

Does Keycloak use OAuth?

Keycloak is Open Source Identity and Access Management Server, which is a OAuth2 and OpenID Connect(OIDC) protocol complaint.

Why OAuth should not be used for authentication?

Let's start with the biggest reason why OAuth isn't authentication: access tokens are not intended for the client application. When an authorization server issues an access token, the intended audience is the protected resource. After all, this is what the token is providing access to.

How do you use an authentication Keycloak?

Configure Keycloak to authenticate your cbioportal instance. Log in to your Keycloak Identity Provider, e.g. http://localhost:8080/auth, as an admin user. ⚠️ when setting this up on something else than localhost (e.g. production), you will need to use/enable https on your Keycloak server.

How to use OAuth in Ktor?

The oauth provider supports the authorization code flow. You can configure OAuth parameters in one place, and Ktor will automatically make a request to a specified authorization server with the necessary parameters. You can get general information about authentication and authorization in Ktor in the Authentication and authorization section.

How to configure client on Ktor with Keycloak?

If you click on OpenId Endpoint Configuration you will have all the necessary URLs to configure your client on KTOR. Now you must add a new client in your Realm so that your users will be able to authenticate into the application through the Keycloak server. Here the important parameters are the client protocol and the Valid Redirect URL.

What is OAuth and how do I use it?

OAuth can be used to authorize users of your application by using external providers, such as Google, Facebook, Twitter, and so on. The oauth provider supports the authorization code flow. You can configure OAuth parameters in one place, and Ktor will automatically make a request to a specified authorization server with the necessary parameters.

What is The clientId and cliensecret for Keycloak?

The clientId and clienSecret are mandatory for the configuration in KTOR (and must not be empty), but for Keycloak they are not mandatory since we set an access type for the client as “public”. If you set it to “confidential”, you will have a new tab in Keycloak to get the name and the secret of the client.


1 Answers

The answer to your first question: it will be used for both if that route corresponds to the redirect URI returned in urlProvider lambda.

The overall process is the following:

  1. A user opens http://localhost:7777/login (any route under authenticate) in a browser
  2. Ktor makes a redirect to authorizeUrl passing necessary parameters
  3. The User logs in through Keycloak UI
  4. Keycloak redirects the user to the redirect URI provided by urlProvider lambda passing parameters required for acquiring an access token
  5. Ktor makes a request to the token URL and executes the routing handler that corresponds to the redirect URI (http://localhost:7777/callback in the example).
  6. In the handler you have access to the OAuthAccessTokenResponse object that has properties for an access token, refresh token and any other parameters returned from Keycloak.

Here is the code for the working example:

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)
}
like image 161
Aleksei Tirman Avatar answered Oct 28 '22 03:10

Aleksei Tirman