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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-boot-admin-samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<module>spring-boot-admin-sample-war</module>
<module>spring-boot-admin-sample-hazelcast</module>
<module>spring-boot-admin-sample-custom-ui</module>
<module>spring-boot-admin-sample-oauth2</module>
</modules>
<dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: '3'

services:
authorization-server:
build:
context: ./spring-boot-admin-sample-oauth2-authorization
container_name: sba-oauth2-auth
ports:
- 8081:8081

admin-server:
build:
context: ./spring-boot-admin-sample-oauth2-admin
container_name: sba-oauth2-admin
environment:
spring.security.oauth2.client.provider.my-provider.token-uri: http://authorization-server:8081/oauth/token
depends_on:
- authorization-server
command: ["./wait-for.sh authorization-server:8081 --timeout=120 -- java -jar app.jar"]
ports:
- 8080:8080

resource-server:
build:
context: ./spring-boot-admin-sample-oauth2-resource
container_name: sba-oauth2-resource
environment:
spring.boot.admin.client.url: http://admin-server:8080
spring.security.oauth2.resourceserver.jwt.jwk-set-uri: http://authorization-server:8081/.well-known/jwks.json
depends_on:
- authorization-server
- admin-server
command: ["./wait-for.sh admin-server:8080 --timeout=120 -- java -jar app.jar"]
ports:
- 8082:8082
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2014-2019 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-admin-sample-oauth2</artifactId>
<packaging>pom</packaging>
<name>Spring Boot Admin Sample OAuth2</name>
<description>Spring Boot Admin Sample OAuth2</description>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-samples</artifactId>
<version>${revision}</version>
<relativePath>..</relativePath>
</parent>
<modules>
<module>spring-boot-admin-sample-oauth2-admin</module>
<module>spring-boot-admin-sample-oauth2-authorization</module>
<module>spring-boot-admin-sample-oauth2-resource</module>
</modules>
<properties>
<nimbus-jose-jwt.version>7.8</nimbus-jose-jwt.version>
<spring-security-oauth2-autoconfigure.version>2.1.8.RELEASE</spring-security-oauth2-autoconfigure.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring-security-oauth2-autoconfigure.version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM openjdk:8-jre-alpine

COPY /target/spring-boot-admin-sample-oauth2-admin.jar app.jar

EXPOSE 8080

# Add wait-for
COPY wait-for.sh wait-for.sh
RUN chmod +x wait-for.sh

ENTRYPOINT ["/bin/sh", "-c"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2014-2019 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-admin-sample-oauth2-admin</artifactId>
<name>Spring Boot Admin Sample OAuth2 - Spring Boot Admin</name>
<description>Spring Boot Admin Sample OAuth2 - Spring Boot Admin</description>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-sample-oauth2</artifactId>
<version>${revision}</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>
de.codecentric.boot.admin.sample.oauth2.SpringBootAdminOAuth2AdminServerApplication
</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2014-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package de.codecentric.boot.admin.sample.oauth2;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;

@EnableAdminServer
@SpringBootApplication
public class SpringBootAdminOAuth2AdminServerApplication {

private static final String AUTHORIZATION_HEADER = "Authorization";

public static void main(String[] args) {
SpringApplication.run(SpringBootAdminOAuth2AdminServerApplication.class, args);
}

@Bean
public HttpHeadersProvider provider(@RegisteredOAuth2AuthorizedClient("my-client") OAuth2AuthorizedClient client) {
OAuth2AccessToken accessToken = client.getAccessToken();
return instance -> {
HttpHeaders headers = new HttpHeaders();
headers.set(AUTHORIZATION_HEADER, accessToken.getTokenType().getValue() + " " + accessToken.getTokenValue());
return headers;
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2014-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package de.codecentric.boot.admin.sample.oauth2.config;

import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class PasswordGrantTypeContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Map<String, Object>> {

@Override
public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
Map<String, Object> contextAttributes = new HashMap<>();
String scope = authorizeRequest.getAttribute(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope)) {
contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
StringUtils.delimitedListToStringArray(scope, " "));
}
contextAttributes.put(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME,
authorizeRequest.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME));
contextAttributes.put(
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME,
authorizeRequest.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME));
return contextAttributes;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2014-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package de.codecentric.boot.admin.sample.oauth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;

import static org.springframework.security.oauth2.client.OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME;
import static org.springframework.security.oauth2.client.OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// @formatter:off
httpSecurity
.csrf()
.disable()
.authorizeRequests()
.anyRequest().permitAll();
// @formatter:on
}

@Bean
public OAuth2AuthorizedClient authorizedClient(AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("my-client")
.principal(new UsernamePasswordAuthenticationToken("subject", "password"))
.attribute(USERNAME_ATTRIBUTE_NAME, "subject")
.attribute(PASSWORD_ATTRIBUTE_NAME, "password")
.build();
return clientManager.authorize(authorizeRequest);
}

@Bean
public AuthorizedClientServiceOAuth2AuthorizedClientManager clientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {

AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientService);

manager.setAuthorizedClientProvider(
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build());

manager.setContextAttributesMapper(new PasswordGrantTypeContextAttributesMapper());

return manager;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
server:
port: 8080

logging:
level:
org.springframework.web.client.RestTemplate: TRACE

spring:
security:
oauth2:
client:
registration:
my-client:
client-id: reader
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.

provider:
my-provider:
token-uri: http://localhost:8081/oauth/token
Loading