jhipster / generator-jhipster

JHipster is a development platform to quickly generate, develop, & deploy modern web applications & microservice architectures.
https://www.jhipster.tech
Apache License 2.0
21.49k stars 4.02k forks source link

Gateway as ResourceServer in JHipster 6.x.x / CORS preflight OPTIONS #9903

Closed jsm174 closed 5 years ago

jsm174 commented 5 years ago
Overview of the issue

In JHipster 5.8.2 we were using a Gateway as a resource server by using @ruddell 's ResourceServerConfiguration.java found here.

We had a custom react website (with a different domain) that manages oauth2 tokens from keycloak using keycloak.js

We would then add the authorization header using those tokens while calling the microservice using axios.

In JHipster 6.x.x, oauth2 support has changed significantly. According to https://github.com/jhipster/generator-jhipster/issues/9276, @EnableResourceServer is no longer recommended.

With a stock Gateway, when attempting to access the microservice, Chrome will do a CORS preflight call using OPTIONS. Because the preflight does not send the Authorization header, AuthorizationHeaderUtil.java will crash when trying to determine the OAuth2AuthorizedClient (oauthToken is null):

   public Optional<String> getAuthorizationHeader() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
            oauthToken.getAuthorizedClientRegistrationId(),
            oauthToken.getName());

I tried modifying getAuthorizationHeader() to just return Optional.empty() but then I start receiving 401's which I think is because of AnonymousAuthenticationToken:

o.s.s.w.a.AnonymousAuthenticationFilter  : Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9c243cda: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffffa64e: RemoteIpAddress: 192.168.1.237; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
Motivation for or Use Case

Would really like to update our app to latest JHipster prior to going live.

Reproduce the error
Related issues
Suggest a Fix
JHipster Version(s)
gateway@0.0.0 /Users/jmillard/jhipster/latest3/gateway
└── generator-jhipster@6.1.0
Environment and Tools

openjdk version "12.0.1" 2019-04-16 OpenJDK Runtime Environment AdoptOpenJDK (build 12.0.1+12) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 12.0.1+12, mixed mode, sharing)

git version 2.20.1 (Apple Git-117)

node: v10.15.3

npm: 6.4.1

Docker version 18.09.2, build 6247962

docker-compose version 1.23.2, build 1110ad01

Browsers and Operating System
mraible commented 5 years ago

I coded things in JHipster 6 so a resource server is available automatically when using OAuth 2 for Auth. I’ve been testing it this week with an Ionic client and it seems to work just fine. I’ve only tested a monolith though, I haven’t tested it with a gateway. I’ll try to do that today.

On Jun 12, 2019, at 07:15, Jason Millard notifications@github.com wrote:

Overview of the issue

In JHipster 5.8.2 we were using a Gateway as a resource server by using @ruddell 's ResourceServerConfiguration.java found here.

We had a custom react website (with a different domain) that manages oauth2 tokens from keycloak using keycloak.js

We would then add the authorization header using those tokens while calling the microservice using axios.

In JHipster 6.x.x, oauth2 support has changed significantly. According to #9276, @EnableResourceServer is no longer recommended.

With a stock Gateway, when attempting to access the microservice, Chrome will do a CORS preflight call using OPTIONS. Because the preflight does not send the Authorization header, AuthorizationHeaderUtil.java will crash when trying to determine the OAuth2AuthorizedClient (oauthToken is null):

public Optional getAuthorizationHeader() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
    OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
        oauthToken.getAuthorizedClientRegistrationId(),
        oauthToken.getName());

I tried modifying getAuthorizationHeader() to just return Optional.empty() but then I start receiving 401's which I think is because of AnonymousAuthenticationToken:

o.s.s.w.a.AnonymousAuthenticationFilter : Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9c243cda: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffffa64e: RemoteIpAddress: 192.168.1.237; SessionId: null; Granted Authorities: ROLE_ANONYMOUS' Motivation for or Use Case

Would really like to update our app to latest JHipster prior to going live.

Reproduce the error

Related issues

Suggest a Fix

JHipster Version(s)

gateway@0.0.0 /Users/jmillard/jhipster/latest3/gateway └── generator-jhipster@6.1.0

Environment and Tools

openjdk version "12.0.1" 2019-04-16 OpenJDK Runtime Environment AdoptOpenJDK (build 12.0.1+12) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 12.0.1+12, mixed mode, sharing)

git version 2.20.1 (Apple Git-117)

node: v10.15.3

npm: 6.4.1

Docker version 18.09.2, build 6247962

docker-compose version 1.23.2, build 1110ad01

Browsers and Operating System

Checking this box is mandatory (this is just to show you read everything) — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or mute the thread.

vishal423 commented 5 years ago

This scenario is currently broken on Gateway due to unconditional typecast to OAuth2AuthenticationToken, which wouldn't hold true in authorization server scenario (token type would be JwtAuthenticationToken). It's fix is also included in #9874

jsm174 commented 5 years ago

@vishal423 - I agree about the unconditional typecast, but in my use case, it's not even a JwtAuthenticationToken.

jsm174 commented 5 years ago

I temporarily disabled web security in chrome, to see if I could get avoid the OPTIONS preflight call:

open /Applications/Google\ Chrome.app --args --disable-web-security --user-data-dir

Sure enough, I now get the JwtAuthenticationToken for SecurityContextHolder.getContext().getAuthentication();

So I will have to wait for https://github.com/jhipster/generator-jhipster/pull/9874

As for the CORS issue, I tried to step through CorsFilter.java. corsConfiguration is null so it just moves on to the next filter.

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        if (CorsUtils.isCorsRequest(request)) {
            CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
            if (corsConfiguration != null) {
                boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
                if (!isValid || CorsUtils.isPreFlightRequest(request)) {
                    return;
                }
            }
        }

        filterChain.doFilter(request, response);
    }
jsm174 commented 5 years ago

Okay, I think I figured out the CORS part of this issue.

PR https://github.com/jhipster/generator-jhipster/pull/9262 updated the microservice path prefix to begin with /services/

In the Gateway's WebConfigurer.java, I added the following:

source.registerCorsConfiguration("/services/*/api/**", config);

to:

@Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = jHipsterProperties.getCors();
        if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) {
            log.debug("Registering CORS filter");
            source.registerCorsConfiguration("/api/**", config);
            source.registerCorsConfiguration("/management/**", config);
            source.registerCorsConfiguration("/v2/api-docs", config);
            source.registerCorsConfiguration("/*/api/**", config);
            source.registerCorsConfiguration("/services/*/api/**", config);
            source.registerCorsConfiguration("/*/management/**", config);
        }
        return new CorsFilter(source);
    }

That at least gets me to getAuthorizationHeader() with a JwtAuthenticationToken!

mraible commented 5 years ago

@jsm174 I just tried creating a Gateway with 6.1.0 and using an Ionic client to communicate with it. It all works fine and I didn't have any CORS issues. I'm using OAuth 2.0 for auth. Here's the output of jhipster info in my gateway project.

JHipster Version(s)
oauthgateway@0.0.0 /Users/mraible/oauthgateway
└── generator-jhipster@6.1.0 
JHipster configuration, a .yo-rc.json file generated in the root folder
.yo-rc.json file
{
  "generator-jhipster": {
    "promptValues": {
      "packageName": "com.mycompany.myapp",
      "nativeLanguage": "en"
    },
    "jhipsterVersion": "6.1.0",
    "applicationType": "gateway",
    "baseName": "oauthgateway",
    "packageName": "com.mycompany.myapp",
    "packageFolder": "com/mycompany/myapp",
    "serverPort": "8080",
    "authenticationType": "oauth2",
    "cacheProvider": "ehcache",
    "enableHibernateCache": true,
    "websocket": false,
    "databaseType": "sql",
    "devDatabaseType": "h2Disk",
    "prodDatabaseType": "mysql",
    "searchEngine": false,
    "messageBroker": false,
    "serviceDiscoveryType": "eureka",
    "buildTool": "maven",
    "enableSwaggerCodegen": false,
    "useSass": true,
    "clientPackageManager": "npm",
    "clientFramework": "angularX",
    "clientTheme": "none",
    "clientThemeVariant": "",
    "testFrameworks": ["protractor"],
    "jhiPrefix": "jhi",
    "entitySuffix": "",
    "dtoSuffix": "DTO",
    "otherModules": [],
    "enableTranslation": true,
    "nativeLanguage": "en",
    "languages": ["en"]
  }
}

JDL for the Entity configuration(s) entityName.json files generated in the .jhipster directory
JDL entity definitions
entity Blog {
  name String required minlength(3),
  handle String required minlength(2)
}
entity Entry {
  title String required,
  content TextBlob required,
  date Instant required
}
entity Tag {
  name String required minlength(2)
}
relationship ManyToOne {
  Blog{user(login)} to User,
  Entry{blog(name)} to Blog
}
relationship ManyToMany {
  Entry{tag(name)} to Tag{entry}
}

paginate Entry, Tag with infinite-scroll

Environment and Tools

openjdk version "11.0.2" 2019-01-15 OpenJDK Runtime Environment 18.9 (build 11.0.2+9) OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

git version 2.20.1 (Apple Git-117)

node: v10.15.3

npm: 6.9.0

yeoman: 2.0.6

Docker version 18.09.2, build 6247962

docker-compose version 1.23.2, build 1110ad01

jsm174 commented 5 years ago

@mraible Thank you for testing. Was your ionic app hitting a microservice that is proxied via the gateway?

After stepping through this, I'm positive corsFilter needs to be updated to account for the microservice path prefix.

mraible commented 5 years ago

I only generated the entities on my gateway. I did not create any microservices. Let me try that.

mraible commented 5 years ago

@jsm174 After testing everything with microservices, it seems there's some additional logic that's needed to get the access token (and user information) from a JwtAuthenticationToken. https://github.com/jhipster/generator-jhipster/pull/9905 does that. I did not need to make any CORS changes when testing this.

jsm174 commented 5 years ago

Thanks. Thats great!

Is your gateway's application.yml zuul.prefix set to /services?

zuul: # those values must be configured depending on the application specific needs
  sensitive-headers: Cookie,Set-Cookie #see https://github.com/spring-cloud/spring-cloud-netflix/issues/3126
  host:
    max-total-connections: 1000
    max-per-route-connections: 100
  prefix: /services
  semaphore:
    max-semaphores: 500

When I call the microservice directly, I'm using axios and hitting

http://gateway:8080/services/testmicroservice/api/sampleservice

mraible commented 5 years ago

@jsm174 Yes, my zuul.prefix is set to /services. I did not change anything in application.yml:

zuul: # those values must be configured depending on the application specific needs
  sensitive-headers: Cookie,Set-Cookie #see https://github.com/spring-cloud/spring-cloud-netflix/issues/3126
  host:
    max-total-connections: 1000
    max-per-route-connections: 100
  prefix: /services
  semaphore:
    max-semaphores: 500

While we're on the subject, having different API endpoints for a monolith vs a microservices architecture seems like a bad design to me. Isn't the whole point of an API gateway to figure out the URLs for you?

It seems odd to me that my Ionic client has to worry about if it should call http://localhost:8080/api/blogs (for a monolith) or http://localhost:8080/services/blog/api/blogs (for microservices). IMO, it should be possible to call /api/blogs and let the backend figure out where it gets its data from.

WDYT @jhipster/developers?

jsm174 commented 5 years ago

@mraible I really appreciate you taking the time to look into this.

I am at a loss why CORS is not an issue for you and it is for me.

All I can say is my gateway and webapp (that accesses the microservices) have different domains. ie http://localhost:8080 vs http://web.mydomain.com:3000 and PR https://github.com/jhipster/generator-jhipster/pull/9906 fixes it.

TBH, I don't know much about ionic. If it's just a web browser wrapper, I can't see it working different than Chrome. It would have to be executing the CORS preflights.

mraible commented 5 years ago

Ionic 4 uses Angular and I’m running it in Chrome so it’s not that different from JHipster’s UI. It runs on localhost:8100 and JHipster runs on 8080. Different ports works the same as different domains AFAIK. Are you using JWT for Auth? I’m using OIDC.

On Jun 12, 2019, at 18:06, Jason Millard notifications@github.com wrote:

@mraible I really appreciate you taking the time to look into this.

I am at a loss why CORS is not an issue for you and it is for me.

All I can say is my gateway and webapp (that accesses the microservices) have different domains. ie http://localhost:8080 vs http://web.mydomain.com:3000 and PR #9906 fixes it.

TBH, I don't know much about ionic. If it's just a web browser wrapper, I can't see it working different than Chrome. It would have to be executing the CORS preflights.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

jsm174 commented 5 years ago

Different ports works the same as different domains AFAIK.

Agreed!

Are you using JWT for Auth? I’m using OIDC.

Our react website connects to a Keycloak server using keycloak-js. So I'm assuming it is uses oauth2 access tokens.

I'll try to put a sample together.

mraible commented 5 years ago

@jsm174 I closed this issue because it's resolved for me. If you're using keycloak-js, that's not something we currently support in JHipster. I'm not saying I'm against supporting it, but we love to be IdP agnostic. We test against Keycloak and Okta currently, but we should strive to work with all OIDC providers.

jsm174 commented 5 years ago

@mraible Isn't keycloak-js from Keycloak?

https://www.npmjs.com/package/keycloak-js

mraible commented 5 years ago

Yes, but we don’t currently use it in JHipster. Everything OAuth related is handled by Spring Security. There is no client side code, except in the mobile app modules (for React Native and Ionic), which both use AppAuth.

On Jun 13, 2019, at 06:24, Jason Millard notifications@github.com wrote:

@mraible Isn't keycloak-js from Keycloak?

https://www.npmjs.com/package/keycloak-js

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

jsm174 commented 5 years ago

@mraible understood. I will look into using AppAuth.

I did put an example project to recreate this issue at:

https://github.com/jsm174/jh610-cors-issue-demo

You'll see the keycloak use is pretty simplistic:

https://github.com/jsm174/jh610-cors-issue-demo/blob/master/frontend/src/App.js

imaxkhan commented 1 year ago

hi im using this sample https://github.com/amrutprabhu/keycloak-spring-cloud-gateway-and-resource-server.git i changed it to connect spring authorization server which is federated with azure b2c when i hit the sign in button it will navigate to azure b2c login page and after that the login of jhipster show my user name and every thing is fine.. i can call any api inside gateway but when i call another resource server like /product which the corresponding route is defined in properties i receive unauthorized from my resource server. i check and found that access token is not present in resource server.. configuration about token relay is present. im using authorization_code grant and azure b2c give me id_token and access token..but this jhipster project not realying access token to resource server... any help? its two weeks im working on it

mraible commented 1 year ago

Hello @imaxkhan. Please don't comment on closed issues because most people won't see them and you won't get much help. I'd suggest you ask your question on Stack Overflow with the "jhipster" tag.

We currently don't have instructions for integrating with Spring Authorization Server, but it should be possible with our OAuth support.