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.
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}")
}
}
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" }
}
}
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?
Keycloak is Open Source Identity and Access Management Server, which is a OAuth2 and OpenID Connect(OIDC) protocol complaint.
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.
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.
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.
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.
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.
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.
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:
authenticate
) in a browserauthorizeUrl
passing necessary parametersurlProvider
lambda passing parameters required for acquiring an access tokenOAuthAccessTokenResponse
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)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With