Skip to content
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

Spring Boot Admin with OAuth2 sample implementation #1262

Closed
wants to merge 8 commits into from
Closed

Spring Boot Admin with OAuth2 sample implementation #1262

wants to merge 8 commits into from

Conversation

elijah-pl
Copy link

@elijah-pl elijah-pl commented Sep 18, 2019

The aim of this PR is to close #1195 by providing a sample application demonstrating integration with Spring Security 5 and OAuth2 module specifically.

Copy link
Collaborator

@joshiste joshiste left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elijah-pl Thank you for the hard work for adding this sample.

I think it would be great to also have a short chapter in the documentation, describing the steps we need to take for the admin server to integrate with oauth2 (leaving out the authorization and resource server part). Could you add some words on it?

</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
Copy link
Collaborator

@joshiste joshiste Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is in maintenance mode. I'm not very happy to use it as the authorization server most probably needs to be rewritten with the next spring security version (5.3)...

I wonder if there is any mock-authorization server we could use instead of rolling our own "implementation"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spring team prepared demo application with new OAuth2 approach some time ago. In that demo they made use of Cloudfoundry UAA

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on how easy it will be to set up authorization server using Spring Security 5.3. If it will be the matter of adding @SpringBootApplication class + application.yml, then probably we could stick to native solution.

At this moment I know nothing about 5.3, I will check it out later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, taking into consideration the fact that 5.3 is not on the horizon, and you are not quite satisfied with spring-security-oauth2-autoconfigure, I decided to update my solution and make use of Cloudfoundry UAA.

I played a bit with it, it was quite easy to setup. It supports all major grant types that we may be interested in: authorization_code, client_cridentials, password, etc.

At this point, I have just migrated already existing solution with password grant type.
If you are ok with Cloudfoundry UAA, we can close this thread and agree on grant type in another thread.

Whatever type we pick, it should be easy to change it in uaa.yml

Copy link
Collaborator

@joshiste joshiste Dec 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think, this sample shouldn't involve cloud foundry. May be the OpenId connect Playground may prove useful

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spring security team also announced that they won't provide any authorization server. They point users to keycloak or other oidc services.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to make this a stand-alone experience which is good for a sample I believe it is possible to implement the authorization server application based on good old spring-security-oauth2 while the resource server and the SBA would benefit from first-class oAuth2 support in Spring5.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spring security team also announced that they won't provide any authorization server. They point users to keycloak or other oidc services.

Could you please provide the source link? As I can see in their blog post (updated section), it seems like they changed their mind.

Anyway, I agree that this example, probably, should not focus too much on authorization server configuration - it should be more about the integration of SBA with Spring Security OAuth2.

My motivation for using Cloudfoundry UAA was the fact that it did not require an account creation on any 3rd party service in case someone wanted to run the example.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please provide the source link? As I can see in their blog post (updated section), it seems like they changed their mind.

I have no other information. But currently for me their decision is unchanged.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are no longer planning on adding Authorization Server support to Spring Security.

https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix

client-secret: secret
provider: my-provider
client-authentication-method: basic
authorization-grant-type: password
Copy link
Collaborator

@joshiste joshiste Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this sample should stick to common practices and use the authorization_code grant type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please provide an example flow? As far as I know authorization_code grant type is not well-suited for server-to-server authentication, so I try to understand your vision of how it should work step-by-step

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we both have a different understanding of the whole picture.

In my understanding, a not logged-in user is forwarded to the configured authorization provider by SBA. The authorization token allows the user a) to use SBA and b) to use the actuator endpoints of the client applications. Therefore SBA needs to forward the authorization token in requests made to the underlying applications. This should use the authorization_code grant-type.

The SBA server does also query some endpoints in the background (e.g. /health and /info). So the SBA server needs to authenticate against the client applications. This authentication should be done via the password grant-type or using basic auth.

Copy link
Author

@elijah-pl elijah-pl Sep 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SBA server does also query some endpoints in the background (e.g. /health and /info). So the SBA server needs to authenticate against the client applications. This authentication should be done via the password grant-type or using basic auth.

I don't think I follow this part. Why client applications must require an additional basic authentication in order to expose /health and /info endpoints? Should not those be in the scope of the token?

On the other hand, if SBA server is protected by OAuth2 and token is required to use it, how resource server should register itself?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I follow this part. Why client applications must require an additional basic authentication in order to expose /health and /info endpoints? Should not those be in the scope of the token?

Yeah the SBA server must have a token that allows to access these endpoints. (As alternative it might use some credentials and basic authentication to access those - think of it like an api key - which is imho easier to setup)

On the other hand, if SBA server is protected by OAuth2 and token is required to use it, how resource server should register itself?

Either just the endpoint for registering is left unprotected or the same applies here:
The sba clients either needs a oauth2 token or some other form of credentials for authentication...

Copy link
Author

@elijah-pl elijah-pl Sep 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prepared an example version with the scenario you proposed, however I encountered problems during testing the solution:

1. Refreshing available actuator endpoints

Consider the following scenario:

Given:
* Authorization, SBA and Resource servers are up.
* Resource server is protected with OAuth2, but allows accessing to /health and /info endpoints with basic authentication.

When:
* Resource server registers itself using basic authentication before SBA login is performed and token is obtained.
* SBA (using basic authentication) fetches available actuator endpoints of the Resource server.
* SBA login is performed and token is obtained.

Then:
* Requests to the Resource server include Bearer token
* UI does not display all details, but only Metadata and Health
* After restarting Resource server all endpoints are displayed

Reason:
* It seems like available endpoints are not refreshed after being once fetched.

Solution:
* Well, I am not sure if there is a way to trigger a refresh. Please, advice.

2. Problem fetching OAuth2AuthorizedClient from HttpHeadersProvider

Put simply, Spring provides a mechanism to fetch client (with token) using client-id and principal, however I can not find a convenient way to obtain a principal. SecurityContextHolder.getContext().getAuthentication() returns null when called from HttpHeadersProvider.

The workaround I found for now is to implement custom in-memory OAuth2AuthorizedClientService that is able to fetch any client with suiting scope, however I am not satisfied with the solution.

Copy link
Collaborator

@joshiste joshiste Dec 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put simply, Spring provides a mechanism to fetch client (with token) using client-id and principal, however I can not find a convenient way to obtain a principal. SecurityContextHolder.getContext().getAuthentication() returns null when called from HttpHeadersProvider.

This is due to 2 facts:

  1. Spring Boot Admin makes fetches in the background (without any user interaction). So SBA still need some own token. For these background fetches the easiest way would be to have some kind of basic auth using username/password or an api-token

  2. Spring Boot Admin uses the WebClient for making requests. Due to the reactive nature the request ist executed in a different thread and the ThreadLocal backing the SecurityContextHolder is not available.
    If you just need access to the original headers write a InstanceExchangeFilterFunction which has access to the headers from the original request (filtered by the HttpHeaderFilter and settings from spring.boot.admin.instance-proxy.ignored-headers - defaults to "Cookie", "Set-Cookie", "Authorization").
    If you need the principal itself I guess, we could include it to the request attributes passed to the InstanceExchangeFilterFunction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just need access to the original headers write a InstanceExchangeFilterFunction which has access to the headers from the original request (filtered by the HttpHeaderFilter and settings from spring.boot.admin.instance-proxy.ignored-headers - defaults to "Cookie", "Set-Cookie", "Authorization").

Not sure that access to the request headers would be enough to solve the problem.

Here is how OAuth2Login works in Spring 5. The requests between SBA front and SBA back would be authorized with a Cookie. All tokens obtained from OpenID Connect provider will be stored on the backend and fetched into Authentication object by Spring Security in the context of request processing. So basically this is what you want to do inside HttpHeadersProvider (pseudo-code):

    @Bean
    public HttpHeadersProvider customHttpHeadersProvider() {
        return  instance -> {

            if (requestContext.getRoute().equals("/actuator/info") || requestContext.getRoute().equals("/actuator/health")) {
                // user-agnostic request
                // use token obtained with Client Credentials Grant
            } else {
                Authentication authentication = securityContext.getAuthentication(); // as SecurityContextHolder.getContext().getAuthentication() will not work in reactive world

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

                httpHeaders.add(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", client.getAccessToken()));
            }

Without having requestContext and securityContext defined above I cannot see how the HttpHeadersProvider could be implemented.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 92.146% when pulling eb4db57 on elijah-pl:sample/oauth-2-spring-security-5 into 2b80dd3 on codecentric:master.

@elijah-pl
Copy link
Author

Please let me know if solution meets your expectations, so I can start working on documentation part.

@joshiste
Copy link
Collaborator

joshiste commented Oct 7, 2019

@elijah-pl sorry I'm currently quite busy and this PR is very huge. It will take some time for me to review this.

@Hccake
Copy link

Hccake commented Oct 26, 2019

I also encountered the problem of OAuth, looking forward to this sample

@joshiste
Copy link
Collaborator

@Hccake So may be you want to follow allong the instructions added in this PR and help me with the review?

@Hccake
Copy link

Hccake commented Oct 31, 2019

@joshiste Sorry I tried to download the pr code but it didn't work because I won't be using cloudfoundry。

@msamusenka
Copy link

Joining the discussion.

I've been trying to integrate SBA with newest Spring Security 5 OAuth2 implementation. I stopped at the same place as @elijah-pl did. When your code gets control in custom HttpHeadersProvider the security context is missing. If I break execution on breakpoint inside InstancesProxyController::endpointProxy the context is there. But after traveling through a number of reactive calls it gets lost when SBA forward a request to the client. I am new to the WebFlux world but quick search tells that reactive world supports ReactiveSecurityContextHolder wish provides a Mono<> of security context. https://stackoverflow.com/a/51133327/1921819

In general I believe that interface of HttpHeadersProvider should change either to reactive or to accept request context together with the instance.

@msamusenka
Copy link

One more comment regarding the /health and /info endpoints.

OAuth has a grant type named Client credentials https://oauth.net/2/grant-types/client-credentials/ which seems to be a preferred way of authenticating applications (clients) in OAuth2 world.

SBA will have to manage these tokens (take care of expiration and renewal) and use different tokens depending on the endpoint being reached.

@msamusenka
Copy link

Sorry for excessive activity in the thread.

My research shows that SecurityContextHolder.getContext().getAuthentication() when called from HttpHeadersProvider would randomly return null or authenticated Principle. I believe this depends on the thread where continuation is executed. If it runs on the request thread we get the context from @ThreadLocal variable and we get null otherwise.

I believe there is no way of implementing the OAuth integration without the support from core spring-boot-admin-server. It should provide the security context as well as the context of the request that is being made (as we need to distinguish between the endpoints that are authorized with user or client token).

I believe that the solution with custom OAuth2AuthorizedClientService proposed by @elijah-pl cannot be used as a sample since it does not provide a per-user security. The key that we use to search for the OAuth2AuthorizedClient is the openid scope. So if multiple users log into the application we will be picking a random authentication to authorize the requests to resource server endpoints.

@elijah-pl
Copy link
Author

@masm22 Thanks for your research.

Indeed, as I mentioned in the comment, custom OAuth2AuthorizedClientService is sort of a dirty workaround. It can be handy as a temporary solution for a single-user scenario.

At this point, I am not planning to do any further investigations on subject, as task requires a bit of @joshiste's commitment and cooperation.

@joshiste
Copy link
Collaborator

@elijah-pl sorry I let this PR slip a little bit - I'll will have a look right now at your comments.

@joshiste
Copy link
Collaborator

@masm22 Also thanks to you that you help reviewing this PR!

@joshiste joshiste force-pushed the master branch 2 times, most recently from c380d4e to c8f2793 Compare January 12, 2020 15:34
@jaadlani
Copy link

jaadlani commented Jul 2, 2020

Hello Guys,

is this PR still relevant?

@joshiste
Copy link
Collaborator

joshiste commented Jul 2, 2020

Hello Guys,
is this PR still relevant?

Hmm I the PR itself is a bit outdated - but the topic itself not...

@ewiegs4
Copy link

ewiegs4 commented Jul 17, 2020

I have OAuth2 working in servlet mode with Hazelcast session enabled. One thing I've noticed that I don't see addressed here is that when the session is expired/lost, the front end service calls will start getting 302s back. It looks like the front end code is equipped to handle 401s by redirecting the window.location, but 302s are not handled and require an explicit refresh of the browser to get re-authenticated.

@joshiste joshiste force-pushed the master branch 2 times, most recently from 57d7211 to d2b765b Compare November 18, 2020 15:30
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide Sample Project using OAuth2
7 participants