I'm currently looking at implementing an API Gateway using Spring Cloud Gateway. There will be some React clients to the APIs and also some devices. The devices will be granted access using OAuth device_code grant.
The API gateway will pass JWT to back end resource API servers, which will validate the request. I have followed the following example which seems to get me part of the way there.
https://spring.io/blog/2019/08/16/securing-services-with-spring-cloud-gateway
I've also been able to swap out the IdP in that example for WSO2.
I obtained a bearer token from the IdP using curl in order to test using an opaque token.
curl -u OzVconnyapPW2yWzxrSebCKmY9Qa:5cBcZmnnaW5gGOW3qt9sumw4Ubka -k -d "grant_type=password&username=admin&password=admin" -H "Content-Type:application/x-www-form-urlencoded" https://localhost:9443/oauth2/token
which returned me an access token.
curl -k http://springboot.example.com:8080/ -H "Authorization: Bearer 123b546d-f35b-38b8-a2c8-4e0d4487329d"
But this tries to redirect me to login.
How can I get Spring Cloud API Gateway to process requests where an Authorization: Bearer xxxx header has been added?
Am I using the correct 'opaque token' (i.e. is an Authorization token the correct way to do this)?
Can the oauth2 client be bypassed if an Authorization header is set and could a resource server be triggered instead?
This diagram represents my current thinking, which could be wrong (please say if so!)
API Gateway app:
package com.scg.gateway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
#Controller
#SpringBootApplication
public class GatewayApplication {
#Autowired
private TokenRelayGatewayFilterFactory filterFactory;
#Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("resource", r -> r.path("/resource")
.filters(f -> f.filters(filterFactory.apply())
.removeRequestHeader("Cookie")) // Prevents cookie being sent downstream
.uri("http://springboot.example.com:9000")) // Taking advantage of docker naming
.build();
}
#GetMapping("/")
public String index(Model model,
#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
#AuthenticationPrincipal OAuth2User oauth2User) {
model.addAttribute("userName", oauth2User.getName());
model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
model.addAttribute("userAttributes", oauth2User.getAttributes());
return "index";
}
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
API Gateway Yaml
server:
port: 8080
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.web.HttpLogging: DEBUG
org.springframework.security: DEBUG
org.springframework.security.oauth2: DEBUG
org.springframework.cloud.gateway: DEBUG
spring:
autoconfigure:
# TODO: remove when fixed https://github.com/spring-projects/spring-security/issues/6314
exclude: org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
gateway:
provider: wso2
client-id: OzVconnyapPW2yWzxrSebCKmY9Qa
client-secret: 5cBcZmnnaW5gGOW3qt9sumw4Ubka
authorization-grant-type: authorization_code
redirect-uri-template: "{baseUrl}/login/oauth2/code/wso2"
scope: openid,profile,email,resource.read
provider:
wso2:
authorization-uri: http://idp.example.com:9763/oauth2/authorize
token-uri: http://idp.example.com:9763/oauth2/token
user-info-uri: http://idp.example.com:9763/oauth2/userinfo
user-name-attribute: sub
jwk-set-uri: http://idp.example.com:9763/oauth2/jwks
Related
We are currently working on a POC of Spring Cloud Dataflow, deployed in an Azure Kubernetes Service.
One of our requirements is to integrate it with an Azure Active Directory.
The authentication part went smoothly, but for the authorization, we need SCDF to be able to recognize appRoles of users (RBAC). But the behaviour so far seems to be that only the scopes are exposed, and if all scopes were exposed, then the user has all the service-roles (ROLE_VIEW, ROLE_DESTROY, ROLE_DEPLOY,...).
For instance, we'd like one user to have ROLE_VIEW only, and another user to have both ROLE-VIEW and ROLE_DESTROY.
What we've done so far:
Following the documentation, we created an App Registration (dataflow-server) exposing the following scopes & API permissions:
api://dataflow-server/dataflow.destroy
api://dataflow-server/dataflow.view
api://dataflow-server/dataflow.deploy
api://dataflow-server/dataflow.manage
api://dataflow-server/dataflow.schedule
api://dataflow-server/dataflow.create
api://dataflow-server/dataflow.modify
We created 2 appRoles, "role_reader" and "role_writer", to grant to specific users.
And we updated the configmap of the SCDF server to include these configurations:
spring:
cloud:
dataflow:
security:
authorization:
provider-role-mappings:
dataflow-server:
map-oauth-scopes:true
role-mappings:
ROLE_VIEW: role_reader
ROLE_DESTROY:role_writer
ROLE_DEPLOY: role_writer
ROLE_CREATE: role_writer
ROLE_MANAGE: role_writer
ROLE_SCHEDULE: role_writer
ROLE_MODIFY: role_writer
And:
spring:
security:
oauth2:
client:
registration:
dataflow-server:
provider: azure
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
client-id: <client_id_from_the_azure_app_registration>
client-secret: <client_secret>
scope:
- openid
- profile
- email
- offline_access
- api://dataflow-server/dataflow.view
- api://dataflow-server/dataflow.destroy
- api://dataflow-server/dataflow.schedule
- api://dataflow-server/dataflow.manage
- api://dataflow-server/dataflow.create
- api://dataflow-server/dataflow.deploy
- api://dataflow-server/dataflow.modify
provider:
azure:
issuer-uri: https://login.microsoftonline.com/<tenant-id>/v2.0
user-name-attribute: name
resourceserver:
jwt:
jwt-set-uti: https://login.microsoftonline.com/<tenant-id>/v2.0/keys
My understanding after reading the docs was that this should be sufficient to map the service roles (ROLE_VIEW) with the Azure AppRoles (role_reader) and allow to use RBAC.
Where did I go wrong ?
Thanks in advance for taking the time to read !
I'm trying to link my microservices to my gateway. , but I'm not able to access the api-docs of my microservice through the gateway.
Error from Swagger-UI:
Failed to load API definition
Fetch error
Not Found http://localhost:8080/microservice/v2/api-docs
Swagger version:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
I can GET the api-docs through the the microservice directly (port 8081):
http://localhost:8081/v2/api-docs
But I'm not able to do it through the gateway (port 8080):
http://localhost:8080/microservice/v2/api-docs
Microservice's properties:
spring:
application:
name: microservice
server:
port: 5082
...
Gateway's properties:
server:
port: 2443
spring:
cloud:
gateway:
routes:
- id: microservice
uri: lb://microservice
predicates:
- Path=/microservice/**
...
The workaround that I've found is to add a #GetMapping in my Controller.java to point the URL specifically to the api-docs (but I'm pretty sure this is not the proper solution).
WebClient webClient;
#ResponseBody
#GetMapping(value = "/v2/api-docs")
public String getApiDocs()
{
factory = new DefaultUriBuilderFactory("http://localhost:8081");
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
this.webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.build();
return webClient.get()
.uri("/v2/api-docs")
.retrieve()
.bodyToMono(String.class)
.block();
}
FYI:
The microservice and gateway are both registered with Eureka.
I can access the api-docs of the gateway just fine, but not the api-docs of the microservice.
The issue is that the gateway is not configured to load swagger associated resources (UI, config files).
I will use in the explanation gateway config using #Configuration class, but probably it can be used in the properties file as well.
Specify for each service a custom Swagger API path. So it can be used to configure routing in the gateway in the next steps. It will add prefix /microservice1-api in the path for the swagger UI URL and swagger JSON configuration file.
springdoc:
api-docs:
path: /microservice1-api
swagger-ui:
path: /api.html
Specify the route to each prefix in the gateway config. So it will be properly route paths associated with service swagger resources.
RouteLocatorBuilder.Builder apiDocJsonRoutes(RouteLocatorBuilder.Builder builder) {
//uri can be actual URI or load balancer path
return builder
.route(p -> p.path( "/microservice1-api/**").uri("microservice1"))
.route(p -> p.path( "/microservice2-api/**").uri("microservice2"))
.route(p -> p.path("/microservice3-api/**").uri("microservice3"));
}
You have to define routing for /swagger-ui/index.html path to proper service. In this case path to service is recognized by URL parameter configUrl. After Step 1 if you open swagger in the browser, you will see that at the end of address line you have parameter ?configUrl=/microservice1-api/swagger-config. So if this parameter value has prefix associated with a specific service, you can use it for routing.
RouteLocatorBuilder.Builder apiDocUIRoutes(RouteLocatorBuilder.Builder builder) {
return builder
.route(p -> swaggerUi(p, "microservice1", "/microservice1-api/swagger-config"))
.route(p -> swaggerUi(p, "microservice2", "/microservice2-api/swagger-config"))
.route(p -> swaggerUi(p, "microservice3", "/microservice3-api/swagger-config"));
}
private Buildable<Route> swaggerUi(PredicateSpec p, String service, String expectedValue) {
return p.path("/swagger-ui/index.html").and().query("configUrl", expectedValue)
.uri(service);
}
Open API in the browser using http://gateway_path/microservice_route/api.html
EDIT/SOLUTION:
I've got it, partly thanks to #anemyte's comment. Although the eureka.hostname Property was not the issue at play (though it did warrant correction), looking more closely got me to the true cause of the problem: the network interface in use, port forwarding and (bad) luck.
The services that I chose for this prototypical implementation were those that have port-forwardings in a production setting (I must have unfortunately forgotten to add a port-forwarding to the example service below - dumb, though I do not know if this would have helped).
When a Docker Swarm service has a port forwarding, the container has an additional bridge interface in addition to the overlay interface which is used for internal container-to-container communication.
Unfortunately, the client services were choosing to register with Eureka with their bridge interface IP as the advertised IP instead of the internal swarm IP - possibly because that is what InetAddress.getLocalhost() (which is internally used by Spring Cloud) would return in that case.
This led me to erroneously believe that Spring Boot Admin could reach these services - as I externally could, when it in fact could not as the wrong IP was being advertised. Using cURL to verify this only compounded the confusion as I was using the overlay IP to check whether the services can communicate, which is not the one that was being registered with Eureka.
The (provisional) solution to the issue was setting the spring.cloud.inetutils.preferred-networks setting to 10.0, which is the default IP address pool (more specifically: 10.0.0.0/8) for the internal swarm networks (documentation here). There is also a blacklist approach using spring.cloud.inetutils.ignored-networks, however I prefer not to use it.
In this case, client applications advertised their actual swarm overlay IP to Eureka, and SBA was able to reach them.
I do find it a bit odd that I did not get any error messages from SBA, and will be opening an issue on their tracker. Perhaps I was simply doing something wrong.
(original question follows)
I have the following setup:
Service discovery using Eureka (with eureka.client.fetch-registry=true and eureka.instance.preferIpAddress=true)
Spring Boot Admin running in the same application as Eureka, with spring.boot.admin.context-path=/admin
Keycloak integration, such that:
SBA itself uses a service account to poll the various /actuator endpoints of my client applications.
The SBA UI itself is protected via a login page which expects an administrative login.
Locally, this setup works. When I start my eureka-server application together with client applications, I see the following correct behaviour:
Eureka running on e.g. localhost:8761
Client applications successfully registering with Eureka via IP registration (eureka.instance.preferIpAddress=true)
SBA running at e.g. localhost:8761/admin and discovering my services
localhost:8761/admin correctly redirects to my Keycloak login page, and login correctly provides a session for the SBA UI
SBA itself successfully polling the /actuator endpoints of any registered applications.
However, I have issues replicating this setup inside a Docker Swarm.
I have two Docker Services, let's say eureka-server and client-api - both are created using the same network and the containers can reach each other via this network (via e.g. curl). eureka-server correctly starts and client-api registers with Eureka right away.
Attempting to navigate to eureka_url/admin correctly shows the Keycloak login page and redirects back to the Spring Boot Admin UI after a successful login. However, no applications are registered and I cannot figure out why.
I've attempted to enable more debug/trace logging, but I see absolutely no logs; it's as if SBA is simply not fetching the Eureka registry.
Does anyone know of a way to troubleshoot this behaviour? Has anyone had this issue?
EDIT:
I'm not quite sure which settings may be pertinent to the issue, but here are some of my configuration files (as code snippets since they're not that small, I hope that's OK):
application.yaml
(Includes base eureka properties, SBA properties, and Keycloak properties for SBA)
---
eureka:
hostname: localhost
port: 8761
client:
register-with-eureka: false
# Registry must be fetched so that Spring Boot Admin knows that there are registered applications
fetch-registry: true
serviceUrl:
defaultZone: http://${eureka.hostname}:${eureka.port}/eureka/
instance:
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30
environment: eureka-test-${user.name}
server:
enable-self-preservation: false # Intentionally disabled for non-production
spring:
application:
name: eureka-server
boot:
admin:
client:
prefer-ip: true
# Since we are running in Eureka, "/" is already the root path for Eureka itself
# Register SBA under the "/admin" path
context-path: /admin
cloud:
config:
enabled: false
main:
allow-bean-definition-overriding: true
keycloak:
realm: ${realm}
auth-server-url: ${auth_url}
# Client ID
resource: spring-boot-admin-automated
# Client secret used for service account grant
credentials:
secret: ${client_secret}
ssl-required: external
autodetect-bearer-only: true
use-resource-role-mappings: false
token-minimum-time-to-live: 90
principal-attribute: preferred_username
build.gradle
// Versioning / Spring parents poms
apply from: new File(project(':buildscripts').projectDir, '/dm-versions.gradle')
configurations {
all*.exclude module: 'spring-boot-starter-tomcat'
}
ext {
springBootAdminVersion = '2.3.1'
keycloakVersion = '11.0.2'
}
dependencies {
compileOnly 'org.projectlombok:lombok'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation "de.codecentric:spring-boot-admin-starter-server:${springBootAdminVersion}"
implementation 'org.keycloak:keycloak-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-security'
compile "org.keycloak:keycloak-admin-client:${keycloakVersion}"
testCompileOnly 'org.projectlombok:lombok'
}
dependencyManagement {
imports {
mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakVersion}"
}
}
The actual application code:
package com.app.eureka;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
#EnableAdminServer
#EnableEurekaServer
#SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
Keycloak configuration:
package com.app.eureka.keycloak.config;
import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.security.Principal;
import java.util.Objects;
#KeycloakConfiguration
#EnableConfigurationProperties(KeycloakSpringBootProperties.class)
class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter {
private static final String X_API_KEY = System.getProperty("sba_api_key");
#Value("${keycloak.token-minimum-time-to-live:60}")
private int tokenMinimumTimeToLive;
/**
* {#link HttpHeadersProvider} used to populate the {#link HttpHeaders} for
* accessing the state of the disovered clients.
*
* #param keycloak
* #return
*/
#Bean
public HttpHeadersProvider keycloakBearerAuthHeaderProvider(final Keycloak keycloak) {
return provider -> {
String accessToken = keycloak.tokenManager().getAccessTokenString();
HttpHeaders headers = new HttpHeaders();
headers.add("X-Api-Key", X_API_KEY);
headers.add("X-Authorization-Token", "keycloak-bearer " + accessToken);
return headers;
};
}
/**
* The Keycloak Admin client that provides the service-account Access-Token
*
* #param props
* #return keycloakClient the prepared admin client
*/
#Bean
public Keycloak keycloak(KeycloakSpringBootProperties props) {
final String secretString = "secret";
Keycloak keycloakAdminClient = KeycloakBuilder.builder()
.serverUrl(props.getAuthServerUrl())
.realm(props.getRealm())
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientId(props.getResource())
.clientSecret((String) props.getCredentials().get(secretString))
.build();
keycloakAdminClient.tokenManager().setMinTokenValidity(tokenMinimumTimeToLive);
return keycloakAdminClient;
}
/**
* Put the SBA UI behind a Keycloak-secured login page.
*
* #param http
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/**/*.css", "/admin/img/**", "/admin/third-party/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll();
}
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) {
SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
grantedAuthorityMapper.setPrefix("ROLE_");
grantedAuthorityMapper.setConvertToUpperCase(true);
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
}
#Bean
protected SessionRegistry buildSessionRegistry() {
return new SessionRegistryImpl();
}
/**
* Allows to inject requests scoped wrapper for {#link KeycloakSecurityContext}.
* <p>
* Returns the {#link KeycloakSecurityContext} from the Spring
* {#link ServletRequestAttributes}'s {#link Principal}.
* <p>
* The principal must support retrieval of the KeycloakSecurityContext, so at
* this point, only {#link KeycloakPrincipal} values and
* {#link KeycloakAuthenticationToken} are supported.
*
* #return the current <code>KeycloakSecurityContext</code>
*/
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public KeycloakSecurityContext provideKeycloakSecurityContext() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Principal principal = Objects.requireNonNull(attributes).getRequest().getUserPrincipal();
if (principal == null) {
return null;
}
if (principal instanceof KeycloakAuthenticationToken) {
principal = (Principal) ((KeycloakAuthenticationToken) principal).getPrincipal();
}
if (principal instanceof KeycloakPrincipal<?>) {
return ((KeycloakPrincipal<?>) principal).getKeycloakSecurityContext();
}
return null;
}
}
KeycloakConfigurationResolver
(separate class to prevent circular bean dependency that happens for some reason)
package com.app.eureka.keycloak.config;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class KeycloakConfigurationResolver {
/**
* Load Keycloak configuration from application.properties or application.yml
*
* #return
*/
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
Logout controller
package com.app.eureka.keycloak.config;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
#Controller
class LogoutController {
/**
* Logs the current user out, preventing access to the SBA UI
* #param request
* #return
* #throws Exception
*/
#PostMapping("/admin/logout")
public String logout(final HttpServletRequest request) throws Exception {
request.logout();
return "redirect:/admin";
}
}
I unfortunately do not have a docker-compose.yaml as our deployment is done mostly through Ansible, and anonymizing those scripts is rather difficult.
The services are ultimately created as follows (using docker service create):
(some of these networks may not be relevant as this is a local swarm running on my personal node, of note are the swarm networks)
dev#ws:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
3ba4a65c319f bridge bridge local
21065811cbff docker_gwbridge bridge local
ti1ksbdxlouo services overlay swarm
c59778b105b5 host host local
379lzdi0ljp4 ingress overlay swarm
dd92d2f75a31 none null local
eureka-server Dockerfile:
FROM registry/image:latest
MAINTAINER "dev#com.app"
COPY eureka-server.jar /home/myuser/eureka-server.jar
USER myuser
WORKDIR /home/myuser
CMD /usr/bin/java -jar \
-Xmx523351K -Xss1M -XX:ReservedCodeCacheSize=240M \
-XX:MaxMetaspaceSize=115625K \
-Djava.security.egd=file:/dev/urandom eureka-server.jar \
--server.port=8761; sh
Eureka/SBA app Docker swarm service:
dev#ws:~$ docker service create --name eureka-server -p 8080:8761 --replicas 1 --network services --hostname eureka-server --limit-cpu 1 --limit-memory 768m eureka-server
Client applications are then started as follows:
Dockerfile
FROM registry/image:latest
MAINTAINER "dev#com.app"
COPY client-api.jar /home/myuser/client-api.jar
USER myuser
WORKDIR /home/myuser
CMD /usr/bin/java -jar \
-Xmx523351K -Xss1M -XX:ReservedCodeCacheSize=240M \
-XX:MaxMetaspaceSize=115625K \
-Djava.security.egd=file:/dev/urandom -Deureka.instance.hostname=client-api client-api.jar \
--eureka.zone=http://eureka-server:8761/eureka --server.port=0; sh
And then created as Swarm services as follows:
dev#ws:~$ docker service create --name client-api --replicas 1 --network services --hostname client-api --limit-cpu 1 --limit-memory 768m client-api
On the client side, of note are the following eureka.client settings:
eureka:
name: ${spring.application.name}
instance:
leaseRenewalIntervalInSeconds: 10
instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instanceId:${random.int}}
preferIpAddress: true
client:
registryFetchIntervalSeconds: 5
That's all I can think of right now. The created docker services are running in the same network and can ping each other by IP as well as by hostname (cannot show output right now as I am not actively working on this at the moment, unfortunately).
In the Eureka UI I can, in fact, see my client applications registered and running - it's only SBA which does not seem to notice that there are any.
I found nothing wrong with the configuration you have presented. The only weak lead I see is eureka.hostname=localhost from application.yml. localhost and loopback IPs are two things that better to be avoided with swarm. I think you should check if it isn't something network related.
I'm trying to create a sample authorization server using the spring security oauth2 framework. The tutorials are confusing compared to any other spring related examples.
Update: If you are looking for a working solution, go to my answer. Ignore the code below.
When I invoked the token issue endpoint, the following error was thrown
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
Here's my setup (using Groovy). I'm using spring-security-oauth2:2.3.4.RELEASE, spring-cloud-security:2.0.1.RELEASE, and boot:2.1.1.RELEASE.
#Configuration
#CompileStatic
class OAuth2ClientSpringConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
AuthenticationManager authenticationManager
#Autowired
UserDetailsService userDetailsService
// register clients
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient('clientone')
.secret('secret')
.authorizedGrantTypes('password')
.scopes('one', 'two')
}
//use default auth manager and user details service
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients() //<--- update
}
}
Static user credentials
#Configuration
#CompileStatic
class UserCredentialsSpringConfig extends WebSecurityConfigurerAdapter {
#Bean
AuthenticationManager authenticationManagerBean() {
super.authenticationManagerBean()
}
#Bean
UserDetailsService userDetailsServiceBean() {
super.userDetailsServiceBean()
}
protected void configure(AuthenticationManagerBuilder auth) {
auth.inMemoryAuthentication()
.withUser('user1').password('pwd1').roles('USER')
.and()
.withUser('admin').password('pwd1').roles('ADMIN')
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/token").permitAll()
.anyRequest().authenticated()
}
}
Postman setup
Added the client credentials in both places - Authorization header and in a normal header.
Used the body form params to send user credentials
Ran into the following error when I hit http://localhost:8080/auth/oauth/token
I've looked into different tutorials and not figured it out. Any inputs would be helpful.
There are several tutorials online but some of them fail to mention that they are using Boot 1.x. That's what I ran into when I combined those instructions with Boot 2.x. Those tutorials might be valid with Boot 2.x but things were already confusing enough that I could not figure out a working model.
Here's a working solution for Boot 2.x. I started afresh with the code example at https://github.com/spring-projects/spring-security-oauth2-boot/tree/master/samples/spring-boot-sample-secure-oauth2-provider .
Import the spring-security-oauth2-autoconfigure dependency.
Add #EnableAuthorizationServer to your main class. That's all you need for a simplest working model. No need to add a AuthorizationServerConfigurerAdapter, etc because it's auto configured with the data from application.yml. Take a look at the application.yml in the above github link.
Here's the Postman configuration
Copy the sample client id and secret from the application.yml and supply them as Auth headers.
Copy the generated password from boot log and put it in Body form data along with other attributes as shown in the picture.
That's it. Hit http://localhost:8080/oauth/token and you should see something like below
By specification, the token issue endpoint must be protected.
You have to provide client_id and client_secret as parameters (form) or as an http-basic authorization header
Taken directly from spec
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
Please review
https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1
Why don't we remove the client out of your equation for a bit, and just focus on your Token Endpoint
curl is your friend, here is a FORM example (verified working example against Cloud Foundry UAA local where client secret is an empty string)
curl -v \
-H "Content-Type=application/x-www-form-urlencoded" \
-H "Accept=application/json" \
-d "grant_type=password" \
-d "client_id=cf" \
-d "response_type=token" \
-d "username=marissa" \
-d "password=koala" \
-d "client_secret=" \
http://localhost:8080/uaa/oauth/token
and using http-basic
curl -v \
-H "Content-Type=application/x-www-form-urlencoded" \
-H "Accept=application/json" \
-d "grant_type=password" \
-d "client_id=cf" \
-d "response_type=token" \
-d "username=marissa" \
-d "password=koala" \
-u "cf:" \
http://localhost:8080/uaa/oauth/token
Can you run these commands with your actual data and let us know the commands (perhaps update your question) and let us know the results. You see, we don't know what your UI app is actually doing, and using curl, will let us eliminate that from the question.
Has methods secure Config Server by oauth2 token ?
I plan to implement spring cloud config-server by oauth2 token,so client-server can fetch property by :
cloud:
config:
uri: http://user:password#localhost:8888
Is it feasible ?
but ... i met some problems .
I start a demo https://github.com/keryhu/spring-oauth2-config-server.git
It contains four services :
1 : eureka : start first,and can implement service register and discovery,it has no oauth2 enviroment.
2 : auth-server : JWT OAuth2 server configuration ,start secondly.
#SessionAttributes("authorizationRequest")
#EnableResourceServer
#EnableDiscoveryClient
and inmemory user :
security:
user:
password: password
3: config-server : start thirdly
#EnableDiscoveryClient
#EnableConfigServer
#EnableResourceServer
and in application.yml :
spring:
cloud:
config:
server:
git:
uri: https://github.com/keryhu/cloud-config
security:
oauth2:
resource:
jwt:
keyValue: |
-----BEGIN PUBLIC KEY-----
....
-----END PUBLIC KEY-----
4: pc-gateway : is a client-server,also a ui server. start lastly
When i test the secured uri: http://localhost:8080/hello, the page was redirected to
http://localhost:9999/uua/login
After entering "user:password",it redirects back
http://localhost:8080/hello
So i think the oauth-server and oauth-client is fine.
but.. i also set the following configuration in bootstrap.yml
cloud:
config:
uri: http://user:password#localhost:8888
When starting pc-gateway service, Fetching config from server has 401 Unauthorized errors :
INFO 954 --- [main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
WARN 954 --- [main] c.c.c.ConfigServicePropertySourceLocator : Could not locate PropertySource: 401 Unauthorized
Need help ! thanks !