Generator-jhipster: Access token not renewed when using Keycloak

Created on 31 Dec 2017  路  15Comments  路  Source: jhipster/generator-jhipster

Overview of the issue

With a freshly generated microservice architecture (with just one microservice in my case) using Keycloak, the microservice responds only with 401 once the access token lifespan is reached (by default 5 minutes).

No feedback in the jhipster application is provided. Fresh login is necessary to continue working with the entities. It is still possible with the gateway itself (the "Administration" menu if logged in as admin).

Motivation for or Use Case

I would expect the user to stay logged in as long as he's actively working with the application.
Also, I think that a user should be given a feedback once the session expires, instead of displaying pages with no content.

Reproduce the error
  1. Create a microservice application containing some entities, Gateway and choose OIDC as the auth. mechanism for both.
  2. Create docker images for both and a docker-compose using jhipster docker-compose
  3. Launch docker compose and log-in to the generated application.
  4. After 5 minutes, the pages within the "Entities" menu will not contain any content when loaded. If you don't want to wait 5 minutes, set the "Access Token Lifespan" setting in the KeyCloak admin console (Realm settings > Tokens) to a lower value.

In the browser console, it can be seen that the requests to the microservice are responded with 401.
The docker-compose console says:

keycloak_1              | 13:59:57,066 WARN  [org.keycloak.events] (default task-15) type=USER_INFO_REQUEST_ERROR, realmId=jhipster, clientId=null, userId=null, ipAddress=172.18.0.9, error=invalid_token, auth_method=validate_access_token
requirement-app_1       | 2017-12-30 13:59:57.076  WARN 6 --- [ XNIO-2 task-13] o.s.b.a.s.o.r.UserInfoTokenServices      : Could not fetch user details: class org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException, Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it.
##### **Related issues** https://github.com/jhipster/generator-jhipster/issues/6672 ##### **Suggest a Fix** I guess that a new access token should be fetched once the lifespan is over. ##### **JHipster Version(s)** 4.13.1 ##### **JHipster configuration** **Microservice application**
.yo-rc.json file
{
  "generator-jhipster": {
    "promptValues": {
      "packageName": "org.securityrat.requirementms"
    },
    "jhipsterVersion": "4.13.1",
    "baseName": "requirement",
    "packageName": "org.securityrat.requirementms",
    "packageFolder": "org/securityrat/requirementms",
    "serverPort": "8081",
    "authenticationType": "oauth2",
    "cacheProvider": "no",
    "clusteredHttpSession": false,
    "websocket": false,
    "databaseType": "sql",
    "devDatabaseType": "h2Disk",
    "prodDatabaseType": "mariadb",
    "searchEngine": false,
    "messageBroker": false,
    "serviceDiscoveryType": "consul",
    "buildTool": "maven",
    "enableSocialSignIn": false,
    "enableSwaggerCodegen": false,
    "jwtSecretKey": "replaced-by-jhipster-info",
    "enableTranslation": false,
    "applicationType": "microservice",
    "testFrameworks": [
      "gatling",
      "cucumber"
    ],
    "jhiPrefix": "jhi",
    "clientPackageManager": "yarn",
    "skipClient": true,
    "skipUserManagement": true
  }
}

Gateway:


.yo-rc.json file

{
  "generator-jhipster": {
    "promptValues": {
      "packageName": "org.securityrat.gateway"
    },
    "jhipsterVersion": "4.13.1",
    "baseName": "gateway",
    "packageName": "org.securityrat.gateway",
    "packageFolder": "org/securityrat/gateway",
    "serverPort": "8080",
    "authenticationType": "oauth2",
    "cacheProvider": "hazelcast",
    "enableHibernateCache": false,
    "clusteredHttpSession": false,
    "websocket": false,
    "databaseType": "sql",
    "devDatabaseType": "h2Disk",
    "prodDatabaseType": "mariadb",
    "searchEngine": false,
    "messageBroker": false,
    "serviceDiscoveryType": "consul",
    "buildTool": "maven",
    "enableSocialSignIn": false,
    "enableSwaggerCodegen": false,
    "clientFramework": "angularX",
    "useSass": true,
    "clientPackageManager": "yarn",
    "applicationType": "gateway",
    "testFrameworks": [
      "gatling",
      "cucumber",
      "protractor"
    ],
    "jhiPrefix": "jhi",
    "enableTranslation": false
  }
}

docker-compose


.yo-rc.json file

{
  "generator-jhipster": {
    "appsFolders": [
      "gateway",
      "requirement"
    ],
    "directoryPath": "../",
    "gatewayType": "zuul",
    "monitoring": "prometheus",
    "serviceDiscoveryType": "consul",
    "jwtSecretKey": "replaced-by-jhipster-info"
  }
}

Entity configuration(s) entityName.json files generated in the .jhipster directory


JDL entity definitions

/**
 * Requirements are organized in different sets.
 * These define also the structure of contained requirements.
 * Selecting a suitable requirement set is the initial step when creating a "case".
 */
entity RequirementSet (requirement_set) {
  name String required,
  description TextBlob,
  showOrder Integer,
  active Boolean required
}
/**
 * The "core" part of a particular requirement.
 */
entity Skeleton (skeleton) {
  name String required,
  description TextBlob,
  showOrder Integer,
  active Boolean required
}
/**
 * Describes one group/collection of requirement attributes.
 * E.g. "Criticality" for Low, Medium, High.
 */
entity AttributeGroup (attribute_group) {
  name String required,
  description TextBlob,
  type AttributeType required,
  showOrder Integer,
  active Boolean required
}
/**
 * Every requirement can be classified in many ways
 * according to different attributes.
 */
entity Attribute (attribute) {
  name String required,
  description TextBlob,
  showOrder Integer,
  active Boolean required
}
/**
 * Describes properties of one type of a requirement extension.
 */
entity ExtensionDefinition (extension_definition) {
  name String required,
  description TextBlob,
  section ExtensionSection required,
  type ExtensionType,
  showOrder Integer,
  active Boolean required
}
/**
 * Requirement extension (extending the requirement skeleton).
 */
entity Extension (extension) {
  content TextBlob required,
  description TextBlob,
  showOrder Integer,
  active Boolean required
}
/**
 * Implements ternary relation of req. skeleton, attribute an extension.
 * Requirement extension can be added to a skeleton if a particular attribute is set.
 */
entity SkAtEx (sk_at_ex)

enum AttributeType {
  FE_TAG,
  PARAMETER,
  CATEGORY
}

enum ExtensionSection {
  STATUS,
  ENHANCEMENT
}

enum ExtensionType {
  ENUM,
  FREETEXT
}

relationship OneToMany {
  RequirementSet{skeleton} to Skeleton{requirementSet(name)},
  RequirementSet{attributeGroup} to AttributeGroup{requirementSet(name)},
  AttributeGroup{attribute} to Attribute{attributeGroup(name)},
  RequirementSet{extensionDefinition} to ExtensionDefinition{requirementSet(name)},
  ExtensionDefinition{extension} to Extension{extensionDefinition(name)},
  Skeleton{skAtEx} to SkAtEx{skeleton(name)},
  Attribute{skAtEx} to SkAtEx{attribute(name)},
  Extension{skAtEx} to SkAtEx{extension(content)}
}
relationship ManyToOne {
  Attribute{parent} to Attribute
}

paginate RequirementSet, Skeleton, AttributeGroup, Attribute, ExtensionDefinition, Extension, SkAtEx with infinite-scroll
service RequirementSet, Skeleton, AttributeGroup, Attribute, ExtensionDefinition, Extension, SkAtEx with serviceClass
microservice RequirementSet, Skeleton, AttributeGroup, Attribute, ExtensionDefinition, Extension, SkAtEx with requirement
filter RequirementSet, Skeleton, AttributeGroup, Attribute, ExtensionDefinition, Extension, SkAtEx

Browsers and Operating System

jhipster devbox
openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-0ubuntu0.17.04.2-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)

git version 2.11.0

node: v8.9.3

npm: 5.6.0

bower: 1.8.2

gulp:
[15:22:49] CLI version 3.9.1

yeoman: 2.0.0

yarn: 1.3.2

Docker version 17.11.0-ce, build 1caf76c

  • [X] Checking this box is mandatory (this is just to show you read everything)
OIDOAuth2

Most helpful comment

Thanks for putting together the sample @ImperfectClone.

I managed to figure out the issue and it's mostly mis-configuration.

Gateway

  • MicroserviceSecurityConfiguration is configured as a @EnableResourceServer when it's not a Resource Server as your Microservice app acts as the Resource Server
  • Your are providing a @Bean of type UserInfoTokenServices in MicroserviceSecurityConfiguration but this bean is auto-configured for you by @EnableOAuth2Sso. Even if you were to provide your own UserInfoTokenServices, you need to at least set the OAuth2RestTemplate via UserInfoTokenServices.setRestTemplate. I would recommend to allow Boot to auto-configure this for you so don't provide your own UserInfoTokenServices @Bean if you can avoid it.
  • You're configuring a JwtTokenStore. This is only required by a @EnableResourceServer
  • You don't need to configure the following properties: token-info-uri, prefer-token-info, jwt.key-uri. NOTE: user-info-uri is required by @EnableOAuth2Sso

Microservice

  • MicroserviceSecurityConfiguration is configured with a @Bean of type UserInfoTokenServices which is used by @EnableOAuth2Sso. However, you don't have @EnableOAuth2Sso in this app which makes sense given that it's a Resource Server. This is where the main issue lies. When the Resource Server receives a request (e.g. /transformers) it will use the UserInfoTokenServices to call the user-info-uri to get the User's Info. First off, this flow only should happen in the @EnableOAuth2Sso app (Gateway). Second, the client that makes the call to user-info-uri is client-id=internal, which is not the client that has the refresh_token (client-id=web_app has it in Gateway). So when client internal calls the user-info-uri using the access token granted to client web_app, after the token expires, a 401 is triggered by the Resource Server and should be caught by the Javascript client in Gateway to handle it. Given that Gateway is not using OAuth2RestTemplate (which handles refresh_token), the javascript client will need to handle the 401 and perform the refresh_token grant. It would be much easier if you configured things in such a way so that OAuth2RestTemplate handles this for you.

So a few things to fix in your configuration. Hope this helps.

All 15 comments

This issue breaks the whole idea of Keycloak at the moment.

@mraible does any of your recent PRs address this?

@deepu105 I don't think so. I have a hard time believing Spring Security doesn't handle refresh tokens properly, but I could be wrong. Maybe @jgrandja has some advice?

@deepu105 @mraible Based on the log provided:

org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException, Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it.

This tells me that the OAuth Client is not configured with an authorization_grant that supports refresh_token grant. Is it possible your client is configured with implicit or client_credentials grant?

@jgrandja We use Spring Security's OAuth support and @EnableOAuth2Sso in JHipster. See the following files for how it's configured:

Related: do we need all the properties in application.yml? https://github.com/jhipster/generator-jhipster/issues/7281

Looking at the code and OAuth2RestTemplate is configured with AuthorizationCodeAccessTokenProvider so it is using authorization_grant which supports refresh_token grant. I'm not sure what's going on without getting deeper into the issue.

Are you able to publish to a repo that reproduces the issue so I can debug for you?

Thanks @mraible and @jgrandja for looking into this; I've also run into the same issue as the OP.

We use Spring Security's OAuth support and @EnableOAuth2Sso in JHipster. See the following files for how it's configured:

I think that, since the issue relates to microservices, SecurityConfiguration.java.ejs#L76 maybe isn't the relevant file.

I can see the @EnableOAuth2Sso annotation being applied in the gateway at OAuth2SsoConfiguration.java.ejs#L36. I can't find it anywhere in my microservice app.

Are you able to publish to a repo that reproduces the issue so I can debug for you?

I've made a repo which can be used to reproduce the issue with JHipster 4.14.1. It contains two apps: a gateway and microservice, which I've creatively named gateway and microservice. The below also assumes that Docker is available to create Registry and KeyCloak containers from the default JHipster images.

Recreating the issue

Clone the example repository and cd into it:

git clone https://gitlab.com/ImperfectClone/jhipster-issue-6929.git && cd jhipster-issue-6929

KeyCloak and Registry (Docker)

Start KeyCloak in Docker - all configuration is per the default provided by JHipster, except for accessTokenLifespan in jhipster-realm.json. This has been reduced from 300 to 60 seconds, just to speed up reproducing the issue (the issue is the same with this set to 300, it just takes five minutes instead of one to manifest):

docker-compose -f gateway/src/main/docker/keycloak.yml up -d

Start the JHipster Registry in Docker:

docker-compose -f gateway/src/main/docker/jhipster-registry.yml up -d

(If KeyCloak isn't up in time, the Registry will fail to start and the above command will need to be repeated when KeyCloak is ready.)

Gateway

From the gateway directory run:

  • yarn install or npm i to fetch node_modules and
  • bash mvnw -Pdev,webpack to run the Gateway.

Microservice

From the microservice directory, run bash mvnw to start it.

Steps to reproduce via UI

Once everything has been started, visit http://localhost:8080.

Click 'sign in' and enter username 'admin' and password 'admin' into the KeyCloak login screen.

Select the 'Transformer' entry from the 'Entities' menu. This should present a list of a few Transformers (which have been added via the microservice Liquibase). The 'View' and 'Edit' buttons on the UI will work as expected for the first 60 seconds.

After waiting for >60 seconds, click on 'View' or 'Edit'.

The error message reported by the OP will display in the microservice log:

Could not fetch user details: class 
org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException
Unable to obtain a new access token for resource 'null'. The provider manager is not 
configured to support it.

At this point the front-end will redirect to the home route. The value of the JSESSIONID cookie gets updated here, and, navigating back to 'Transformer', everything works as expected again for another 60 seconds.

@ImperfectClone Thanks but I would prefer a stripped down sample (without docker) that reproduces the issue. Ideally, samples that I can startup locally.

Thanks but I would prefer a stripped down sample (without docker) that reproduces the issue. Ideally, samples that I can startup locally.

@jgrandja Thanks - I'm happy to adjust the sample[0] if I can, though I'm afraid I don't know enough about the Spring Security part to be able to judge which bits I should strip down. From a JHipster standpoint, this is (as far as I can see) the minimal possible app that recreates the issue.

On the local/Docker point: the above instructions do run the Gateway and Microservice locally and without Docker - it's just the Registry and KeyCloak that are run in Docker; this seemed (certainly in KeyCloak's case) to be the easiest way to provide this, since the JHipster image + conf files pre-configure it to 'just work' with the Gateway and microservice, whereas to get them working with any external/pre-existing KeyCloak instance you'd need to modify the application configuration. So, you could just ignore the KeyCloak and Registry sections of my post and modify the conf to use whatever Eureka and KeyCloak instances you wanted to instead, and not use the Docker images - but maybe I've misunderstood what you meant here.

[0] https://gitlab.com/ImperfectClone/jhipster-issue-6929.git

Thanks for putting together the sample @ImperfectClone.

I managed to figure out the issue and it's mostly mis-configuration.

Gateway

  • MicroserviceSecurityConfiguration is configured as a @EnableResourceServer when it's not a Resource Server as your Microservice app acts as the Resource Server
  • Your are providing a @Bean of type UserInfoTokenServices in MicroserviceSecurityConfiguration but this bean is auto-configured for you by @EnableOAuth2Sso. Even if you were to provide your own UserInfoTokenServices, you need to at least set the OAuth2RestTemplate via UserInfoTokenServices.setRestTemplate. I would recommend to allow Boot to auto-configure this for you so don't provide your own UserInfoTokenServices @Bean if you can avoid it.
  • You're configuring a JwtTokenStore. This is only required by a @EnableResourceServer
  • You don't need to configure the following properties: token-info-uri, prefer-token-info, jwt.key-uri. NOTE: user-info-uri is required by @EnableOAuth2Sso

Microservice

  • MicroserviceSecurityConfiguration is configured with a @Bean of type UserInfoTokenServices which is used by @EnableOAuth2Sso. However, you don't have @EnableOAuth2Sso in this app which makes sense given that it's a Resource Server. This is where the main issue lies. When the Resource Server receives a request (e.g. /transformers) it will use the UserInfoTokenServices to call the user-info-uri to get the User's Info. First off, this flow only should happen in the @EnableOAuth2Sso app (Gateway). Second, the client that makes the call to user-info-uri is client-id=internal, which is not the client that has the refresh_token (client-id=web_app has it in Gateway). So when client internal calls the user-info-uri using the access token granted to client web_app, after the token expires, a 401 is triggered by the Resource Server and should be caught by the Javascript client in Gateway to handle it. Given that Gateway is not using OAuth2RestTemplate (which handles refresh_token), the javascript client will need to handle the 401 and perform the refresh_token grant. It would be much easier if you configured things in such a way so that OAuth2RestTemplate handles this for you.

So a few things to fix in your configuration. Hope this helps.

@jgrandja Thank you very much for taking the time to diagnose these configuration issues! I'll work on fixing these problems in my project.

I've applied @jgrandja's suggestion to use OAuth2RestTemplate to my sample - in a new branch, minimal_fix. The diff with the version reproducing the issue can be viewed here.

To checkout on the fix branch:
git clone https://gitlab.com/ImperfectClone/jhipster-issue-6929.git && cd jhipster-issue-6929 && git checkout minimal_fix

(Of course, just git checkout master to get back to the state that reproduces the issue.)

This represents an attempt to execute the smallest change I could find that would allow the gateway to successfully refresh an expired token before passing requests to the microservice. In other words, I've not yet made the other configuration changes suggested, but the minimum change I could get working that produces the desired behaviour from a microservices + KeyCloak app produced by the current version of the generator.

Furthermore, I'm sure my approach isn't the optimal way to apply this fix - but it is working and therefore may provide a useful workaround to others experiencing this issue (at your own risk if I've broken the security entirely without realising!) until a version of the generator in which this issue is resolved becomes available.

Description of changes

What I've done is to add an @Bean for OAuth2RestTemplate to MicroserviceSecurityConfiguration.java:

@Bean
public OAuth2RestTemplate oAuth2RestTemplate(
    OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
    OAuth2ClientContext oAuth2ClientContext
) {
    return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oAuth2ClientContext);
}

I've then changed TokenRelayFilter.run() to extract the token from the OAuth2RestTemplate, replacing the use of the static method in AuthorizationHeaderUtil for this purpose.

public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String TOKEN_TYPE = "Bearer";

private final Logger log = LoggerFactory.getLogger(TokenRelayFilter.class);

@Autowired
private OAuth2RestTemplate oAuth2RestTemplate;

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    try {
        ctx.addZuulRequestHeader(
            AUTHORIZATION_HEADER,
            String.format("%s %s", TOKEN_TYPE, this.oAuth2RestTemplate.getAccessToken())
        );
    } catch (Exception e) {
        log.debug(
            "Exception " + e.getClass() + " for expired session for " +
                ctx.getRequest().getRemoteUser()
        );
    }
    return null;
}

I'm catching all Exceptions here so that, when the session does expire (testable by reducing 'Tokens -> SSO Session Idle' in KeyCloak UI or ssoSessionIdleTimeout in jhipster-realm.json) and OAuth2RestTemplate.getAccessToken() throws, the exception doesn't prevent the null from being returned, which in turn would prevent the user from getting redirected to the KeyCloak login page and instead present them with an empty view of the protected route (I think due to HTTP 500 rather then 401 being received on the client). The exception seems always to be UserRedirectRequiredException - but I wasn't confident there were no other cases so left the generic Exception handler there for now.

I have incorporated most of the point discussed here on this repository :
https://github.com/farrault/jhipster_oauth2_microservice/pull/1/files

I'm very open to discuss with you the different points.

@farrault I attempted to make the changes in your repository in https://github.com/jhipster/generator-jhipster/pull/7666. I doubt this has everything it needs, and it might break monoliths. Hopefully, Travis tests can figure out what breaks. Please review and let me know what I missed.

For me this is solved with the latest changes from @farrault and @mraible so I'm closing this

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Steven-Garcia picture Steven-Garcia  路  3Comments

dronavallisaikrishna picture dronavallisaikrishna  路  3Comments

SudharakaP picture SudharakaP  路  3Comments

frantzynicolas picture frantzynicolas  路  3Comments

trajakovic picture trajakovic  路  4Comments