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

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?

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:
A user opens http://localhost:7777/login (any route under authenticate) in a browser
Ktor makes a redirect to authorizeUrl passing necessary parameters
The User logs in through Keycloak UI
Keycloak redirects the user to the redirect URI provided by urlProvider lambda passing parameters required for acquiring an access token
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).
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)
}

Related

How to use OAuth Authorization Code for CLIs

Trying to allow a CLI I'm developing to "login" via web browser and obtain an access token for the user's account, similar to how gcloud and github's CLIs do it. I realize it'll be using the OAuth Authorization Code flow.
But what about the client_secret?
I've found out that github cli just doesn't care about exposing it, and it's right there in the source code: https://github.com/cli/cli/blob/6a8deb1f5a9f2aa0ace2eb154523f3b9f23a05ae/internal/authflow/flow.go#L25-L26
Why is this not a problem? or is it?
I'm not yet using OAuth for the cli's login
STANDARDS
The CLI app is a native public client and should use authorization code flow + PKCE rather than a fixed client secret. It should also follow the flow from
RFC8252 and receive the browser response using a local HTTP (loopback) URI.
THIS IMPLEMENTATION
Looks like the github code here uses a client secret and does not use PKCE. You may have to provide a client secret if using this library, but it cannot be kept secret from users. Any user could easily view it, eg with an HTTP proxy tool.
CODE
If the infrastructure enables you to follow the standards, aim for something similar to this Node.js code.
* The OAuth flow for a console app
*/
export async function login(): Promise<string> {
// Set up the authorization request
const codeVerifier = generateRandomString();
const codeChallenge = generateHash(codeVerifier);
const state = generateRandomString();
const authorizationUrl = buildAuthorizationUrl(state, codeChallenge);
return new Promise<string>((resolve, reject) => {
let server: Http.Server | null = null;
const callback = async (request: Http.IncomingMessage, response: Http.ServerResponse) => {
if (server != null) {
// Complete the incoming HTTP request when a login response is received
response.write('Login completed for the console client ...');
response.end();
server.close();
server = null;
try {
// Swap the code for tokens
const accessToken = await redeemCodeForAccessToken(request.url!, state, codeVerifier);
resolve(accessToken);
} catch (e: any) {
reject(e);
}
}
}
// Start an HTTP server and listen for the authorization response on a loopback URL, according to RFC8252
server = Http.createServer(callback);
server.listen(loopbackPort);
// Open the system browser to begin authentication
Opener(authorizationUrl);
});
}
/*
* Build a code flow URL for a native console app
*/
function buildAuthorizationUrl(state: string, codeChallenge: string): string {
let url = authorizationEndpoint;
url += `?client_id=${encodeURIComponent(clientId)}`;
url += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=code';
url += `&scope=${scope}`;
url += `&state=${encodeURIComponent(state)}`;
url += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
url += '&code_challenge_method=S256';
return url;
}
/*
* Swap the code for tokens using PKCE and return the access token
*/
async function redeemCodeForAccessToken(responseUrl: string, requestState: string, codeVerifier: string): Promise<string> {
const [code, responseState] = getLoginResult(responseUrl);
if (responseState !== requestState) {
throw new Error('An invalid authorization response state was received');
}
let body = 'grant_type=authorization_code';
body += `&client_id=${encodeURIComponent(clientId)}`;
body += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
body += `&code=${encodeURIComponent(code)}`;
body += `&code_verifier=${encodeURIComponent(codeVerifier)}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body,
};
const response = await fetch(tokenEndpoint, options);
if (response.status >= 400) {
const details = await response.text();
throw new Error(`Problem encountered redeeming the code for tokens: ${response.status}, ${details}`);
}
const data = await response.json();
return data.access_token;
}

Ktor OAuth not working when grant_type is password

I implemented OAuth authorization in my kotlin Ktor backend.
When using grant_type = client_credentials, everything works as expected.
However, when using grant_type = password, it does not even execute the body of loadTokens.
Breakpoints inside loadTokens in the client_credentials example do hit, in the password example, loadTokens hits a few times but nothing inside.
There is also no auth request logged for the tokenClient in the second example, even though log level has been configured.
This one works:
if (user == null) {
loadTokens {
val tokens = tokenClient.submitForm<FaOAuthResult>("https://sandbox.xxx.io/oauth/token",
Parameters.build {
append("grant_type", "client_credentials")
append("client_id", "xxx")
append("client_secret", "xxx")
})
BearerTokens(
accessToken = tokens.access_token,
refreshToken = tokens.access_token
)
}
And this one doesn't:
else {
loadTokens {
val tokens =
tokenClient.submitForm<FaOAuthResult>("https://sandbox.xxx.io/oauth/token",
Parameters.build {
append("grant_type", "password")
append("client_id", "xxx")
append("client_secret", "xxx")
append("username", user.finapiUserId!!)
append("password", user.finapiPassword!!)
})
BearerTokens(
accessToken = tokens.access_token,
refreshToken = tokens.access_token
)
}

Multiple urlProviders on the same oauth setting in ktor with keycloak

I am making a ktor web application with keycloak as auth.
val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings(
name = CLIENT_NAME,
authorizeUrl = KEYCLOAK_AUTH,
accessTokenUrl = KEYCLOAK_TOKEN,
clientId = CLIENT_ID,
clientSecret = CLIENT_SECRET,
accessTokenRequiresBasicAuth = false,
requestMethod = HttpMethod.Post, // must POST to token endpoint
defaultScopes = listOf("roles")
)
There are multiple endpoints that I want to secure but the redirect URI can only be set to one URL, in the example below it's /login/oauth and /secret.
install(Authentication)
{
oauth("secretOAuth") {
client = HttpClient(Apache)
providerLookup = { keycloakProvider }
urlProvider = { "/secret" }
}
oauth("keycloakOAuth") {
client = HttpClient(Apache)
providerLookup = { keycloakProvider }
urlProvider = { "/login/oauth" }
}
}
Does it make sense to create multiple of these authentication paths like in the example above or would it be bad practice?
The /login/oauth and /secret are pointing to the following routes:
authenticate(keycloakOAuth) {
get("/login/oauth") {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
?: throw Exception("No principal was given")
createSession(principal)
call.respondRedirect("/")
}
}
authenticate(secretOAuth){
get("/secret")
{
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
?: throw Exception("No principal was given")
createSession(principal)
call.respondHtml {
body{
h1{
+"You accessed secure page!"
}
}
}
}
}
Functionality wise it works, when the user isn't logged in it prompts them with the login window, otherwise they get to see the secret/their session gets created but I'm not sure if this is the correct way of doing it. Is there really no way to change the urlProvider based on the accessed URI? Because then I will have to make an authentication for each protected endpoint and that might be a bit too much.
In your scenario, it's redundant to have multiple authentication configurations for the same provider because a client can acquire an access token with any. I suggest using the authenticate block to just get an access token and save it somewhere. You can use the usual routes for the "secret" resources where you can check a user having an access token.
routing {
authenticate("keycloakOAuth") {
get("/login") {
// The endpoint for user login
}
get("/callback") {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
if (principal != null) {
// Save access token (principal.accessToken) for further use
} else {
// Something went wrong
}
}
}
get("/secret") {
if (!hasToken()) {
call.respondRedirect("/login")
}
}
}

Extension Grants - Invalid Grant Type Delegation - Identity Server 4 .NET Core 2.2

I am trying to figure out how to implement a delegation grant type in conjunction with client credentials, by following the tutorial from HERE, which is literally one page, since I have and API1 resource calling another API2 resource.
I've implemented the IExtensionGrantValidator and copied the code from the docs using the class name they provided, and added the client with grant type delegation. However, I am not sure where and how to call this method below, at first I was calling it from the client and tried passing the JWT I initially got to call API1 to the DelegateAsync method but I kept getting a bad request
In API 1 you can now construct the HTTP payload yourself, or use the IdentityModel helper library:
public async Task<TokenResponse> DelegateAsync(string userToken)
{
var payload = new
{
token = userToken
};
// create token client
var client = new TokenClient(disco.TokenEndpoint, "api1.client", "secret");
// send custom grant to token endpoint, return response
return await client.RequestCustomGrantAsync("delegation", "api2", payload);
}
So, I tried from API1 requesting a token in a method called GetAPI2Response which attempts to call a method in API2:
[HttpGet]
[Route("getapi2response")]
public async Task<string> GetApi2Response()
{
var client = new HttpClient();
var tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
Address = "http://localhost:5005/connect/token",
GrantType = "delegation",
ClientId = "api1_client",
ClientSecret = "74c4148e-70f4-4fd9-b444-03002b177937",
Parameters = { { "scope", "stateapi" } }
});
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:6050/api/values");
if (!response.IsSuccessStatusCode)
{
Debug.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
return content;
}
return "failed";
}
However, this returns when debugging an invalid grant type. Strangely, I noticed when running IDSRV the code in the IExtensionGrantValidator method does not get hit, until you click the link for the discovery docs then it appears as a grant type
I'm obviously doing something wrong since I am not including the aforementioned DelegateAsync method from the docs, as its not clear to me where it goes.
The docs seem to be a bit outdated. With the actual extension methods there must be something like:
var tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
Address = "http://localhost:5005/connect/token",
GrantType = "delegation",
ClientId = "api1_client",
ClientSecret = "74c4148e-70f4-4fd9-b444-03002b177937",
Parameters = new Dictionary<string, string>{{ "token", userToken }, { "scope", "stateapi" } }
})
you already implemented it, but forgot to add the initial token. When you extract it from the GetApi2Response() it can become your DelegateAsync.
Then your client configuration in Identityserver has to contain the delegation GrantType for the api1_client. Also don't forget the registration:
services.AddIdentityServer().AddExtensionGrantValidator<YourIExtensionGrantValidatorImpl>()

Ktor Session Cookie Authentication

I'd like to use a session cookie for authentication with Ktor and what I have so far is:
private const val SEVER_PORT = 8082
private const val SESSION_COOKIE_NAME = "some-cookie-name"
data class AuthSession(
val authToken: String
)
fun main() {
embeddedServer(Netty, port = SEVER_PORT, module = Application::basicAuthApplication).start(wait = true)
}
fun Application.basicAuthApplication() {
install(Sessions) {
cookie<AuthSession>(SESSION_COOKIE_NAME, SessionStorageMemory()) {
cookie.path = "/"
}
}
install(DefaultHeaders)
install(CallLogging)
install(Authentication) {
session<AuthSession> {
validate { session ->
// TODO: do the actual validation
null
}
}
}
routing {
authenticate {
get("/") {
call.respondText("Success")
}
}
}
}
But everytime when I do:
curl -v localhost:8082
I get an HTTP 200 and the response "Success"
I expected to get an HTTP 401 Not authorized or something similar.
Can somebody give me insights here how to do proper session cookie authentication with Ktor?
thanks
UPDATE:
Okay I realized there is a session auth type which is not documented with authentication feature docs.
The issue with your current code is that you are not specifying the challenge explicitly, the default challenge specified inside is SessionAuthChallenge.Ignore so you have to change it to SessionAuthChallenge.Unauthorized or SessionAuthChallenge.Redirect
So your code should look like:
install(Authentication) {
session<AuthSession> {
challenge = SessionAuthChallenge.Unauthorized
validate { session ->
// TODO: do the actual validation
null
}
}
}
OLD:
You are not specifying the type of authentication you want to use, probably basic, form or jwt, you may want to try something like this for form authentications for example:
install(Authentication) {
form("login") {
skipWhen { call -> call.sessions.get<AuthSession>() != null }
userParamName = "username"
passwordParamName = "password"
challenge = FormAuthChallenge.Unauthorized
validate { credentials ->
// Handle credentials validations
}
}
}
Check the official documentation for more info.