New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak #2

Open
sdoxsee opened this Issue Apr 20, 2017 · 78 comments

Comments

Projects
None yet
6 participants
@sdoxsee

sdoxsee commented Apr 20, 2017

Hi @gmarziou. I'm excited to see the discussion here https://groups.google.com/forum/m/?hl=en#!topic/jhipster-dev/8ehVzTvx9D8 about the new gateway and getting user management out of jhipster gateway apps. Keycloak is an excellent option for that user aspect. I was noticing that you were using keycloak API adapters to connect to it in your sample app. Perhaps I'm misunderstanding but I think it would be far better to consider keycloak as one of many possible oauth2 authorization servers and to interact with it from jhipster using standard sso oauth2 (openid connect) rather than keycloak SDKs. As good as keycloak is, I think it would really limit jhipsters ability to be adopted for microservice projects when it's that tied to keycloak because companies likely already have their own oauth2 authorization servers and won't likely drop them to use keycloak. Keycloak is among many options...

MitreID connect
Cloudfoundry UAA
Even Google
See http://openid.net/developers/certified/ for just a taste

Using standard oauth2 allows much more plug and play with any provider and is very easy to configure (see https://github.com/sdoxsee/jhipster-openid-connect-microservices for example...Sorry it's based on such an old jhipster) . Keycloak should be able to easily be configured in that way. I'd love to be part of the conversation but can't comment on the Google forum so thought I'd get in touch here. Let me know how I can be of help. Thanks

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Apr 20, 2017

Owner

Yes I have considered this approach too and if you look at my sample .yo-rc.json file you'll see that I started from a UUA authType but the fact is that the spring security oauth application properties are simple URLs and do not match easily with Keycloak concepts like client, realm, ... So it means that it would be JHipster's work to build the auth URL, it could be done for sure and maybe by calling some classes from keycloak spring-boot-adapter.

So I like your oauth2 approach but for me it covers only the scope of spring-security checking and parsing tokens, the configuration part seems to be enough addressed and this is the part that may cause some trouble to our users as they'll use Keycloak doc.

So maybe an hybrid approach would work.

Another point I haven't addressed is that most examples in JS use a static keycloak.json file, this cannot work for real, we need to build a dynamic keycloak.json based on gateway's application properties.

Owner

gmarziou commented Apr 20, 2017

Yes I have considered this approach too and if you look at my sample .yo-rc.json file you'll see that I started from a UUA authType but the fact is that the spring security oauth application properties are simple URLs and do not match easily with Keycloak concepts like client, realm, ... So it means that it would be JHipster's work to build the auth URL, it could be done for sure and maybe by calling some classes from keycloak spring-boot-adapter.

So I like your oauth2 approach but for me it covers only the scope of spring-security checking and parsing tokens, the configuration part seems to be enough addressed and this is the part that may cause some trouble to our users as they'll use Keycloak doc.

So maybe an hybrid approach would work.

Another point I haven't addressed is that most examples in JS use a static keycloak.json file, this cannot work for real, we need to build a dynamic keycloak.json based on gateway's application properties.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Apr 20, 2017

Awesome @gmarziou. The way that we configure to jhipster UAA should be similar. Spring security works great with keycloak. "realm" is just part of the issuer URL and "client" is a standard oauth2 concept (client_id, no?). All the URLs for OpenID Connect Providers (like keycloak) expose the URLs that are needed via a well-defined URL http://keycloakhost:keycloakport/auth/realms/{realm}/.well-known/openid-configuration

this would give you a map of all your endpoints in a standard way so that if your keycloak instance/realm was at http://localhost:8080/auth/realms/demo then you'd find the following json at http://localhost:8080/auth/realms/demo/.well-known/openid-configuration

{
    "issuer":"http://localhost:8080/auth/realms/demo",
    "authorization_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth",
    "token_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/token",
    "userinfo_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/userinfo",
    "end_session_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/logout",
    "jwks_uri":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/certs",
    "grant_types_supported":["authorization_code","refresh_token","password"],
    "response_types_supported":["code"],
    "subject_types_supported":["public"],
    "id_token_signing_alg_values_supported":["RS256"],
    "response_modes_supported":["query"]
}

the keycloak concept of realm (as far as i can tell) is just an "instance" of an authorization provider (of which you could have several---but not so important in the basic case). So, you'd connect to a keycloak authorization_endpoint and get tokens from token_endpoint, etc. You'd configure spring-security-oauth2 to look for the public key to verify JWTs at jwks_uri

The problem with using the keycloak spring-boot-adapter is that it makes it keycloak-specific which I don't think it needs to be. I've used keycloak for other projects without using any keycloak code on the client. It would be a shame to limit ourselves to a single provider what we can easily connect to any conforming provider with with open standards.

for example, if keycloak was on my local and I'd configured a client "admin-portal:secret" on keycloak, I could authenticate with keycloak simply by changing my application.yml config from https://github.com/sdoxsee/jhipster-openid-connect-microservices/blob/master/admin-portal/src/main/resources/config/application.yml#L35 to something like this (based on the info from the keycloak .well-known/openid-configuration endpoint.

targets.uaa: http://localhost:8080/auth/realms/demo

security.oauth2:
  client:
    accessTokenUri: ${targets.uaa}/protocol/openid-connect/token
    userAuthorizationUri: ${targets.uaa}/protocol/openid-connect/auth
    clientId: admin-portal
    clientSecret: secret
    # scope: openid,menu.read,menu.write,order.admin
    scope: openid,foo.read,foo.write,foo.admin
  resource:
    user-info-uri: ${targets.uaa}/protocol/openid-connect/userinfo
    jwt.keyUri: ${targets.uaa}/protocol/openid-connect/certs

sdoxsee commented Apr 20, 2017

Awesome @gmarziou. The way that we configure to jhipster UAA should be similar. Spring security works great with keycloak. "realm" is just part of the issuer URL and "client" is a standard oauth2 concept (client_id, no?). All the URLs for OpenID Connect Providers (like keycloak) expose the URLs that are needed via a well-defined URL http://keycloakhost:keycloakport/auth/realms/{realm}/.well-known/openid-configuration

this would give you a map of all your endpoints in a standard way so that if your keycloak instance/realm was at http://localhost:8080/auth/realms/demo then you'd find the following json at http://localhost:8080/auth/realms/demo/.well-known/openid-configuration

{
    "issuer":"http://localhost:8080/auth/realms/demo",
    "authorization_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth",
    "token_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/token",
    "userinfo_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/userinfo",
    "end_session_endpoint":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/logout",
    "jwks_uri":"http://localhost:8080/auth/realms/demo/protocol/openid-connect/certs",
    "grant_types_supported":["authorization_code","refresh_token","password"],
    "response_types_supported":["code"],
    "subject_types_supported":["public"],
    "id_token_signing_alg_values_supported":["RS256"],
    "response_modes_supported":["query"]
}

the keycloak concept of realm (as far as i can tell) is just an "instance" of an authorization provider (of which you could have several---but not so important in the basic case). So, you'd connect to a keycloak authorization_endpoint and get tokens from token_endpoint, etc. You'd configure spring-security-oauth2 to look for the public key to verify JWTs at jwks_uri

The problem with using the keycloak spring-boot-adapter is that it makes it keycloak-specific which I don't think it needs to be. I've used keycloak for other projects without using any keycloak code on the client. It would be a shame to limit ourselves to a single provider what we can easily connect to any conforming provider with with open standards.

for example, if keycloak was on my local and I'd configured a client "admin-portal:secret" on keycloak, I could authenticate with keycloak simply by changing my application.yml config from https://github.com/sdoxsee/jhipster-openid-connect-microservices/blob/master/admin-portal/src/main/resources/config/application.yml#L35 to something like this (based on the info from the keycloak .well-known/openid-configuration endpoint.

targets.uaa: http://localhost:8080/auth/realms/demo

security.oauth2:
  client:
    accessTokenUri: ${targets.uaa}/protocol/openid-connect/token
    userAuthorizationUri: ${targets.uaa}/protocol/openid-connect/auth
    clientId: admin-portal
    clientSecret: secret
    # scope: openid,menu.read,menu.write,order.admin
    scope: openid,foo.read,foo.write,foo.admin
  resource:
    user-info-uri: ${targets.uaa}/protocol/openid-connect/userinfo
    jwt.keyUri: ${targets.uaa}/protocol/openid-connect/certs
@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Apr 20, 2017

jhipster apps shouldn't care about keycloak except by its URLs and (only in the case of a gateway app) by the client id and secret assigned to the gateway in keycloak. That provides great power and flexibility for jhipster and makes the jhipster side much simpler.

sdoxsee commented Apr 20, 2017

jhipster apps shouldn't care about keycloak except by its URLs and (only in the case of a gateway app) by the client id and secret assigned to the gateway in keycloak. That provides great power and flexibility for jhipster and makes the jhipster side much simpler.

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Apr 20, 2017

Owner

OK, you convinced me on server side 😄

On client side, keycloak-js adapter handles refresh tokens, JHipster uaa seems to do it in server, I don't understand how it works.

Anyway, I will be busy next two weeks so cannot work more on this but I'm definitely interested in integrating Keycloak with JHipster.

Owner

gmarziou commented Apr 20, 2017

OK, you convinced me on server side 😄

On client side, keycloak-js adapter handles refresh tokens, JHipster uaa seems to do it in server, I don't understand how it works.

Anyway, I will be busy next two weeks so cannot work more on this but I'm definitely interested in integrating Keycloak with JHipster.

@jdubois

This comment has been minimized.

Show comment
Hide comment
@jdubois

jdubois Apr 28, 2017

Hi guys,

  • I totally agree with you, and I would love to have Keycloak integrated in a "non intrusive" way. For me it's mostly to have a working dev environment, and to show how you can use an OAuth2 server with JHipster, and then it's up the end user to configure this correctly in prod
  • I'm also speaking with @bleporini about this, he would also like to help

-> could you add a specific ticket for this on https://github.com/jhipster/generator-jhipster/issues so that everyone can see the discussion and follow what's happening?

jdubois commented Apr 28, 2017

Hi guys,

  • I totally agree with you, and I would love to have Keycloak integrated in a "non intrusive" way. For me it's mostly to have a working dev environment, and to show how you can use an OAuth2 server with JHipster, and then it's up the end user to configure this correctly in prod
  • I'm also speaking with @bleporini about this, he would also like to help

-> could you add a specific ticket for this on https://github.com/jhipster/generator-jhipster/issues so that everyone can see the discussion and follow what's happening?

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Aug 31, 2017

Hello,
Any update on the keycloak integration?
Sounds interesting!!

danielpetisme commented Aug 31, 2017

Hello,
Any update on the keycloak integration?
Sounds interesting!!

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Aug 31, 2017

Hi @danielpetisme, I know you've already seen jhipster/generator-jhipster#6139 but I'm referencing it here for future reference as it seems as though the issue will be tracked there. Thanks

sdoxsee commented Aug 31, 2017

Hi @danielpetisme, I know you've already seen jhipster/generator-jhipster#6139 but I'm referencing it here for future reference as it seems as though the issue will be tracked there. Thanks

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Aug 31, 2017

danielpetisme commented Aug 31, 2017

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 4, 2017

I think we could use angular-oauth2-oidc on the Angular side. I've used this with Okta's APIs and got things working. I did have to use Okta's Auth SDK to do authentication without a redirect though. Here's a blog post that shows how I did it.

I believe it should be possible to create a module that works with both Keycloak, Okta, and many other providers. Spring Security 5 has OIDC support and documents how to integrate with Google, GitHub, Facebook, and Okta.

However, SS 5's OIDC support seems to be mostly for authentication on the server, it doesn't setup a Resource Server and do JWT validation. We do that for Okta with a small Spring Security integration library. It only contains a few classes to do JWT validation.

mraible commented Sep 4, 2017

I think we could use angular-oauth2-oidc on the Angular side. I've used this with Okta's APIs and got things working. I did have to use Okta's Auth SDK to do authentication without a redirect though. Here's a blog post that shows how I did it.

I believe it should be possible to create a module that works with both Keycloak, Okta, and many other providers. Spring Security 5 has OIDC support and documents how to integrate with Google, GitHub, Facebook, and Okta.

However, SS 5's OIDC support seems to be mostly for authentication on the server, it doesn't setup a Resource Server and do JWT validation. We do that for Okta with a small Spring Security integration library. It only contains a few classes to do JWT validation.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 5, 2017

@mraible thanks for looking into these things. A couple comments.

angular-auth2-oidc seems to use localStorage/sessionStorage for the JWT. Stormpath seemed to recommend keeping the token in a cookie rather than web storage for security reasons.

We'd also need to do this for angular.js and react which seems like a pain. If we embraced the session (and spring session for scaling) we could avoid the complexity of managing all these different client implementations. Dave Syer seems to recommend leveraging the session instead instead of the so-called "stateless" approach.

sdoxsee commented Sep 5, 2017

@mraible thanks for looking into these things. A couple comments.

angular-auth2-oidc seems to use localStorage/sessionStorage for the JWT. Stormpath seemed to recommend keeping the token in a cookie rather than web storage for security reasons.

We'd also need to do this for angular.js and react which seems like a pain. If we embraced the session (and spring session for scaling) we could avoid the complexity of managing all these different client implementations. Dave Syer seems to recommend leveraging the session instead instead of the so-called "stateless" approach.

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 5, 2017

@sdoxsee Yes, Stormpath recommended keeping the token in a cookie. However, when they wrote that, they only had server-side implementations and could rely on a back channel flow with a clientId/secret. When Stormpath introduced its Client API support, we added support for storing tokens in localStorage since the Client API server did not keep sessions.

I used to work for Stormpath before we joined forces with Okta.

We could embrace the session, but I think stateless scales better. I'm not sure JHipster wants to add another dependency, and OAuth/OIDC is meant to be stateless with its access/refresh tokens having expires information in them.

mraible commented Sep 5, 2017

@sdoxsee Yes, Stormpath recommended keeping the token in a cookie. However, when they wrote that, they only had server-side implementations and could rely on a back channel flow with a clientId/secret. When Stormpath introduced its Client API support, we added support for storing tokens in localStorage since the Client API server did not keep sessions.

I used to work for Stormpath before we joined forces with Okta.

We could embrace the session, but I think stateless scales better. I'm not sure JHipster wants to add another dependency, and OAuth/OIDC is meant to be stateless with its access/refresh tokens having expires information in them.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 6, 2017

Thanks @mraible for your thoughts.

@sdoxsee Yes, Stormpath recommended keeping the token in a cookie. However, when they wrote that, they only had server-side implementations and could rely on a back channel flow with a clientId/secret. When Stormpath introduced its Client API support, we added support for storing tokens in localStorage since the Client API server did not keep sessions.

Interesting, I was surprised that you were recommending web storage with your stormpath background and given what the article I referenced said. However, I don't see how the Client API gets around the XSS risks since it sounds like the client application still would store the access token in web storage (leaving it open to rogue JS dependencies). Browser storage of access tokens is a necessary risk to take for front-end-only clients (i.e. implicit flow) since they can't keep a secret but if there if it's not a front-end-only client (and jhipster is not front-end-only), then it's more secure to use the authorization code flow where the access tokens and client secrets are kept server-side.

We could embrace the session, but I think stateless scales better.

Even if stateless is better at scaling, if it isn't secure, I wouldn't chose it as jhipster's default architecture. I'm open to seeing if/how Client API gets around this but I imagine most OIDC providers (Auth0, Keycloak, MitreID Connect, etc.) don't have something to get around XSS with web storage--even if Stormpath/Okta do. I'd love to see jhipster to be flexible, secure and decoupled in this regard.

I'm not sure JHipster wants to add another dependency,

I agree jhipster would want to minimize dependencies (like a redis instance backing HttpSession for sticky sessions) but if you're doing microservices already, I'd argue a single redis instance is a pretty small price to pay for security (and I'd argue...simplicity in client and server side).

and OAuth/OIDC is meant to be stateless with its access/refresh tokens having expires information in them.

I'm not sure what you mean that OAuth/OIDC is meant to be stateless. Perhaps I'm misunderstanding. It's true that resource servers don't need to track the session, but that's not what I mean by leveraging or embracing the session. Only the gateway tracks the session and then relays the token on to the resources servers (via Zuul proxy) to the resources servers in a stateless manner. Sticky sessions only apply if one wishes to scale up the gateway--the other services remain stateless. I believe the authorization server is NOT stateless, for example, as implicit and authorization code flows rely on redirection and session.

What did you think of Dave Syer's comments regarding stateless vs session here and following?

sdoxsee commented Sep 6, 2017

Thanks @mraible for your thoughts.

@sdoxsee Yes, Stormpath recommended keeping the token in a cookie. However, when they wrote that, they only had server-side implementations and could rely on a back channel flow with a clientId/secret. When Stormpath introduced its Client API support, we added support for storing tokens in localStorage since the Client API server did not keep sessions.

Interesting, I was surprised that you were recommending web storage with your stormpath background and given what the article I referenced said. However, I don't see how the Client API gets around the XSS risks since it sounds like the client application still would store the access token in web storage (leaving it open to rogue JS dependencies). Browser storage of access tokens is a necessary risk to take for front-end-only clients (i.e. implicit flow) since they can't keep a secret but if there if it's not a front-end-only client (and jhipster is not front-end-only), then it's more secure to use the authorization code flow where the access tokens and client secrets are kept server-side.

We could embrace the session, but I think stateless scales better.

Even if stateless is better at scaling, if it isn't secure, I wouldn't chose it as jhipster's default architecture. I'm open to seeing if/how Client API gets around this but I imagine most OIDC providers (Auth0, Keycloak, MitreID Connect, etc.) don't have something to get around XSS with web storage--even if Stormpath/Okta do. I'd love to see jhipster to be flexible, secure and decoupled in this regard.

I'm not sure JHipster wants to add another dependency,

I agree jhipster would want to minimize dependencies (like a redis instance backing HttpSession for sticky sessions) but if you're doing microservices already, I'd argue a single redis instance is a pretty small price to pay for security (and I'd argue...simplicity in client and server side).

and OAuth/OIDC is meant to be stateless with its access/refresh tokens having expires information in them.

I'm not sure what you mean that OAuth/OIDC is meant to be stateless. Perhaps I'm misunderstanding. It's true that resource servers don't need to track the session, but that's not what I mean by leveraging or embracing the session. Only the gateway tracks the session and then relays the token on to the resources servers (via Zuul proxy) to the resources servers in a stateless manner. Sticky sessions only apply if one wishes to scale up the gateway--the other services remain stateless. I believe the authorization server is NOT stateless, for example, as implicit and authorization code flows rely on redirection and session.

What did you think of Dave Syer's comments regarding stateless vs session here and following?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 7, 2017

Interesting, I was surprised that you were recommending web storage with your stormpath background and given what the article I referenced said. However, I don't see how the Client API gets around the XSS risks since it sounds like the client application still would store the access token in web storage (leaving it open to rogue JS dependencies). Browser storage of access tokens is a necessary risk to take for front-end-only clients (i.e. implicit flow) since they can't keep a secret but if there if it's not a front-end-only client (and jhipster is not front-end-only), then it's more secure to use the authorization code flow where the access tokens and client secrets are kept server-side.

This comes out of necessity from some of the apps I've built where the client sits on one server and the API sits on another. When the front end talks to the IdP to get the access/ID token, it has to store it somewhere. Sure, it can store it in a cookie, but localStorage works just as well.

For clients that talk to the same server for tokens, cookies make sense. This is the way JHipster works currently, but many of the UI apps I've seen developed are deployed on separate servers and the server has CORS rules that allow them to. Personally, I like it when the UI is decoupled from the API and they live as separate apps. It's a great developer experience when you can change the URL for the backend and point to dev, staging, and prod from your local UI.

I respect Dave Syer, so I believe most everything he says. It's a similar notion that @rdegges wrote about on the Okta Developer blog: https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

I think what we need to get down to is: what's the best way to implement Keycloak/Okta/OIDC support in JHipster?

Do we want to write custom code in the client (using something like angular-oauth2-oidc) or do we want to use a login widget that's less intrusive? Okta has a Sign-In Widget we can use for more than just login (e.g. password reset, and eventually registration).

If I use that do the auth with Okta, I'll get back an access token, which I can sent to the API. If I use Okta's Spring Boot Starter, everything should work nicely. I imagine Keycloak offers a similar login widget and JWT verifier, but not sure.

Another option is to make the login link redirect to the IdP for login. Regardless, we should probably remove user management from JHipster, or write custom code to talk to the IdP for user management.

@sdoxsee What do you think is the best way to implement this?

mraible commented Sep 7, 2017

Interesting, I was surprised that you were recommending web storage with your stormpath background and given what the article I referenced said. However, I don't see how the Client API gets around the XSS risks since it sounds like the client application still would store the access token in web storage (leaving it open to rogue JS dependencies). Browser storage of access tokens is a necessary risk to take for front-end-only clients (i.e. implicit flow) since they can't keep a secret but if there if it's not a front-end-only client (and jhipster is not front-end-only), then it's more secure to use the authorization code flow where the access tokens and client secrets are kept server-side.

This comes out of necessity from some of the apps I've built where the client sits on one server and the API sits on another. When the front end talks to the IdP to get the access/ID token, it has to store it somewhere. Sure, it can store it in a cookie, but localStorage works just as well.

For clients that talk to the same server for tokens, cookies make sense. This is the way JHipster works currently, but many of the UI apps I've seen developed are deployed on separate servers and the server has CORS rules that allow them to. Personally, I like it when the UI is decoupled from the API and they live as separate apps. It's a great developer experience when you can change the URL for the backend and point to dev, staging, and prod from your local UI.

I respect Dave Syer, so I believe most everything he says. It's a similar notion that @rdegges wrote about on the Okta Developer blog: https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

I think what we need to get down to is: what's the best way to implement Keycloak/Okta/OIDC support in JHipster?

Do we want to write custom code in the client (using something like angular-oauth2-oidc) or do we want to use a login widget that's less intrusive? Okta has a Sign-In Widget we can use for more than just login (e.g. password reset, and eventually registration).

If I use that do the auth with Okta, I'll get back an access token, which I can sent to the API. If I use Okta's Spring Boot Starter, everything should work nicely. I imagine Keycloak offers a similar login widget and JWT verifier, but not sure.

Another option is to make the login link redirect to the IdP for login. Regardless, we should probably remove user management from JHipster, or write custom code to talk to the IdP for user management.

@sdoxsee What do you think is the best way to implement this?

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 7, 2017

This comes out of necessity from some of the apps I've built where the client sits on one server and the API sits on another. When the front end talks to the IdP to get the access/ID token, it has to store it somewhere. Sure, it can store it in a cookie, but localStorage works just as well.

That architecture sounds good...it's just that an access token stored in localStorage can be accessed by one of the client's JS dependencies or subdependencies. For example, counting the node_modules folders in one of my jhipster projects, I have 1139 (some are dev only but still a lot). If one of those is malicious, it's dangerous. So, I'd say an HttpOnly cookie is better than localStorage...but perhaps keeping the token completely on the server-side and utilizing the sessin is better still?

For clients that talk to the same server for tokens, cookies make sense. This is the way JHipster works currently, but many of the UI apps I've seen developed are deployed on separate servers and the server has CORS rules that allow them to. Personally, I like it when the UI is decoupled from the API and they live as separate apps. It's a great developer experience when you can change the URL for the backend and point to dev, staging, and prod from your local UI.

I like that experience as well. If your UI client is decoupled in that it is separate from the API but your UI is housed within a gateway (zuul proxy), then couldn't you configure your gateway to use the dev/prod/staging IdP and your zuul to proxy to the dev/prod/staging API? Even with a JS only app, you'd still need to say where to get your token (IdP) and where to send your requests (API). Not sure if this meets the developer experience you're describing but I like the idea (I've done it that way once myself)

I respect Dave Syer, so I believe most everything he says. It's a similar notion that @rdegges wrote about on the Okta Developer blog: https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

Hadn't read that one before. Interesting article. Thanks for sharing.

I think what we need to get down to is: what's the best way to implement Keycloak/Okta/OIDC support in JHipster?

Yes. I'm almost always wanting to use OIDC of some sort and I REALLY want to see some secure, flexible integration in JHipster. Especially when I'm integrating lots of services or wanting to leverage an existing user credential base, I'm left frustrated. I think doing this well has great potential to make JHipster even more amazing.

Do we want to write custom code in the client (using something like angular-oauth2-oidc) or do we want to use a login widget that's less intrusive? Okta has a Sign-In Widget we can use for more than just login (e.g. password reset, and eventually registration).

I'd rather not add custom code to the client (for reasons stated in my previous comment). I'm not sure what OIDC flow widgets use but I'm worried that the access token is still stored in web storage...plus hooking in the widget would be different for each IdP (if they even have a widget).

Another option is to make the login link redirect to the IdP for login. Regardless, we should probably remove user management from JHipster, or write custom code to talk to the IdP for user management.

Although this involves a redirect (that could be tricky), this is my favourite option.

  • it eliminates client code variations,
  • it minimizes server configuration to application.yml tweaks for spring cloud security pointing to the discovery endpoint of the IdP.
  • it enables seamless SSO for many different gateway apps if there is already a session on the IdP for the user--letting them right into the different gateway client apps.
  • it should work with any conforming OIDC IdP implementation.

However, a challenge is how to remember the originally requested SPA route that is being requested after the user returns from entering their credentials into the IdP (but perhaps that route could be stored in localStorage before redirecting kinda like here)

I absolutely agree the user management (incl password reset, registration) should be removed from JHipster. For the single user case, the userInfo endpoint on the IdP should be sufficient but I like how auth0 and okta follow the SCIM standard to provide an API to manage/query users. If JHipster is able to assume a standard format for a user, then they could work with the user data in a standard way without having to map it to its own user domain object. Not all OIDC IdPs have user management or SCIM (e.g. mitreid connect) but they could be extended to do so if necessary. SCIM is a "nice to have" but not necessary for jhipster (though it would be nice if jhipster assumed those models) since, once user management is extracted from jhipster, it currently only cares about the authenticated user (which userinfo endpoint provides). I suppose that if someone chose mitreid connect (for example) as their IdP for jhipster, they wouldn't get password reset, registration, or even user CRUD "out of the box" as they normally would with jhipster currently. But I think that's fine (they may have other ways of dealing with users or extensions of the base IdP mitreid connect implementation)

@mraible What do you think of the redirect option? Also, do you think the dev experience you're looking for can be achieved with this approach (by housing the client UI in a gateway app)?

sdoxsee commented Sep 7, 2017

This comes out of necessity from some of the apps I've built where the client sits on one server and the API sits on another. When the front end talks to the IdP to get the access/ID token, it has to store it somewhere. Sure, it can store it in a cookie, but localStorage works just as well.

That architecture sounds good...it's just that an access token stored in localStorage can be accessed by one of the client's JS dependencies or subdependencies. For example, counting the node_modules folders in one of my jhipster projects, I have 1139 (some are dev only but still a lot). If one of those is malicious, it's dangerous. So, I'd say an HttpOnly cookie is better than localStorage...but perhaps keeping the token completely on the server-side and utilizing the sessin is better still?

For clients that talk to the same server for tokens, cookies make sense. This is the way JHipster works currently, but many of the UI apps I've seen developed are deployed on separate servers and the server has CORS rules that allow them to. Personally, I like it when the UI is decoupled from the API and they live as separate apps. It's a great developer experience when you can change the URL for the backend and point to dev, staging, and prod from your local UI.

I like that experience as well. If your UI client is decoupled in that it is separate from the API but your UI is housed within a gateway (zuul proxy), then couldn't you configure your gateway to use the dev/prod/staging IdP and your zuul to proxy to the dev/prod/staging API? Even with a JS only app, you'd still need to say where to get your token (IdP) and where to send your requests (API). Not sure if this meets the developer experience you're describing but I like the idea (I've done it that way once myself)

I respect Dave Syer, so I believe most everything he says. It's a similar notion that @rdegges wrote about on the Okta Developer blog: https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

Hadn't read that one before. Interesting article. Thanks for sharing.

I think what we need to get down to is: what's the best way to implement Keycloak/Okta/OIDC support in JHipster?

Yes. I'm almost always wanting to use OIDC of some sort and I REALLY want to see some secure, flexible integration in JHipster. Especially when I'm integrating lots of services or wanting to leverage an existing user credential base, I'm left frustrated. I think doing this well has great potential to make JHipster even more amazing.

Do we want to write custom code in the client (using something like angular-oauth2-oidc) or do we want to use a login widget that's less intrusive? Okta has a Sign-In Widget we can use for more than just login (e.g. password reset, and eventually registration).

I'd rather not add custom code to the client (for reasons stated in my previous comment). I'm not sure what OIDC flow widgets use but I'm worried that the access token is still stored in web storage...plus hooking in the widget would be different for each IdP (if they even have a widget).

Another option is to make the login link redirect to the IdP for login. Regardless, we should probably remove user management from JHipster, or write custom code to talk to the IdP for user management.

Although this involves a redirect (that could be tricky), this is my favourite option.

  • it eliminates client code variations,
  • it minimizes server configuration to application.yml tweaks for spring cloud security pointing to the discovery endpoint of the IdP.
  • it enables seamless SSO for many different gateway apps if there is already a session on the IdP for the user--letting them right into the different gateway client apps.
  • it should work with any conforming OIDC IdP implementation.

However, a challenge is how to remember the originally requested SPA route that is being requested after the user returns from entering their credentials into the IdP (but perhaps that route could be stored in localStorage before redirecting kinda like here)

I absolutely agree the user management (incl password reset, registration) should be removed from JHipster. For the single user case, the userInfo endpoint on the IdP should be sufficient but I like how auth0 and okta follow the SCIM standard to provide an API to manage/query users. If JHipster is able to assume a standard format for a user, then they could work with the user data in a standard way without having to map it to its own user domain object. Not all OIDC IdPs have user management or SCIM (e.g. mitreid connect) but they could be extended to do so if necessary. SCIM is a "nice to have" but not necessary for jhipster (though it would be nice if jhipster assumed those models) since, once user management is extracted from jhipster, it currently only cares about the authenticated user (which userinfo endpoint provides). I suppose that if someone chose mitreid connect (for example) as their IdP for jhipster, they wouldn't get password reset, registration, or even user CRUD "out of the box" as they normally would with jhipster currently. But I think that's fine (they may have other ways of dealing with users or extensions of the base IdP mitreid connect implementation)

@mraible What do you think of the redirect option? Also, do you think the dev experience you're looking for can be achieved with this approach (by housing the client UI in a gateway app)?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 7, 2017

I tried the redirect option this morning with angular-oauth2-oidc and the following configuration:

typescript

    constructor(
        private principal: Principal,
        private eventManager: JhiEventManager,
        private oauthService: OAuthService
    ) {
        this.oauthService.redirectUri = window.location.origin;
        this.oauthService.clientId = '0oabxuli6b3nTccfp0h7';
        this.oauthService.scope = 'openid profile email';
        this.oauthService.issuer = 'https://dev-158606.oktapreview.com';
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();

        // Load Discovery Document and then try to login the user
        this.oauthService.loadDiscoveryDocument().then(() => {
            // This method just tries to parse the token(s) within the url when
            // the auth-server redirects the user back to the web-app
            // It dosn't send the user the the login page
            this.oauthService.tryLogin();
        });
    }

When it comes back to my app, the URL looks like the following:

http://localhost:9000/#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImhBb1g4ekVxbmFzRFR2M3NvMXdmbXg3eU52amdKdGg5UktlRmpjbzVLLXcifQ.eyJzdWIiOiIwMHU5cW9qNjljOU1zVkhBbTBoNyIsIm5hbWUiOiJNYXR0IFJhaWJsZSIsImVtYWlsIjoibWF0dEByYWlibGVkZXNpZ25zLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9kZXYtMTU4NjA2Lm9rdGFwcmV2aWV3LmNvbSIsImF1ZCI6IjBvYWJ4dWxpNmIzblRjY2ZwMGg3IiwiaWF0IjoxNTA0Nzk3Mjk0LCJleHAiOjE1MDQ4MDA4OTQsImp0aSI6IklELlFxMjZQNkR3OElkbGJXaEZ4OGpVeTNibWtyQTdQN01PYUFIN29HLUtRQ0EiLCJhbXIiOlsicHdkIl0sImlkcCI6IjAwbzlxb2o2OGR2amhPSzNBMGg3Iiwibm9uY2UiOiJQeGozUHBRVmZaTmxKRkNrSGhTZDdqYTFLcm9wMXJ5UVhtMHI2M2lEIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibWF0dEByYWlibGVkZXNpZ25zLmNvbSIsImF1dGhfdGltZSI6MTUwNDc5NDc3NSwiYXRfaGFzaCI6IjBsREhUTHo0YV9YYk9JX3lJQzQ3VEEifQ.AXeOdQeMnFnSAPtrnAImKInG5lfU7PgWldalFrYhIbQmVcT-vLVTymyXX95SEmdKoRP0yvTxqRO6GrGpMZ9Yq2Hwu2KTfKub_oMwqYKwjgJhXRUh8lrxGVOpdUftMroDTBFOWYvqvp6NOgz3wyj32Om-3stZ1ebigobQGoI0pBhaA7h1rbFgSmUhEhZO7KzeKxhgwKtB1xUhCI5i0qcIjqJ7HVxOskYvewHY-ebKIc2U7MvaJ0DIv84fxEiVGlTJSdJRBn8i-lQlyVtVDD4_D86GVN9V8OxpDEsHSffJnTbNx4Z31Z_eMLmCrrnginSPWvRiAas9TkiJIqYLAJyJFg&access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IkRNcklsV09RTnZQX1JfVUxGUDc2by0yelVBd0pyMUhzWUNrQjJRMUk2b28ifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmdaS1hvWGFoUDc5bG5KS3p6c3k2RWtETTNlVHlJUm9DTERCb1MtdXR3Y2siLCJpc3MiOiJodHRwczovL2Rldi0xNTg2MDYub2t0YXByZXZpZXcuY29tIiwiYXVkIjoiaHR0cHM6Ly9kZXYtMTU4NjA2Lm9rdGFwcmV2aWV3LmNvbSIsInN1YiI6Im1hdHRAcmFpYmxlZGVzaWducy5jb20iLCJpYXQiOjE1MDQ3OTcyOTQsImV4cCI6MTUwNDgwMDg5NCwiY2lkIjoiMG9hYnh1bGk2YjNuVGNjZnAwaDciLCJ1aWQiOiIwMHU5cW9qNjljOU1zVkhBbTBoNyIsInNjcCI6WyJlbWFpbCIsIm9wZW5pZCIsInByb2ZpbGUiXX0.rhMWvv93IZMTPivVcV1z2fkOzwisE-fLWEEBz7axm0VRJ6sJw1CIk87obHifmoAZH7G0TahNyUcl3r8LpRAfWMxu2WWJW7Laj6vbXgZjiNkGYsvxAllrBE_M1bVxKRI0ifelvxvZ0Hyx2Hjw_kUNkFXKYgoa7PafGGIxfFntIChEYlJp-lqCms9-b0BEvJRl-9bG-2-Nc33IcZRhZEqXhNMrtb4fvP3AtDdX0HRZoFoOyxKwVA58fezApyamUnWOItT0ywWYAlsqSlh10F8vEumpB04tFG3nIevFtB4ALfTeBU3ZLVkKIUujTUBfo5dUQJfJcdDRHbKAoXb5S9StGQ&token_type=Bearer&expires_in=3600&scope=email+openid+profile&state=Pxj3PpQVfZNlJFCkHhSd7ja1Krop1ryQXm0r63iD

At this point, I get an error from JHipster:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'id_token'
Error: Cannot match any routes. URL Segment: 'id_token'

I can add a route for id_token in home.routes.ts and import it in home.module.ts, but then it redirects to http://localhost:9000/#/id_token w/o the id_token as a parameter. The library's documentation says you can use initialNavigation: false, but that doesn't seem to help.

Anyone know how to redirect and keep the parameters? Another suggestion is to use a hash (e.g. http://localhost:8080/#/home) in the redirect URI, but Okta doesn't support that. Error is "The redirect URIs must not contain a fragment identifier."

/cc @manfredsteyer

mraible commented Sep 7, 2017

I tried the redirect option this morning with angular-oauth2-oidc and the following configuration:

typescript

    constructor(
        private principal: Principal,
        private eventManager: JhiEventManager,
        private oauthService: OAuthService
    ) {
        this.oauthService.redirectUri = window.location.origin;
        this.oauthService.clientId = '0oabxuli6b3nTccfp0h7';
        this.oauthService.scope = 'openid profile email';
        this.oauthService.issuer = 'https://dev-158606.oktapreview.com';
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();

        // Load Discovery Document and then try to login the user
        this.oauthService.loadDiscoveryDocument().then(() => {
            // This method just tries to parse the token(s) within the url when
            // the auth-server redirects the user back to the web-app
            // It dosn't send the user the the login page
            this.oauthService.tryLogin();
        });
    }

When it comes back to my app, the URL looks like the following:

http://localhost:9000/#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImhBb1g4ekVxbmFzRFR2M3NvMXdmbXg3eU52amdKdGg5UktlRmpjbzVLLXcifQ.eyJzdWIiOiIwMHU5cW9qNjljOU1zVkhBbTBoNyIsIm5hbWUiOiJNYXR0IFJhaWJsZSIsImVtYWlsIjoibWF0dEByYWlibGVkZXNpZ25zLmNvbSIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9kZXYtMTU4NjA2Lm9rdGFwcmV2aWV3LmNvbSIsImF1ZCI6IjBvYWJ4dWxpNmIzblRjY2ZwMGg3IiwiaWF0IjoxNTA0Nzk3Mjk0LCJleHAiOjE1MDQ4MDA4OTQsImp0aSI6IklELlFxMjZQNkR3OElkbGJXaEZ4OGpVeTNibWtyQTdQN01PYUFIN29HLUtRQ0EiLCJhbXIiOlsicHdkIl0sImlkcCI6IjAwbzlxb2o2OGR2amhPSzNBMGg3Iiwibm9uY2UiOiJQeGozUHBRVmZaTmxKRkNrSGhTZDdqYTFLcm9wMXJ5UVhtMHI2M2lEIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibWF0dEByYWlibGVkZXNpZ25zLmNvbSIsImF1dGhfdGltZSI6MTUwNDc5NDc3NSwiYXRfaGFzaCI6IjBsREhUTHo0YV9YYk9JX3lJQzQ3VEEifQ.AXeOdQeMnFnSAPtrnAImKInG5lfU7PgWldalFrYhIbQmVcT-vLVTymyXX95SEmdKoRP0yvTxqRO6GrGpMZ9Yq2Hwu2KTfKub_oMwqYKwjgJhXRUh8lrxGVOpdUftMroDTBFOWYvqvp6NOgz3wyj32Om-3stZ1ebigobQGoI0pBhaA7h1rbFgSmUhEhZO7KzeKxhgwKtB1xUhCI5i0qcIjqJ7HVxOskYvewHY-ebKIc2U7MvaJ0DIv84fxEiVGlTJSdJRBn8i-lQlyVtVDD4_D86GVN9V8OxpDEsHSffJnTbNx4Z31Z_eMLmCrrnginSPWvRiAas9TkiJIqYLAJyJFg&access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IkRNcklsV09RTnZQX1JfVUxGUDc2by0yelVBd0pyMUhzWUNrQjJRMUk2b28ifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmdaS1hvWGFoUDc5bG5KS3p6c3k2RWtETTNlVHlJUm9DTERCb1MtdXR3Y2siLCJpc3MiOiJodHRwczovL2Rldi0xNTg2MDYub2t0YXByZXZpZXcuY29tIiwiYXVkIjoiaHR0cHM6Ly9kZXYtMTU4NjA2Lm9rdGFwcmV2aWV3LmNvbSIsInN1YiI6Im1hdHRAcmFpYmxlZGVzaWducy5jb20iLCJpYXQiOjE1MDQ3OTcyOTQsImV4cCI6MTUwNDgwMDg5NCwiY2lkIjoiMG9hYnh1bGk2YjNuVGNjZnAwaDciLCJ1aWQiOiIwMHU5cW9qNjljOU1zVkhBbTBoNyIsInNjcCI6WyJlbWFpbCIsIm9wZW5pZCIsInByb2ZpbGUiXX0.rhMWvv93IZMTPivVcV1z2fkOzwisE-fLWEEBz7axm0VRJ6sJw1CIk87obHifmoAZH7G0TahNyUcl3r8LpRAfWMxu2WWJW7Laj6vbXgZjiNkGYsvxAllrBE_M1bVxKRI0ifelvxvZ0Hyx2Hjw_kUNkFXKYgoa7PafGGIxfFntIChEYlJp-lqCms9-b0BEvJRl-9bG-2-Nc33IcZRhZEqXhNMrtb4fvP3AtDdX0HRZoFoOyxKwVA58fezApyamUnWOItT0ywWYAlsqSlh10F8vEumpB04tFG3nIevFtB4ALfTeBU3ZLVkKIUujTUBfo5dUQJfJcdDRHbKAoXb5S9StGQ&token_type=Bearer&expires_in=3600&scope=email+openid+profile&state=Pxj3PpQVfZNlJFCkHhSd7ja1Krop1ryQXm0r63iD

At this point, I get an error from JHipster:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'id_token'
Error: Cannot match any routes. URL Segment: 'id_token'

I can add a route for id_token in home.routes.ts and import it in home.module.ts, but then it redirects to http://localhost:9000/#/id_token w/o the id_token as a parameter. The library's documentation says you can use initialNavigation: false, but that doesn't seem to help.

Anyone know how to redirect and keep the parameters? Another suggestion is to use a hash (e.g. http://localhost:8080/#/home) in the redirect URI, but Okta doesn't support that. Error is "The redirect URIs must not contain a fragment identifier."

/cc @manfredsteyer

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 7, 2017

FWIW, I tried with Keycloak too:

this.oauthService.issuer = 'http://localhost:8180/auth/realms/master';

This results in a similar error:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'state'
Error: Cannot match any routes. URL Segment: 'state'

Note that Keycloak doesn't let me use http://localhost:9000/# as a redirect either.

Error! Redirect URIs must not contain an URI fragment

mraible commented Sep 7, 2017

FWIW, I tried with Keycloak too:

this.oauthService.issuer = 'http://localhost:8180/auth/realms/master';

This results in a similar error:

ERROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'state'
Error: Cannot match any routes. URL Segment: 'state'

Note that Keycloak doesn't let me use http://localhost:9000/# as a redirect either.

Error! Redirect URIs must not contain an URI fragment
@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 7, 2017

Interesting. Perhaps if the app was using HTML5 routing without the hash it would work more easily rather than interpreting redirect call as a route?

However, I was thinking more along the lines of doing this server-side like what's being done here. I know this is spring security 5 but it's similar even now with spring security 4 (see here or here)

I think the angular-oauth2-oidc is using implicit flow and therefore sending all values in the url. If we were using the authorization_code flow, it would just give us back the "code" that we would exchange for tokens server-side. In any case, I think the IdP configuration should be server-side

sdoxsee commented Sep 7, 2017

Interesting. Perhaps if the app was using HTML5 routing without the hash it would work more easily rather than interpreting redirect call as a route?

However, I was thinking more along the lines of doing this server-side like what's being done here. I know this is spring security 5 but it's similar even now with spring security 4 (see here or here)

I think the angular-oauth2-oidc is using implicit flow and therefore sending all values in the url. If we were using the authorization_code flow, it would just give us back the "code" that we would exchange for tokens server-side. In any case, I think the IdP configuration should be server-side

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 7, 2017

What I did in my old angularjs jhipster project was when I loaded a server-side login endpoint when I hit the "login" state in my angularjs app (see here).
The server would then take me to the IdP authorization endpoint because the server is the only place that knows/cares about that configuration.

sdoxsee commented Sep 7, 2017

What I did in my old angularjs jhipster project was when I loaded a server-side login endpoint when I hit the "login" state in my angularjs app (see here).
The server would then take me to the IdP authorization endpoint because the server is the only place that knows/cares about that configuration.

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 7, 2017

I was able to make it work (for both Okta and Keycloak) by modifying account.module.ts to not use the hash.

RouterModule.forRoot(accountState, { useHash: false })

I'll look into making things work on the server so there's less to do on the client.

mraible commented Sep 7, 2017

I was able to make it work (for both Okta and Keycloak) by modifying account.module.ts to not use the hash.

RouterModule.forRoot(accountState, { useHash: false })

I'll look into making things work on the server so there's less to do on the client.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 7, 2017

@mraible Awesome! Keep me posted

sdoxsee commented Sep 7, 2017

@mraible Awesome! Keep me posted

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 9, 2017

Hey guys.

I also tried to integrate keycloack in a Jhipster microservice architecture.
On the client side, I tweaked a little bit the generated auth-jwt.service.ts. These are my remarks :

  • the angular app request a JWT token directly from the uaa server (it's totally independant from the backend)
  • it uses a specific web_app clientId without any password
  • it stores the token in local storage if Remember me is enabled otherwise it's placed into session storage.
  • no widget, no redirection.

The points @sdoxsee mentioned regarding storage, malicious dependencies, etc. make totally sense but the community seems ok with them...

danielpetisme commented Sep 9, 2017

Hey guys.

I also tried to integrate keycloack in a Jhipster microservice architecture.
On the client side, I tweaked a little bit the generated auth-jwt.service.ts. These are my remarks :

  • the angular app request a JWT token directly from the uaa server (it's totally independant from the backend)
  • it uses a specific web_app clientId without any password
  • it stores the token in local storage if Remember me is enabled otherwise it's placed into session storage.
  • no widget, no redirection.

The points @sdoxsee mentioned regarding storage, malicious dependencies, etc. make totally sense but the community seems ok with them...

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 9, 2017

I got everything working with session auth + Keycloak/Okta. However, I'm unable to get the groups from the IdP to map to Spring Security's Authorities. I think a custom Principal extractor might be what I need.

I'm currently mapping the Principal to a User in JHipster, and inserting/updating on login. This allows a User to exist in JHipster and somewhat stay in sync with the IdP.

AccountResource#getAccount() has most of the meat in my example:

public ResponseEntity<UserDTO> getAccount(Principal principal) {
    OAuth2Authentication authentication = (OAuth2Authentication) principal;
    LinkedHashMap<String, Object> details = (LinkedHashMap) authentication.getUserAuthentication().getDetails();
    User user = new User();
    user.setLogin(details.get("preferred_username").toString());

    if (details.get("given_name") != null) {
        user.setFirstName(details.get("given_name").toString());
    }
    if (details.get("family_name") != null) {
        user.setFirstName(details.get("family_name").toString());
    }
    if (details.get("email_verified") != null) {
        user.setActivated((Boolean) details.get("email_verified"));
    }
    if (details.get("email") != null) {
        user.setEmail(details.get("email").toString());
    }
    if (details.get("locale") != null) {
        String locale = details.get("locale").toString();
        String langKey = locale.substring(0, locale.indexOf("-"));
        user.setLangKey(langKey);
        user.setEmail(details.get("locale").toString());
    }

    Set<Authority> userAuthorities = new LinkedHashSet<>();

    // get groups from details
    if (details.get("groups") != null) {
        List groups = (ArrayList) details.get("groups");
        groups.forEach(group -> {
            Authority userAuthority = new Authority();
            userAuthority.setName(group.toString());
            userAuthorities.add(userAuthority);
        });
    } else {
        authentication.getAuthorities().forEach(role -> {
            Authority userAuthority = new Authority();
            userAuthority.setName(role.getAuthority());
            userAuthorities.add(userAuthority);
        });
    }

    user.setAuthorities(userAuthorities);
    UserDTO userDTO = new UserDTO(user);

    // todo: update Spring Security Authorities to match group claim from IdP
    // This might provide the solution: https://stackoverflow.com/questions/35056169/how-to-get-custom-user-info-from-oauth2-authorization-server-user-endpoint

    // save account in to sync users between IdP and JHipster's local database
    Optional<User> existingUser = userRepository.findOneByLogin(userDTO.getLogin());
    if (existingUser.isPresent()) {
        log.debug("Updating user '{}' in local database...", userDTO.getLogin());
        userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(),
            userDTO.getLangKey(), userDTO.getImageUrl());
    } else {
        log.debug("Saving user '{}' in local database...", userDTO.getLogin());
        userRepository.save(user);
    }

    return new ResponseEntity<>(userDTO, HttpStatus.OK);
}

I created a repo of what I have so far at https://github.com/mraible/jhipster-oidc-example.

The "oauth-redirect" branch has the bits that work with Okta and Keycloak.

OAuth settings are in application.yml - commented lines are keycloak's settings.

The code from above that does the mapping is in AccountResource.java.

mraible commented Sep 9, 2017

I got everything working with session auth + Keycloak/Okta. However, I'm unable to get the groups from the IdP to map to Spring Security's Authorities. I think a custom Principal extractor might be what I need.

I'm currently mapping the Principal to a User in JHipster, and inserting/updating on login. This allows a User to exist in JHipster and somewhat stay in sync with the IdP.

AccountResource#getAccount() has most of the meat in my example:

public ResponseEntity<UserDTO> getAccount(Principal principal) {
    OAuth2Authentication authentication = (OAuth2Authentication) principal;
    LinkedHashMap<String, Object> details = (LinkedHashMap) authentication.getUserAuthentication().getDetails();
    User user = new User();
    user.setLogin(details.get("preferred_username").toString());

    if (details.get("given_name") != null) {
        user.setFirstName(details.get("given_name").toString());
    }
    if (details.get("family_name") != null) {
        user.setFirstName(details.get("family_name").toString());
    }
    if (details.get("email_verified") != null) {
        user.setActivated((Boolean) details.get("email_verified"));
    }
    if (details.get("email") != null) {
        user.setEmail(details.get("email").toString());
    }
    if (details.get("locale") != null) {
        String locale = details.get("locale").toString();
        String langKey = locale.substring(0, locale.indexOf("-"));
        user.setLangKey(langKey);
        user.setEmail(details.get("locale").toString());
    }

    Set<Authority> userAuthorities = new LinkedHashSet<>();

    // get groups from details
    if (details.get("groups") != null) {
        List groups = (ArrayList) details.get("groups");
        groups.forEach(group -> {
            Authority userAuthority = new Authority();
            userAuthority.setName(group.toString());
            userAuthorities.add(userAuthority);
        });
    } else {
        authentication.getAuthorities().forEach(role -> {
            Authority userAuthority = new Authority();
            userAuthority.setName(role.getAuthority());
            userAuthorities.add(userAuthority);
        });
    }

    user.setAuthorities(userAuthorities);
    UserDTO userDTO = new UserDTO(user);

    // todo: update Spring Security Authorities to match group claim from IdP
    // This might provide the solution: https://stackoverflow.com/questions/35056169/how-to-get-custom-user-info-from-oauth2-authorization-server-user-endpoint

    // save account in to sync users between IdP and JHipster's local database
    Optional<User> existingUser = userRepository.findOneByLogin(userDTO.getLogin());
    if (existingUser.isPresent()) {
        log.debug("Updating user '{}' in local database...", userDTO.getLogin());
        userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(),
            userDTO.getLangKey(), userDTO.getImageUrl());
    } else {
        log.debug("Saving user '{}' in local database...", userDTO.getLogin());
        userRepository.save(user);
    }

    return new ResponseEntity<>(userDTO, HttpStatus.OK);
}

I created a repo of what I have so far at https://github.com/mraible/jhipster-oidc-example.

The "oauth-redirect" branch has the bits that work with Okta and Keycloak.

OAuth settings are in application.yml - commented lines are keycloak's settings.

The code from above that does the mapping is in AccountResource.java.

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 12, 2017

I have a question how decouple jhipster and the authorization server provider should be?
I spent couple of days in a Keycloak integration into the microservice architecture. Either I use the keycloak adapter and most of the things are made automagically or I use the standard oauth2 and I have to rewrite the public key retrieval to valide the Token, the principal extraction as Matt mentioned and the role extraction also.
This code is basically a poor-man version of the code made by guys who are paid to get this done.

I understand (and defend) the need of independance between JHipster and an IdP but for me it similar to the DB approach. There is plenty of DB and it wouldn't make sense to try to abstract everything. The generated code is optimized for the DB you choose.

What do you think?

danielpetisme commented Sep 12, 2017

I have a question how decouple jhipster and the authorization server provider should be?
I spent couple of days in a Keycloak integration into the microservice architecture. Either I use the keycloak adapter and most of the things are made automagically or I use the standard oauth2 and I have to rewrite the public key retrieval to valide the Token, the principal extraction as Matt mentioned and the role extraction also.
This code is basically a poor-man version of the code made by guys who are paid to get this done.

I understand (and defend) the need of independance between JHipster and an IdP but for me it similar to the DB approach. There is plenty of DB and it wouldn't make sense to try to abstract everything. The generated code is optimized for the DB you choose.

What do you think?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 13, 2017

@danielpetisme I was able to make everything work in my example without pulling in any Okta or Keycloak libraries. I just added Spring Security's OAuth support, tweaked a few things and it all works! I like this approach because we don't have to do anything special.

Here's my PR that I plan on turning into a JHipster OIDC Module. I like that this doesn't require much new code, but it removes a LOT of code.

screen shot 2017-09-13 at 5 04 31 pm

I looked into using a custom Principal extractor, but it required less code to just put everything in AccountResource#getAccount().

mraible commented Sep 13, 2017

@danielpetisme I was able to make everything work in my example without pulling in any Okta or Keycloak libraries. I just added Spring Security's OAuth support, tweaked a few things and it all works! I like this approach because we don't have to do anything special.

Here's my PR that I plan on turning into a JHipster OIDC Module. I like that this doesn't require much new code, but it removes a LOT of code.

screen shot 2017-09-13 at 5 04 31 pm

I looked into using a custom Principal extractor, but it required less code to just put everything in AccountResource#getAccount().

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 14, 2017

@mraible This looks sooooo good. I love all the deleted code and how a provider is simply a property change in the application.yml. At first glance, this looks exactly like what I'd like and what I think jhipster needs. I'd like to try it out when I get a chance. I imagine coding this for react and angularjs would be very light work. I'd also like to see this kind of change in the microservices architecture as well. Very hipster 😀 Nice work and thank you!

sdoxsee commented Sep 14, 2017

@mraible This looks sooooo good. I love all the deleted code and how a provider is simply a property change in the application.yml. At first glance, this looks exactly like what I'd like and what I think jhipster needs. I'd like to try it out when I get a chance. I imagine coding this for react and angularjs would be very light work. I'd also like to see this kind of change in the microservices architecture as well. Very hipster 😀 Nice work and thank you!

@jdubois

This comment has been minimized.

Show comment
Hide comment
@jdubois

jdubois Sep 14, 2017

This is awesome!

Concerning the current OAuth2 support in JHipster:

  • I know it's very bad, I coded it :-)
  • It is mostly useless, as it is its own OAuth2 server, so you'd better use JWT at the moment
  • In fact nobody uses it, stats for the last month show 0,80% of people use it
  • It gives a lot of maintenance: we have some custom JPA code, a lot of custom code for the MongoDB version (I coded the whole implementation, I know I was crazy). It's not supported in Cassandra (because <2% of people use Cassandra), and I really hope @tchlyah didn't implement that for Couchbase

-> so if you want to remove all the current OAuth2 code, to make JHipster work as an OIDC client instead, you have a big +1 from me.
-> does this make sense to you?

Then, I know you have no custom code for Okta and Keycloack, but here is what I would love:

  • Documentation on integrating Okta and Keycloack
  • Keycloack support, including a Docker Compose file in src/main/docker, so that the whole architecture can be used in dev mode easily
  • Also, being able to test everything in Travis, using the Keycloack Docker image

jdubois commented Sep 14, 2017

This is awesome!

Concerning the current OAuth2 support in JHipster:

  • I know it's very bad, I coded it :-)
  • It is mostly useless, as it is its own OAuth2 server, so you'd better use JWT at the moment
  • In fact nobody uses it, stats for the last month show 0,80% of people use it
  • It gives a lot of maintenance: we have some custom JPA code, a lot of custom code for the MongoDB version (I coded the whole implementation, I know I was crazy). It's not supported in Cassandra (because <2% of people use Cassandra), and I really hope @tchlyah didn't implement that for Couchbase

-> so if you want to remove all the current OAuth2 code, to make JHipster work as an OIDC client instead, you have a big +1 from me.
-> does this make sense to you?

Then, I know you have no custom code for Okta and Keycloack, but here is what I would love:

  • Documentation on integrating Okta and Keycloack
  • Keycloack support, including a Docker Compose file in src/main/docker, so that the whole architecture can be used in dev mode easily
  • Also, being able to test everything in Travis, using the Keycloack Docker image
@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 14, 2017

@jdubois I was going to send a message about the current OAuth2 implementation, but it sounds like there's no need now. ;)

Yes, I think replacing the current OAuth2 code with this OIDC code makes sense. It's not perfect (it currently always redirects to localhost:8080 after login and doesn't detect :9000), but likely good enough as a starting point.

I'll add documentation for Okta and Keycloak and work on the Docker stuff too. Is it possible to configure Keycloak with default users and then put it in a Docker image? I'm guessing yes. Should we put this on JHipster's Docker Hub?

mraible commented Sep 14, 2017

@jdubois I was going to send a message about the current OAuth2 implementation, but it sounds like there's no need now. ;)

Yes, I think replacing the current OAuth2 code with this OIDC code makes sense. It's not perfect (it currently always redirects to localhost:8080 after login and doesn't detect :9000), but likely good enough as a starting point.

I'll add documentation for Okta and Keycloak and work on the Docker stuff too. Is it possible to configure Keycloak with default users and then put it in a Docker image? I'm guessing yes. Should we put this on JHipster's Docker Hub?

@jdubois

This comment has been minimized.

Show comment
Hide comment
@jdubois

jdubois Sep 14, 2017

Great @mraible :-)
Yes my idea for Keycloak would be like the JHipster Console, which is a customized ELK. This would be a customized Keycloak, for use with JHipster, focusing on the dev mode: it is mainly available to help people develop, and as a starting point for people who want to go to prod (but then, they'd better switch to Okta, of course!!).
So yes this would be a specific image on JHipster's Docker Hub -> if you need help on this I'm available

jdubois commented Sep 14, 2017

Great @mraible :-)
Yes my idea for Keycloak would be like the JHipster Console, which is a customized ELK. This would be a customized Keycloak, for use with JHipster, focusing on the dev mode: it is mainly available to help people develop, and as a starting point for people who want to go to prod (but then, they'd better switch to Okta, of course!!).
So yes this would be a specific image on JHipster's Docker Hub -> if you need help on this I'm available

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 14, 2017

@mraible I tried it with my own okta dev account and it works great. Was also able to make my user ROLE_ADMIN.

A couple things for future work

  • remember unauthorized URL (e.g. http://localhost:8080/#/audits) in web storage before redirecting to IdP so that, once authorized, can go back to originally requested page
  • option to logout of the IdP as well

sdoxsee commented Sep 14, 2017

@mraible I tried it with my own okta dev account and it works great. Was also able to make my user ROLE_ADMIN.

A couple things for future work

  • remember unauthorized URL (e.g. http://localhost:8080/#/audits) in web storage before redirecting to IdP so that, once authorized, can go back to originally requested page
  • option to logout of the IdP as well
@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 14, 2017

@sdoxsee Cool! I'm aware of the two issues you mention and will look into fixing them after this first release. I sent the first question to the Spring Security team this morning. Here's what I asked:

How it works is the client sends a GET to http://localhost:8080/login, then Spring Security takes care of the redirect and callback. This works great, but always redirects back to http://localhost:8080. If the user is running JHipster in “dev mode”, the client runs on http://localhost:9000. Is there anyway to pass in the redirect to Spring Security so it’s the client URI (:9000) instead of the server URI (:8080)?

I believe logging out of the IdP should be a Spring Security feature as well, but not sure if it's supported.

mraible commented Sep 14, 2017

@sdoxsee Cool! I'm aware of the two issues you mention and will look into fixing them after this first release. I sent the first question to the Spring Security team this morning. Here's what I asked:

How it works is the client sends a GET to http://localhost:8080/login, then Spring Security takes care of the redirect and callback. This works great, but always redirects back to http://localhost:8080. If the user is running JHipster in “dev mode”, the client runs on http://localhost:9000. Is there anyway to pass in the redirect to Spring Security so it’s the client URI (:9000) instead of the server URI (:8080)?

I believe logging out of the IdP should be a Spring Security feature as well, but not sure if it's supported.

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 16, 2017

@danielpetisme Removing volumes: fixed the problem. Thanks!

Only issue now is I'm not seeing groups returned. I do see roles, but I don't think we want to use those in AccountResource since "offline_access" and "uma_authorization" are roles that aren't to JHipster. I could modify the logic to ignore these groups like I do for Okta.

if (!String.valueOf(group).equalsIgnoreCase("everyone")) {
    Authority userAuthority = new Authority();
    userAuthority.setName(group.toString());
    userAuthorities.add(userAuthority);
}

I think the admin user needs to have ROLE_USER in addition to ROLE_ADMIN too.

mraible commented Sep 16, 2017

@danielpetisme Removing volumes: fixed the problem. Thanks!

Only issue now is I'm not seeing groups returned. I do see roles, but I don't think we want to use those in AccountResource since "offline_access" and "uma_authorization" are roles that aren't to JHipster. I could modify the logic to ignore these groups like I do for Okta.

if (!String.valueOf(group).equalsIgnoreCase("everyone")) {
    Authority userAuthority = new Authority();
    userAuthority.setName(group.toString());
    userAuthorities.add(userAuthority);
}

I think the admin user needs to have ROLE_USER in addition to ROLE_ADMIN too.

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 16, 2017

Actually we can finely manage the roles in Keycloak to remove those roles to keep only JHipster roles.

Keycloak have a group concept but it's for internal usage, it's not exposed, they only talk about roles. So groups are just a way to apply in one shot a set of roles to a user, that's why the admin user in the Users group with the associated ROLE_USER and also the Admins group with the associated ROLE_ADMIN. For simple role management like the default app it maybe sound over-engineering but I think it's closer to the Keycloak spirit.

If you have look to the decode jwt value it expose only roles not groups.
My proposal would be to keep a group management in keycloak but use directly the concept of roles in the JHipster app. Plus, with a configurable attribute name to extract the roles you can name the attribute whatever you want.

@gmarziou do you have any experience on Keycloak management?

danielpetisme commented Sep 16, 2017

Actually we can finely manage the roles in Keycloak to remove those roles to keep only JHipster roles.

Keycloak have a group concept but it's for internal usage, it's not exposed, they only talk about roles. So groups are just a way to apply in one shot a set of roles to a user, that's why the admin user in the Users group with the associated ROLE_USER and also the Admins group with the associated ROLE_ADMIN. For simple role management like the default app it maybe sound over-engineering but I think it's closer to the Keycloak spirit.

If you have look to the decode jwt value it expose only roles not groups.
My proposal would be to keep a group management in keycloak but use directly the concept of roles in the JHipster app. Plus, with a configurable attribute name to extract the roles you can name the attribute whatever you want.

@gmarziou do you have any experience on Keycloak management?

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 16, 2017

Owner

Yes but it was 9 months ago, I have written some tips but they are at work and about version 2.x, so I should refresh them. One thing I remember for sure is that it's easy to mess up a working configuration using the GUI and having hard time to figure what's wrong, so exporting and re-importing in JSON is the way to go.
I think we can improve it progressively later on. For instance, their theme system is quite advanced, we could easily reproduce JHipster look and feel (basic bootstrap) and use same i18n properties files.
Also about roles, you can create them at realm level and at client level.

Owner

gmarziou commented Sep 16, 2017

Yes but it was 9 months ago, I have written some tips but they are at work and about version 2.x, so I should refresh them. One thing I remember for sure is that it's easy to mess up a working configuration using the GUI and having hard time to figure what's wrong, so exporting and re-importing in JSON is the way to go.
I think we can improve it progressively later on. For instance, their theme system is quite advanced, we could easily reproduce JHipster look and feel (basic bootstrap) and use same i18n properties files.
Also about roles, you can create them at realm level and at client level.

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 16, 2017

@danielpetisme I changed the authorities logic to allow groups or roles. Roles seems to be more native to Keycloak, while Okta tends to use groups. Here's what AccountResource.java uses for this logic now.

// get roles from details
if (details.get("roles") != null) {
    List roles = (ArrayList) details.get("roles");
    roles.forEach(role -> {
        // Ignore Keycloaks default roles, or add them to authorities.csv
        if (!String.valueOf(role).equalsIgnoreCase("offline_access") &&
            !String.valueOf(role).equalsIgnoreCase("uma_authorization")) {
            Authority userAuthority = new Authority();
            userAuthority.setName(role.toString());
            userAuthorities.add(userAuthority);
        }
    });
// if roles don't exist, try groups
} else if (details.get("groups") != null) {
    List groups = (ArrayList) details.get("groups");
    groups.forEach(group -> {
        // Ignore Okta's Everyone group, or add it to Liquibase's authorities.csv
        if (!String.valueOf(group).equalsIgnoreCase("everyone")) {
            Authority userAuthority = new Authority();
            userAuthority.setName(group.toString());
            userAuthorities.add(userAuthority);
        }
    });
} else {
    authentication.getAuthorities().forEach(role -> {
        Authority userAuthority = new Authority();
        userAuthority.setName(role.getAuthority());
        userAuthorities.add(userAuthority);
    });
}

All tests pass now! Will work on Travis tests next (probably Monday).

mraible commented Sep 16, 2017

@danielpetisme I changed the authorities logic to allow groups or roles. Roles seems to be more native to Keycloak, while Okta tends to use groups. Here's what AccountResource.java uses for this logic now.

// get roles from details
if (details.get("roles") != null) {
    List roles = (ArrayList) details.get("roles");
    roles.forEach(role -> {
        // Ignore Keycloaks default roles, or add them to authorities.csv
        if (!String.valueOf(role).equalsIgnoreCase("offline_access") &&
            !String.valueOf(role).equalsIgnoreCase("uma_authorization")) {
            Authority userAuthority = new Authority();
            userAuthority.setName(role.toString());
            userAuthorities.add(userAuthority);
        }
    });
// if roles don't exist, try groups
} else if (details.get("groups") != null) {
    List groups = (ArrayList) details.get("groups");
    groups.forEach(group -> {
        // Ignore Okta's Everyone group, or add it to Liquibase's authorities.csv
        if (!String.valueOf(group).equalsIgnoreCase("everyone")) {
            Authority userAuthority = new Authority();
            userAuthority.setName(group.toString());
            userAuthorities.add(userAuthority);
        }
    });
} else {
    authentication.getAuthorities().forEach(role -> {
        Authority userAuthority = new Authority();
        userAuthority.setName(role.getAuthority());
        userAuthorities.add(userAuthority);
    });
}

All tests pass now! Will work on Travis tests next (probably Monday).

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 16, 2017

Owner

@mraible it looks good, regarding ignored roles and groups, I'm not sure inserting them in database would be enough as for instance "everyone" has a special meaning which is interesting and that would require some coding in Authentication (maybe a feature request for later?).

So, at first, I prefer to ignore them as you did.
We could probably configure them in applications properties so that they can be easily set when generating project and later changed by user without requiring code change.

jhipster:
    security:
        authentication:
            ignored-authorithies: 
                - offline_access
                - uma_authorization
                - everyone

This way, we could also reduce the 3 "if" blocks to only one.

Owner

gmarziou commented Sep 16, 2017

@mraible it looks good, regarding ignored roles and groups, I'm not sure inserting them in database would be enough as for instance "everyone" has a special meaning which is interesting and that would require some coding in Authentication (maybe a feature request for later?).

So, at first, I prefer to ignore them as you did.
We could probably configure them in applications properties so that they can be easily set when generating project and later changed by user without requiring code change.

jhipster:
    security:
        authentication:
            ignored-authorithies: 
                - offline_access
                - uma_authorization
                - everyone

This way, we could also reduce the 3 "if" blocks to only one.

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 16, 2017

danielpetisme commented Sep 16, 2017

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 16, 2017

Owner

According to doc about offline tokens, to be able to issue an offline token, users need to have the role mapping for the realm-level role offline_access. I don't know whether we could not map it for a client app and then still get the offline_tokens feature.

Anyway, there is a simpler solution which consists in keeping only the roles which names start by "ROLE_" as this is what Spring Security expects and JHipster too both in server and client code..

Owner

gmarziou commented Sep 16, 2017

According to doc about offline tokens, to be able to issue an offline token, users need to have the role mapping for the realm-level role offline_access. I don't know whether we could not map it for a client app and then still get the offline_tokens feature.

Anyway, there is a simpler solution which consists in keeping only the roles which names start by "ROLE_" as this is what Spring Security expects and JHipster too both in server and client code..

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 16, 2017

Owner

Maybe AccountResource#getAccount() could use this:

        List<String> externalRoles = (List) details.get("roles");
        List<String> appRoles = externalRoles.stream()
            .filter(role -> role.startsWith("ROLE_"))
            .collect(Collectors.toList());
Owner

gmarziou commented Sep 16, 2017

Maybe AccountResource#getAccount() could use this:

        List<String> externalRoles = (List) details.get("roles");
        List<String> appRoles = externalRoles.stream()
            .filter(role -> role.startsWith("ROLE_"))
            .collect(Collectors.toList());
@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 16, 2017

I like it! Good idea @gmarziou

mraible commented Sep 16, 2017

I like it! Good idea @gmarziou

@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 17, 2017

There is a class for that (sounds like an Apple store ad), Spring security provides a SimpleAuthorityMapper which filter authorities starting with a particular prefix (and ROLE_ is the default).
The current extraction makes the job but would be cleaner to delate it to a dedicated class. What do you think?

danielpetisme commented Sep 17, 2017

There is a class for that (sounds like an Apple store ad), Spring security provides a SimpleAuthorityMapper which filter authorities starting with a particular prefix (and ROLE_ is the default).
The current extraction makes the job but would be cleaner to delate it to a dedicated class. What do you think?

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 17, 2017

Owner

It does not filter if I understood correctly, see unit test below here testMapper() fails.

package io.github.jhipster.security.logging;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;

import java.util.*;
import java.util.stream.Collectors;

public class RoleTest {

    private Map<String, List<String>> details;

    @Before
    public void init() {
        List<String> roleList = new ArrayList<>();
        roleList.add("ROLE_ADMIN");
        roleList.add("ROLE_USER");
        roleList.add("offline_access");
        roleList.add("uma_authorization");

        details = new HashMap<>();

        details.put("roles", roleList);
    }

    @Test
    public void test() {
        List<String> externalRoles = details.get("roles");
        List<String> appRoles = externalRoles.stream()
            .filter(role -> role.startsWith("ROLE_"))
            .collect(Collectors.toList());

        Assert.assertEquals(2, appRoles.size());
    }

    @Test
    public void testMapper() {
        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();

        List<String> externalRoles = details.get("roles");
        List<GrantedAuthority> externalAuthorities = externalRoles.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        Set<GrantedAuthority> grantedAuthorities = mapper.mapAuthorities(externalAuthorities);

        Assert.assertEquals(2, grantedAuthorities.size());
    }
}
Owner

gmarziou commented Sep 17, 2017

It does not filter if I understood correctly, see unit test below here testMapper() fails.

package io.github.jhipster.security.logging;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;

import java.util.*;
import java.util.stream.Collectors;

public class RoleTest {

    private Map<String, List<String>> details;

    @Before
    public void init() {
        List<String> roleList = new ArrayList<>();
        roleList.add("ROLE_ADMIN");
        roleList.add("ROLE_USER");
        roleList.add("offline_access");
        roleList.add("uma_authorization");

        details = new HashMap<>();

        details.put("roles", roleList);
    }

    @Test
    public void test() {
        List<String> externalRoles = details.get("roles");
        List<String> appRoles = externalRoles.stream()
            .filter(role -> role.startsWith("ROLE_"))
            .collect(Collectors.toList());

        Assert.assertEquals(2, appRoles.size());
    }

    @Test
    public void testMapper() {
        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();

        List<String> externalRoles = details.get("roles");
        List<GrantedAuthority> externalAuthorities = externalRoles.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());

        Set<GrantedAuthority> grantedAuthorities = mapper.mapAuthorities(externalAuthorities);

        Assert.assertEquals(2, grantedAuthorities.size());
    }
}
@danielpetisme

This comment has been minimized.

Show comment
Hide comment
@danielpetisme

danielpetisme Sep 17, 2017

danielpetisme commented Sep 17, 2017

@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 17, 2017

Owner

BTW, Daniel when you reply on github by e-mail, we see your signature with mobile phone number and e-mail address, I removed them.

Owner

gmarziou commented Sep 17, 2017

BTW, Daniel when you reply on github by e-mail, we see your signature with mobile phone number and e-mail address, I removed them.

@sebastienblanc

This comment has been minimized.

Show comment
Hide comment
@sebastienblanc

sebastienblanc Sep 18, 2017

TBH I didn't follow in details the whole discussions about the roles but to get rid off the ROLE_ prefix, you can use this in your SecurityConfig bean :

 @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

sebastienblanc commented Sep 18, 2017

TBH I didn't follow in details the whole discussions about the roles but to get rid off the ROLE_ prefix, you can use this in your SecurityConfig bean :

 @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }
@gmarziou

This comment has been minimized.

Show comment
Hide comment
@gmarziou

gmarziou Sep 18, 2017

Owner

Thanks Sébastien but we will not use Keycloak adapter for JHipster (unlike what I have done initially in this repo) because we want to support several IdP.

Owner

gmarziou commented Sep 18, 2017

Thanks Sébastien but we will not use Keycloak adapter for JHipster (unlike what I have done initially in this repo) because we want to support several IdP.

@mraible mraible referenced this issue Sep 27, 2017

Merged

Refactor OAuth 2.0 Support #6361

9 of 9 tasks complete
@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 29, 2017

Thanks for the tip @sdoxsee!

@mraible WRT keeping http://localhost:9000/login for the development redirect_uri, one workaround (this is for dev only) is to customize the OAuth2ClientAuthenticationProcessingFilter's attemptAuthentication

I added the following to SecurityConfiguration.java:

@Bean
public FilterRegistrationBean saveLoginOriginFilter() {
    Filter filter = new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
            if (request.getRequestURI().endsWith("/login") && request.getRemoteUser() == null) {
                String referer = request.getHeader("referer");
                if (!StringUtils.isBlank(referrer)) {
                    request.getSession().setAttribute(SAVED_LOGIN_ORIGIN_URI, referrer);
                }
            }
            filterChain.doFilter(request, response);
        }
    };
    FilterRegistrationBean bean = new FilterRegistrationBean(filter);
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

@Bean
public static DefaultRolesPrefixPostProcessor defaultRolesPrefixPostProcessor() {
    return new DefaultRolesPrefixPostProcessor();
}

public static class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FilterChainProxy) {

            FilterChainProxy chains = (FilterChainProxy) bean;

            for (SecurityFilterChain chain : chains.getFilterChains()) {
                for (Filter filter : chain.getFilters()) {
                    if (filter instanceof OAuth2ClientAuthenticationProcessingFilter) {
                        OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationProcessingFilter =
                            (OAuth2ClientAuthenticationProcessingFilter) filter;
                        oAuth2ClientAuthenticationProcessingFilter
                            .setAuthenticationSuccessHandler(new OAuth2AuthenticationSuccessHandler());
                    }
                }
            }
        }
        return bean;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
        return bean;
    }

    @Override
    public int getOrder() {
        return PriorityOrdered.HIGHEST_PRECEDENCE;
    }
}

And I created an OAuth2AuthenticationSuccessHandler.java in the same package:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * AuthenticationSuccessHandler that is Browsersync-aware and redirects to the origin you clicked "login" from.
 */
class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    static final String SAVED_LOGIN_ORIGIN_URI = SecurityConfiguration.class.getName() + "_SAVED_ORIGIN";

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

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication)
        throws IOException {

        handle(request, response);
        clearAuthenticationAttributes(request);
    }

    private void handle(HttpServletRequest request, HttpServletResponse response)
        throws IOException {

        String targetUrl = determineTargetUrl(request);

        if (response.isCommitted()) {
            log.error("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        }

        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    private String determineTargetUrl(HttpServletRequest request) {
        Object savedReferrer = request.getSession().getAttribute(SAVED_LOGIN_ORIGIN_URI);
        if (savedReferrer != null) {
            String savedLoginOrigin = request.getSession().getAttribute(SAVED_LOGIN_ORIGIN_URI).toString();
            request.getSession().removeAttribute(SAVED_LOGIN_ORIGIN_URI);
            return savedLoginOrigin;
        } else {
            return "/";
        }
    }

    private void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

This seems to work. What do y'all think? Is it good enough?

As for logout, I'm not sure we need that. When you logout of an app that you logged in with Facebook, it doesn't log you out of Facebook.

mraible commented Sep 29, 2017

Thanks for the tip @sdoxsee!

@mraible WRT keeping http://localhost:9000/login for the development redirect_uri, one workaround (this is for dev only) is to customize the OAuth2ClientAuthenticationProcessingFilter's attemptAuthentication

I added the following to SecurityConfiguration.java:

@Bean
public FilterRegistrationBean saveLoginOriginFilter() {
    Filter filter = new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
            if (request.getRequestURI().endsWith("/login") && request.getRemoteUser() == null) {
                String referer = request.getHeader("referer");
                if (!StringUtils.isBlank(referrer)) {
                    request.getSession().setAttribute(SAVED_LOGIN_ORIGIN_URI, referrer);
                }
            }
            filterChain.doFilter(request, response);
        }
    };
    FilterRegistrationBean bean = new FilterRegistrationBean(filter);
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

@Bean
public static DefaultRolesPrefixPostProcessor defaultRolesPrefixPostProcessor() {
    return new DefaultRolesPrefixPostProcessor();
}

public static class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FilterChainProxy) {

            FilterChainProxy chains = (FilterChainProxy) bean;

            for (SecurityFilterChain chain : chains.getFilterChains()) {
                for (Filter filter : chain.getFilters()) {
                    if (filter instanceof OAuth2ClientAuthenticationProcessingFilter) {
                        OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationProcessingFilter =
                            (OAuth2ClientAuthenticationProcessingFilter) filter;
                        oAuth2ClientAuthenticationProcessingFilter
                            .setAuthenticationSuccessHandler(new OAuth2AuthenticationSuccessHandler());
                    }
                }
            }
        }
        return bean;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
        return bean;
    }

    @Override
    public int getOrder() {
        return PriorityOrdered.HIGHEST_PRECEDENCE;
    }
}

And I created an OAuth2AuthenticationSuccessHandler.java in the same package:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * AuthenticationSuccessHandler that is Browsersync-aware and redirects to the origin you clicked "login" from.
 */
class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    static final String SAVED_LOGIN_ORIGIN_URI = SecurityConfiguration.class.getName() + "_SAVED_ORIGIN";

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

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication)
        throws IOException {

        handle(request, response);
        clearAuthenticationAttributes(request);
    }

    private void handle(HttpServletRequest request, HttpServletResponse response)
        throws IOException {

        String targetUrl = determineTargetUrl(request);

        if (response.isCommitted()) {
            log.error("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        }

        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    private String determineTargetUrl(HttpServletRequest request) {
        Object savedReferrer = request.getSession().getAttribute(SAVED_LOGIN_ORIGIN_URI);
        if (savedReferrer != null) {
            String savedLoginOrigin = request.getSession().getAttribute(SAVED_LOGIN_ORIGIN_URI).toString();
            request.getSession().removeAttribute(SAVED_LOGIN_ORIGIN_URI);
            return savedLoginOrigin;
        } else {
            return "/";
        }
    }

    private void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

This seems to work. What do y'all think? Is it good enough?

As for logout, I'm not sure we need that. When you logout of an app that you logged in with Facebook, it doesn't log you out of Facebook.

@mraible mraible referenced this issue Sep 29, 2017

Closed

Improve OAuth 2.0 / OIDC Integration #6432

4 of 4 tasks complete
@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 29, 2017

@mraible. Glad it worked. However it feels pretty heavy for something that is dev only. We at least should make the beans dev only profile (in case of security risks?). I had another thought. What if you saved the port in session storage in the JS client so that even if you came back to 8080 the JS would redirect you back to 9000? Maybe you could have a toggle with the development banner that would set the port to 8080 or 9000. Not super elegant and changes JS for both angular and react but the code could be stripped out in prod and it would leave the server side less heavy and confusing for people?

sdoxsee commented Sep 29, 2017

@mraible. Glad it worked. However it feels pretty heavy for something that is dev only. We at least should make the beans dev only profile (in case of security risks?). I had another thought. What if you saved the port in session storage in the JS client so that even if you came back to 8080 the JS would redirect you back to 9000? Maybe you could have a toggle with the development banner that would set the port to 8080 or 9000. Not super elegant and changes JS for both angular and react but the code could be stripped out in prod and it would leave the server side less heavy and confusing for people?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 29, 2017

mraible commented Sep 29, 2017

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 29, 2017

I think you're right about the ports and web storage. As far as I know cookies are shared across the ports of localhost (I've been burned by that at times) so perhaps a cookie could be used?

sdoxsee commented Sep 29, 2017

I think you're right about the ports and web storage. As far as I know cookies are shared across the ports of localhost (I've been burned by that at times) so perhaps a cookie could be used?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 29, 2017

@sdoxsee The problem with handling it in the client is 1) there will be code in the client that's unused in production and 2) where do we add the on-load hooks to do the redirect, and 3) the user experience of load-on-8080 and redirect-to-9000 won't be great in demos. I like the server-side solution because the extra code won't matter.

mraible commented Sep 29, 2017

@sdoxsee The problem with handling it in the client is 1) there will be code in the client that's unused in production and 2) where do we add the on-load hooks to do the redirect, and 3) the user experience of load-on-8080 and redirect-to-9000 won't be great in demos. I like the server-side solution because the extra code won't matter.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 29, 2017

sdoxsee commented Sep 29, 2017

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 29, 2017

@sdoxsee I created a PR at jhipster/generator-jhipster#6436 that implements the server-side solution.

The IdP doesn't need to be configured for http://localhost:9000 because even when you're on http://localhost:9000, the login request goes to http://localhost:8080. See _login.service.ts for more info.

mraible commented Sep 29, 2017

@sdoxsee I created a PR at jhipster/generator-jhipster#6436 that implements the server-side solution.

The IdP doesn't need to be configured for http://localhost:9000 because even when you're on http://localhost:9000, the login request goes to http://localhost:8080. See _login.service.ts for more info.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 29, 2017

@mraible I very well could be wrong but I think the server could be simpler if you kept the port "as is" in the _login.service.ts so that if 8080 then 8080 comes to the server and if 9000 webpack automatically proxies it to 8080 with the referer set to 9000. Then, on the server, you'd set the redirect_uri to 9000 and then you're done. The authorization_code comes back to 9000 (but gets proxied to 8080 on the server) and you carry on as normal? I may come up with a PR to illustrate. Thoughts?

sdoxsee commented Sep 29, 2017

@mraible I very well could be wrong but I think the server could be simpler if you kept the port "as is" in the _login.service.ts so that if 8080 then 8080 comes to the server and if 9000 webpack automatically proxies it to 8080 with the referer set to 9000. Then, on the server, you'd set the redirect_uri to 9000 and then you're done. The authorization_code comes back to 9000 (but gets proxied to 8080 on the server) and you carry on as normal? I may come up with a PR to illustrate. Thoughts?

@mraible

This comment has been minimized.

Show comment
Hide comment
@mraible

mraible Sep 29, 2017

@sdoxsee That could work. I didn't do it this way because I couldn't figure out how to dynamically set the redirect_uri value for Spring OAuth. I asked Joe Grandja of the Spring Security team about this:

How it works is the client sends a GET to http://localhost:8080/login, then Spring Security takes care of the redirect and callback. This works great, but always redirects back to http://localhost:8080. If the user is running JHipster in “dev mode”, the client runs on http://localhost:9000. Is there anyway to pass in the redirect to Spring Security so it’s the client URI (:9000) instead of the server URI (:8080)?

His response:

If I'm understanding you correctly? You would like to configure where the user is re-directed to after they successfully authenticate with the provider?
If this is correct than this is typically handled by an AuthenticationSuccessHandler that is associated with an AbstractAuthenticationProcessingFilter.

In spring-security-oauth, the OAuth2ClientAuthenticationProcessingFilter is an AbstractAuthenticationProcessingFilter which is responsible for exchanging the authorization grant for an Access Token. If the client is able to do the exchange than a successful authentication happens at which point the AuthenticationSuccessHandler.onAuthenticationSuccess is called. So this is where you can provide a SimpleUrlAuthenticationSuccessHandler (as an example) that re-directs/forwards to localhost:9000 for your case.

I see you are using @EnableOAuth2Sso to configure your client and after investigating the code it doesn't provide the capability to set a custom AuthenticationSuccessHandler on the OAuth2ClientAuthenticationProcessingFilter.

@EnableOAuth2Sso imports OAuth2SsoDefaultConfiguration (a WebSecurityConfigurerAdapter) which uses a SsoSecurityConfigurer to configure the OAuth2ClientAuthenticationProcessingFilter and hides pretty much everything. You will see in SsoSecurityConfigurer.oauth2SsoFilter() is where OAuth2ClientAuthenticationProcessingFilter is being configured and this is ultimately where an AuthenticationSuccessHandler would need to be set.

I'm afraid this one won't be easy unless you manual configure the filter. That's what I'm seeing now at least.

mraible commented Sep 29, 2017

@sdoxsee That could work. I didn't do it this way because I couldn't figure out how to dynamically set the redirect_uri value for Spring OAuth. I asked Joe Grandja of the Spring Security team about this:

How it works is the client sends a GET to http://localhost:8080/login, then Spring Security takes care of the redirect and callback. This works great, but always redirects back to http://localhost:8080. If the user is running JHipster in “dev mode”, the client runs on http://localhost:9000. Is there anyway to pass in the redirect to Spring Security so it’s the client URI (:9000) instead of the server URI (:8080)?

His response:

If I'm understanding you correctly? You would like to configure where the user is re-directed to after they successfully authenticate with the provider?
If this is correct than this is typically handled by an AuthenticationSuccessHandler that is associated with an AbstractAuthenticationProcessingFilter.

In spring-security-oauth, the OAuth2ClientAuthenticationProcessingFilter is an AbstractAuthenticationProcessingFilter which is responsible for exchanging the authorization grant for an Access Token. If the client is able to do the exchange than a successful authentication happens at which point the AuthenticationSuccessHandler.onAuthenticationSuccess is called. So this is where you can provide a SimpleUrlAuthenticationSuccessHandler (as an example) that re-directs/forwards to localhost:9000 for your case.

I see you are using @EnableOAuth2Sso to configure your client and after investigating the code it doesn't provide the capability to set a custom AuthenticationSuccessHandler on the OAuth2ClientAuthenticationProcessingFilter.

@EnableOAuth2Sso imports OAuth2SsoDefaultConfiguration (a WebSecurityConfigurerAdapter) which uses a SsoSecurityConfigurer to configure the OAuth2ClientAuthenticationProcessingFilter and hides pretty much everything. You will see in SsoSecurityConfigurer.oauth2SsoFilter() is where OAuth2ClientAuthenticationProcessingFilter is being configured and this is ultimately where an AuthenticationSuccessHandler would need to be set.

I'm afraid this one won't be easy unless you manual configure the filter. That's what I'm seeing now at least.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 30, 2017

Thanks for that info. I'm going to try out what I was thinking and I'll let you know.

sdoxsee commented Sep 30, 2017

Thanks for that info. I'm going to try out what I was thinking and I'll let you know.

@sdoxsee

This comment has been minimized.

Show comment
Hide comment
@sdoxsee

sdoxsee Sep 30, 2017

Having trouble hooking in a custom OAuth2ClientAuthenticationProcessingFilter without manually configuring the filter (like Joe Grandja said above) and like Dave Syer mentions when someone asks to add a custom one. I really only want to change a few lines in the OAuth2ClientAuthenticationProcessingFilter but I think it would end up being more code than what you have...and all for a non-prod setup. I'm happy to keep what you have unless I think of something else :)

sdoxsee commented Sep 30, 2017

Having trouble hooking in a custom OAuth2ClientAuthenticationProcessingFilter without manually configuring the filter (like Joe Grandja said above) and like Dave Syer mentions when someone asks to add a custom one. I really only want to change a few lines in the OAuth2ClientAuthenticationProcessingFilter but I think it would end up being more code than what you have...and all for a non-prod setup. I'm happy to keep what you have unless I think of something else :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment