62

I have issue while calling Keycloak's logout endpoint from an (mobile) application.

This scenario is supported as stated in its documentation:

/realms/{realm-name}/protocol/openid-connect/logout

The logout endpoint logs out the authenticated user.

The user agent can be redirected to the endpoint, in which case the active user session is logged out. Afterward the user agent is redirected back to the application.

The endpoint can also be invoked directly by the application. To invoke this endpoint directly the refresh token needs to be included as well as the credentials required to authenticate the client.

My request has following format:

POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded

refresh_token=<refresh_token>

but this error always occurs:

HTTP/1.1 400 Bad Request
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 123
Date: Wed, 11 Oct 2017 12:47:08 GMT

{
  "error": "unauthorized_client",
  "error_description": "UNKNOWN_CLIENT: Client was not identified by any client authenticator"
}

It seems that Keycloak is unable to detect the current client's identity event if I've provided access_token. I've the used same access_token to access other Keycloak's APIs without any problems, like userinfo (/auth/realms/<my_realm>/protocol/openid-connect/userinfo).

My request was based on this Keycloak's issue. The author of the issue got it worked but it is not my case.

I'm using Keycloak 3.2.1.Final.

Do you have that same problem? Have you got any idea how to solve it?

Falchio
  • 174
  • 1
  • 14
Manh Ha
  • 1,617
  • 2
  • 14
  • 18

11 Answers11

85

Finally, I've found the solution by looking at the Keycloak's source code: https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169. It says:

If the client is a public client, then you must include a "client_id" form parameter.

So what I was missing is the client_id form parameter. My request should have been:

POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded

client_id=<my_client_id>&refresh_token=<refresh_token>

The session should be destroyed correctly.

Aliaksandr Sushkevich
  • 11,550
  • 7
  • 37
  • 44
Manh Ha
  • 1,617
  • 2
  • 14
  • 18
  • I tried this with Keycloak 4.4.0.Final and 4.6.0.Final and could not get it to work successfully. – peter_pilgrim Nov 28 '18 at 10:47
  • 1
    From this answer, I don't understand what does your `client_id` map to in a Spring Web application? I tried 'IdToken.id', 'AccessTokenId.id' and also the 'context.tokenString' - every time I get the error message 'invalid client credentials' – peter_pilgrim Nov 29 '18 at 11:06
  • 6
    In fact, ``access token`` is not needed, just the ``client_id`` and ``refresh_token`` is enough. – maslick Dec 13 '18 at 19:13
  • @Manh Ha can u please confirm where we needs to sent client_id and refresh_token in request body or as url parameter ? – Bhagvat Lande Feb 04 '19 at 13:28
  • @BhagvatLande: these params should be in (POST) request body, not in url. – Manh Ha Feb 04 '19 at 15:58
  • thanks @ManhHa , I am struggling to found refresh token , can you please explain where did i found this , i try almost all ways .. if possible please reply asap . thanks again – Bhagvat Lande Feb 05 '19 at 08:15
  • @BhagvatLande: it depends on which flow of OpenID connect. In my case, it is "Resource owner" flow for which I just send a (POST) request to http://localhost:8080/auth/realms//protocol/openid-connect/token. I obtain something which looks like this: { "access_token": "", "token_type": "bearer", "not-before-policy": 0, "session_state": "cefa0a94-3df5-4d25-affe-f1b517bae85c", "scope": "email profile" } . This is where you can find refresh_token. – Manh Ha Feb 05 '19 at 13:07
  • thanks @ManhHa i checked this it returns refresh token but it seems keycloak creating another new session for same user , this leads me in problem i don't want too create another session – Bhagvat Lande Feb 05 '19 at 13:17
  • actually i was struggling in logout same question posted – Bhagvat Lande Feb 05 '19 at 13:19
  • I don't know what your use case but generally when user is logged in (via one of OpenID Connect's flows, you should save his refresh token. At a later time, when you want to log him out, you use that same refresh token to logout. You should not use the login request above (http://localhost:8080/auth/realms/%3Cmy_realm%3E/protocol/openid-connect/token) to retrieve a (new) refresh token because Keycloak will create a new session for that request, as you've seen. If you can describe more about your system (which flow you are using), it might be that I will be able to tell a little bit more. – Manh Ha Feb 05 '19 at 13:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/187932/discussion-between-bhagvat-lande-and-manh-ha). – Bhagvat Lande Feb 05 '19 at 14:03
  • @ManhHa Where do you get "refresh_token" from to include in your "data"? – SwissNavy Nov 29 '19 at 15:04
  • @ManhHa I see in the chat you said "I just send a (POST) request to localhost:8080/auth/realms//protocol/openid-connect/token" . When I do that (with or without cliend_id in the payload), I get 400 code which is the same error taht means "no valid credentials" – SwissNavy Nov 29 '19 at 15:22
  • 1
    @SwissNavy: you get the refresh token when you log user in (meaning you might have send a request to localhost:8080/auth/realms//protocol/openid-connect/token), the response of that request might look like this: { "access_token": "", "expires_in": 299, "refresh_expires_in": 1799, "refresh_token": "", "token_type": "bearer", "not-before-policy": 0, "session_state": "", "scope": "profile email" } You can find the refresh token in that response. – Manh Ha Dec 02 '19 at 09:20
  • @ManhHa . Thanks, but I mean where is the refresh_token stored (if it's stored)? I did not find it in the cookies. I log in and browse the site for some time, now I want to logout, where can I retrieve this token from at this point? Hitting openid-connect/token just gives me `{"error":"unauthorized_client","error_description":"INVALID_CREDENTIALS: Invalid client credentials"}`, maybe I am not calling it right... – SwissNavy Dec 02 '19 at 10:17
  • 1
    @SwissNavy: it depends on how you integrate with Keycloak: Which OpenID Connect flow (Implicit Flow/Authentication Flow/Resource Owner Password Grant/Client Credentials Grant), because I think that not all of these flows give you a refresh token. You might need to refer to the OpenID Connect protocol's docs for more information. What technology stack you are using is also important because certain library/framework might store differently the refresh token. Might I ask what flow + technology stack you are working on? – Manh Ha Dec 02 '19 at 13:00
  • @ManhHa. I have a web container with Django 2.2 that talks to keycloak container( build using pulled jboss/keycloak image). All works fine for logging in, but for logout I need refresh_token so I call for it: `url_token = "http://:8080/auth/realms/acc/protocol/openid-connect/token/" data = {'client_id' : '', 'grant_type': 'client_credentials'} r = requests.post(url_token, data = data)` and get `{"error":"unauthorized_client","error_description":"INVALID_CREDENTIALS: Invalid client credentials"}`. Also tried with username and password instead of client_id. – SwissNavy Dec 02 '19 at 13:44
  • @ManhHa. Same response from curl POST. Some of my struggle is described here: https://stackoverflow.com/questions/58198713/how-to-log-out-from-keycloak-from-django-code – SwissNavy Dec 02 '19 at 13:45
  • @ManhHa In my realm.json file I have this line: "accessTokenLifespanForImplicitFlow" : 900 . Does this mean I am using Implicit Flow? If so, how should I logout? I did not write that app so I am somewhat guessing here.. – SwissNavy Dec 02 '19 at 14:03
  • 1
    I've never used Django (because I'm a Java guy ) but I try to look at the framework you are using (Social Core) for Keycloak implementation. I does not seem to store refresh token anywhere: https://github.com/python-social-auth/social-core/blob/1d809941ab8a99af9e1bdf12bae548202c94eaa2/social_core/backends/keycloak.py . It'd be better you address your question to the library maintainers. – Manh Ha Dec 02 '19 at 14:12
  • 1
    Your second question about the realm.json, I don't think that parameter means you are using implicit. You might want however analyse the network exchanges between your app and Keycloak by using https://developers.google.com/web/tools/chrome-devtools/network/reference if you are using Chrome. Look for a parameter grant_type, it should be the flow you are looking for. – Manh Ha Dec 02 '19 at 14:12
  • @ManhHa Thanks. There's no grant_type in my network traffic. After the login my request.session contains this stuff: items: dict_items([('next', '/'), ('keycloak_state', 'k3edci3ZqYCdh7KXIGhJ6Wte32t3lRY4'), ('_auth_user_id', '37'), ('_auth_user_backend', 'social_core.backends.keycloak.KeycloakOAuth2'), ('_auth_user_hash', '073b9f72bb8f7eaf95c15da55a20006d9009a03c'), ('social_auth_last_login_backend', 'keycloak')]), I wonder if any of that can be used for logout call... – SwissNavy Dec 02 '19 at 14:38
  • @SwissNavy: unfortunately no, you cannot use any of these to call the /logout endpoint. – Manh Ha Dec 02 '19 at 14:46
  • Thank you, it's perfect now! My tokens are finally invalidated! – bodich May 19 '23 at 09:17
16

Works with Keycloak 6.0.

Just for clarity: we do expire refreshToken, but accessToken IS STILL VALID while "Access Token Lifespan" time. Next time user tries to renew access token passing refresh token, Keycloak returns 400 Bad request, what should be catch and send as 401 Unauthorised response.

public void logout(String refreshToken) {
    try {
        MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
        requestParams.add("client_id", "my-client-id");
        requestParams.add("client_secret", "my-client-id-secret");
        requestParams.add("refresh_token", refreshToken);

        logoutUserSession(requestParams);

    } catch (Exception e) {
        log.info(e.getMessage(), e);
        throw e;
    }
}

private void logoutUserSession(MultiValueMap<String, String> requestParams) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(requestParams, headers);
    String realmName = "your-realm-name";
    String url = "/auth/realms/" + realmName + "/protocol/openid-connect/logout";

    restTemplate.postForEntity(url, request, Object.class);
    // got response 204, no content
}
Dmitri Algazin
  • 3,332
  • 27
  • 30
  • 1
    How did you manage to also expire the accessToken?. My concern is that if the access token is still valid after logout then there is a security risk. How can I handle that? – Cheeta Dec 13 '19 at 07:19
  • 1
    usually accessToken is valid for short time, like 15 min. How to force to expire it, I don't know, ask a question in keycloak forum https://keycloak.discourse.group/ – Dmitri Algazin Dec 13 '19 at 10:20
  • In my case, the API `auth/realms/my-realm/protocol/openid-connect/userinfo` gives 401 with access_token. I am using keycloak 7.01 – arulraj.net Sep 30 '21 at 01:57
14

Finally. It worked for me. I made a REST call as shown below:

Headers:

{
 "Authorization" : "Bearer <access_token>",
 "Content-Type" : "application/x-www-form-urlencoded"
}

Request Body:

{
    "client_id" : "<client_id>",
    "client_secret" : "<client_secret>",
    "refresh_token" : "<refresh_token>"
}

Method:

POST

URL:

<scheme>://<host>:<port>/auth/realms/<realmName>/protocol/openid-connect/logout

I received 200 as a response... If you do anything wrong you will get 401 or 400 errors. It's very tough to debug this issue. BTW my keycloak version is 12.0.4

Let me know if the post is not clear or if you need more information.

SANDEEP MACHIRAJU
  • 817
  • 10
  • 17
  • Adding client_id and client_secret to request body might create security issue.Can we expose this variable on the browser or network? – Coşkun Uyar Aug 08 '22 at 14:57
  • This is, in general, used in server-side communication (Example: Gateway to Keycloak) in a secure channel (HTTPS). For the browser, maybe you can go with the 302 redirection method. – SANDEEP MACHIRAJU Aug 10 '22 at 22:01
6

in version 3.4 you need as x-www-form-urlencoded body key client_id, client_secret and refresh_token.

Subodh Joshi
  • 12,717
  • 29
  • 108
  • 202
Nerospeed
  • 173
  • 1
  • 7
  • Sending client_id and client_secret in body is discouraged by RFC 6749, better to use HTTP Basic authentication instead. Only refresh_token needs to be sent in body, I checked that it works with Keycloak. – Zmey Oct 31 '19 at 23:22
2

FYI: OIDC spec and Google's implementation has a token revocation endpoint

It was implemented in Keycloak 10. See Keycloak JIRA for details

Sergey Ponomarev
  • 2,947
  • 1
  • 33
  • 43
2

According to the code: https://github.com/keycloak/keycloak/blob/master/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L106

This is how it worked for my SpringBoot FX app

GET http://loccalhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout?post_redirect_uri=your_encodedRedirectUri&id_token_hint=id_token

Yaw
  • 156
  • 1
  • 4
1

This approach does not require any manual endpoint triggers. It relies on LogoutSuccessHandler and particularly on OidcClientInitiatedLogoutSuccessHandler that checks if end_session_endpoint is present on ClientRegistration bean.

By some circumstances end_session_endpoint is not used by default on most auth providers (except Okta) when paired with Spring Security, and we are left to inject it into ClientRegistration manually. The easiest way was to put it before InMemoryClientRegistrationRepository initialization, right after application.properties or application.yaml loading.

package com.tb.ws.cscommon.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
public class ClientRegistrationConfig {

  @Bean
  @ConditionalOnMissingBean({ClientRegistrationRepository.class})
  InMemoryClientRegistrationRepository clientRegistrationRepository(
      OAuth2ClientProperties properties) {
    List<ClientRegistration> registrations =
        OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties)
            .values()
            .stream()
            .map(
                o ->
                    ClientRegistration.withClientRegistration(o)
                        .providerConfigurationMetadata(
                            Map.of(
                                "end_session_endpoint",
                                "http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/logout"))
                        .build())
            .collect(Collectors.toList());

    return new InMemoryClientRegistrationRepository(registrations);
  }
}

And in WebSecurity:

package com.tb.ws.cscommon.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

@Slf4j
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
  private final InMemoryClientRegistrationRepository registrationRepository;

  public WebSecurity(InMemoryClientRegistrationRepository registrationRepository) {
    this.registrationRepository = registrationRepository;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    String[] permitAccess = new String[] {"/", "/styles/**"};

    http.authorizeRequests()
        .antMatchers(permitAccess)
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .oauth2Login()
        .and()
        .logout(
            logout -> {
              logout.logoutSuccessHandler(logoutSuccessHandler());
              logout.invalidateHttpSession(true);
              logout.clearAuthentication(true);
              logout.deleteCookies("JSESSIONID");
            });
  }

  private LogoutSuccessHandler logoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler handler =
        new OidcClientInitiatedLogoutSuccessHandler(registrationRepository);
    handler.setPostLogoutRedirectUri("http://127.0.0.1:8005/");

    return handler;
  }
}

By default, Spring Security appends query parameters id_token_hint and post_logout_redirect_uri onto end_session_endpoint. This can be changed with OidcClientInitiatedLogoutSuccessHandler handler. This can be used with social providers. Just have a relevant end_session_endpoint for each provider.

Properties file application.yaml used in this example:

spring:
  application:
    name: cs-common
  main:
    banner-mode: off
  security:
    oauth2:
      client:
        registration:
          cs-common-1:
            client_id: cs-common
            client-secret: 03e2f8e1-f150-449c-853d-4d8f51f66a29
            scope: openid, profile, roles
            authorization-grant-type: authorization_code
            redirect_uri: http://127.0.0.1:8005/login/oauth2/code/cs-common-1
        provider:
          cs-common-1:
            authorization-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/auth
            token-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/token
            jwk-set-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/certs
            user-info-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username
server:
  port: 8005
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8004/eureka
  instance:
    instance-id: ${spring.application.name}:${instanceId:${random.value}}

To test we just kick Spring Security's default GET /logout endpoint from the UI.

Misc:

  • Spring Boot 2.5
  • Spring Cloud 2020.0.3
  • Java 11
  • Keycloak Server 13.0.1

Client settings:

  • Standard Flow Enabled
  • Implicit Flow Disabled
  • Direct Access Grants Enabled

Someone, somewhere may find it helpful.

P.S. The app and its properties file are for learning

downvoteit
  • 588
  • 2
  • 7
  • 12
  • 1
    Thanks. It helped me. Couple of points to add: 1. We need to add `"http://127.0.0.1:8005/"` as another **Valid Redirect URIs** in Keycloak 2. Instead of using hardcoded `"http://127.0.0.1:8005/"`, we can use `"{baseUrl}/"` like `handler.setPostLogoutRedirectUri("{baseUrl}/");` – K. Siva Prasad Reddy Oct 13 '21 at 23:54
1

At least on newer versions of keycloak, when I authenticate I get back an id_token value which I can then use to perform a GET request to the /logout endpoint which terminates the session in the keycloak console

https://example.com/realms/{realm_id}/protocol/openid-connect/logout?id_token_hint={id_token}

I've found trying to do the POST to these endpoints just becomes a bit of a nightmare and is not very well documented.

The URL takes you to the standard KeyCloak user signout page and doesn't seem to throw any bad status codes if the id_token_hint value is no longer valid.

I don't bother showing the page to the user, I just perform the call on the backend once they click signout and take them to my own splash screen which is far easier to customize

Kom N
  • 63
  • 4
0

I tried this with Keycloak 4.4.0.Final and 4.6.0.Final. I checked the keycloak server log and I saw the following warning messages in the console output.

10:33:22,882 WARN  [org.keycloak.events] (default task-1) type=REFRESH_TOKEN_ERROR, realmId=master, clientId=security-admin-console, userId=null, ipAddress=127.0.0.1, error=invalid_token, grant_type=refresh_token, client_auth_method=client-secret
10:40:41,376 WARN  [org.keycloak.events] (default task-5) type=LOGOUT_ERROR, realmId=demo, clientId=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqYTBjX18xMHJXZi1KTEpYSGNqNEdSNWViczRmQlpGS3NpSHItbDlud2F3In0.eyJqdGkiOiI1ZTdhYzQ4Zi1mYjkyLTRkZTYtYjcxNC01MTRlMTZiMmJiNDYiLCJleHAiOjE1NDM0MDE2MDksIm5iZiI6MCwiaWF0IjoxNTQzNDAxMzA5LCJpc3MiOiJodHRwOi8vMTI3Lj, userId=null, ipAddress=127.0.0.1, error=invalid_client_credentials

So how did build the HTTP request? First, I retrieved the user principal from the HttpSession and cast to the internal Keycloak instance types:

KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) request.getUserPrincipal();
final KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal)keycloakAuthenticationToken.getPrincipal();
final RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) keycloakPrincipal.getKeycloakSecurityContext();
final AccessToken accessToken = context.getToken();
final IDToken idToken = context.getIdToken();

Second, I created the logout URL as in the top stack overflow answer (see above):

final String logoutURI = idToken.getIssuer() +"/protocol/openid-connect/logout?"+
            "redirect_uri="+response.encodeRedirectURL(url.toString());

And now I then build the rest of the HTTP request like so:

KeycloakRestTemplate keycloakRestTemplate = new KeycloakRestTemplate(keycloakClientRequestFactory);
HttpHeaders headers = new HttpHeaders();
headers.put("Authorization", Collections.singletonList("Bearer "+idToken.getId()));
headers.put("Content-Type", Collections.singletonList("application/x-www-form-urlencoded"));

And also build the body content string:

StringBuilder bodyContent = new StringBuilder();
bodyContent.append("client_id=").append(context.getTokenString())
            .append("&")
            .append("client_secret=").append(keycloakCredentialsSecret)
            .append("&")
            .append("user_name=").append(keycloakPrincipal.getName())
            .append("&")
            .append("user_id=").append(idToken.getId())
            .append("&")
            .append("refresh_token=").append(context.getRefreshToken())
            .append("&")
            .append("token=").append(accessToken.getId());
HttpEntity<String> entity = new HttpEntity<>(bodyContent.toString(), headers);
//   ...
ResponseEntity<String> forEntity = keycloakRestTemplate.exchange(logoutURI, HttpMethod.POST, entity, String.class); // *FAILURE*

As you can observed, I attempted many variations of theme, but I kept getting invalid user authentication. Oh yeah. I injected the keycloak credentials secret from the application.properties into object instance field with @Value

@Value("${keycloak.credentials.secret}")
private String keycloakCredentialsSecret;

Any ideas from Java Spring Security experienced engineers?

ADDENDUM I created a realm in KC called 'demo' and a client called 'web-portal' with the following parameters:

Client Protocol: openid-connect
Access Type: public
Standard Flow Enabled: On
Implicit Flow Enabled: Off
Direct Access Grants Enabled: On
Authorization Enabled: Off

Here is the code that rebuilds the redirect URI, I forgot to include it here.

final String scheme = request.getScheme();             // http
final String serverName = request.getServerName();     // hostname.com
final int serverPort = request.getServerPort();        // 80
final String contextPath = request.getContextPath();   // /mywebapp

// Reconstruct original requesting URL
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);

if (serverPort != 80 && serverPort != 443) {
    url.append(":").append(serverPort);
}

url.append(contextPath).append("/offline-page.html");

That's all

peter_pilgrim
  • 1,160
  • 11
  • 18
  • You are using which kind of grant_type for your login flow? I was using grant_type=password because it was a mobile client with a native (iOS) login form. Because of that, I had to POST to the endpoint but was not able to redirect user to Keycloak signout page. It seems that you are developping a web application, have you try just redirect user to the sign out page of Keycloak: https://www.keycloak.org/docs/latest/securing_apps/index.html#logout? – Manh Ha Nov 28 '18 at 13:41
  • My grant type is 'public' and client protocol is 'openid-connect' and I am also using Vaadin 10 to secure a Java web application – peter_pilgrim Nov 29 '18 at 10:50
  • I just added redirectURI code to my answer above. The other thing that Vaadin 10 has different logout mechanism compared to Vaadin 8. It uses a new Flow API. See here https://vaadin.com/docs/v10/flow/advanced/tutorial-application-lifecycle.html I can confirm that Logout without Keycloak works, because I tested their own Vaadin Bakery Spring Security application. However, this does not log the user out of Keycloak and hence I was attempting to make a RESTful call to the Keycloak server to logout the user out and then close the Vaadin (Http) Session. Make sense? :-/ – peter_pilgrim Nov 29 '18 at 11:04
  • OpenID Connect (that Keycloak implements) use the same grant_type as OAuth2, so its value should be one of the following: https://oauth.net/2/grant-types/ . As I don't have yet any idea of why your code doesn't work, could you provide a sample Github repo to reproduce the problem? It will be easier for me or others can look at and maybe has a hint about that. – Manh Ha Nov 29 '18 at 12:38
  • I am wondering if this should be a brand new stack overflow ticket. Should I create a new one? – peter_pilgrim Nov 29 '18 at 14:00
  • Yes, I think so. My initial question was about a mobile client interacting with Keycloak server. Yours is Spring/Vaadin-related. The solution might be not the same. – Manh Ha Nov 29 '18 at 14:25
  • I created a Github repo: https://github.com/peterpilgrim/acme-client-vaadin10-app (removed the sensitivities) and I asked a new Stack Overflow question https://stackoverflow.com/questions/53541373/how-to-make-rest-call-logout-work-with-spring-boot-vaadin-10-and-keycloak-4 – peter_pilgrim Nov 29 '18 at 14:31
  • Note that idToken can be null. If anyone will want to get token string, use context.getTokenString(); instead of context.getToken() – victory Mar 21 '20 at 16:31
0

In the JWT you have "session_state"

{
    "exp": 1616268254,
    "iat": 1616267954,
     ....
    "session_state": "c0e2cd7a-11ed-4537-b6a5-182db68eb00f",
    ...
}

After

public void testDeconnexion() {
        
        String serverUrl = "http://localhost:8080/auth";
        String realm = "master";
        String clientId = "admin-cli";
        String clientSecret = "1d911233-bfb3-452b-8186-ebb7cceb426c";
        
        String sessionState = "c0e2cd7a-11ed-4537-b6a5-182db68eb00f";

        Keycloak keycloak = KeycloakBuilder.builder()
                .serverUrl(serverUrl)
                .realm(realm)
                .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                .clientId(clientId)
                .clientSecret(clientSecret) 
                .build();

        String realmApp = "MeineSuperApp";      

        RealmResource realmResource = keycloak.realm(realmApp);
        realmResource.deleteSession(sessionState);      
        
    }
0

For keycloak 16 using feign client i'm using the following solution:

Feign Client:

@FeignClient(name = "KeycloakClient")
public interface KeycloakClient {
    @PostMapping(
          value = "/auth/realms/{realm}/protocol/openid-connect/logout",
          consumes = "application/x-www-form-urlencoded")
      Response logout(
          @RequestHeader("Authorization") String authorizationHeader,
          @PathVariable(value = "realm") String realm,
          @RequestBody String body);
}

Calling the logout:

public void logout(String accessToken,String refreshToken,String realm,String clientId,String clientSecret){
    String logoutTemplate = "client_id=%s&client_secret=%s&refresh_token=%s";
    String logoutBody = String.format(
        logoutTemplate,
        clientId,
        clientSecret,
        refreshToken)
    keycloakClient.logout("Bearer " + accessToken,realm,logoutBody);
}