Spring cloud gateway with swagger 3 openapi doesn't work - swagger-ui

Please help, I tried to configure the security permissionAll endpoint but it still doesn't work:
Swagger config
#Bean
#Lazy(false)
public List<GroupedOpenApi> apis(SwaggerUiConfigParameters swaggerUiConfigParameters, RouteDefinitionLocator locator) {
List<GroupedOpenApi> groups = new ArrayList<>();
List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
for (RouteDefinition definition : definitions) {
System.out.println("id: " + definition.getId()+ " "+definition.getUri().toString());
}
definitions.stream().filter(routeDefinition -> routeDefinition.getId().matches(".*-service")).forEach(routeDefinition -> {
String name = routeDefinition.getId().replaceAll("-service", "");
swaggerUiConfigParameters.addGroup(name);
GroupedOpenApi.builder().pathsToMatch("/" + name + "/**").group(name).build();
});
return groups;
}
gateway application.yml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: system-service
uri: lb://system-service
predicates:
- Path=/system/**
filters:
- RewritePath=/system/(?<path>.*), /$\{path}
- id: openapi
uri: http://localhost:${server.port}
predicates:
- Path=/v3/api-docs/**
filters:
- RewritePath=/v3/api-docs/(?<path>.*), /$\{path}/v3/api-docs
WebFluxSecurityConfig
private final String[] whiteListSwagger = {
"/v3/api-docs/**", "/swagger-ui/**", "/webjars/**", "/swagger-ui.html"
};
/**
* Spring security filter chain security web filter chain.
*
* #param http the http
* #return the security web filter chain
*/
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.cors().configurationSource(corsConfigurationSource()).and().csrf().disable()
.authorizeExchange()
.pathMatchers(whiteListOauth2).permitAll()
.pathMatchers(whiteListSwagger).permitAll()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
Output
enter image description here
Tks.
I tried to read the document but still not solved the problem.
springdoc-openapi v1.6.12

maybe this will help you:
https://github.com/springdoc/springdoc-openapi/issues/1678
#Bean
MultipleOpenApiWebFluxResource multipleOpenApiResource(
List<GroupedOpenApi> groupedOpenApis,
ObjectFactory<OpenAPIService> defaultOpenAPIBuilder,
AbstractRequestService requestBuilder,
GenericResponseService responseBuilder,
OperationService operationParser,
SpringDocConfigProperties springDocConfigProperties,
SpringDocProviders springDocProviders) {
return new MultipleOpenApiWebFluxResource(
groupedOpenApis,
defaultOpenAPIBuilder,
requestBuilder,
responseBuilder,
operationParser,
springDocConfigProperties,
springDocProviders);
}

Related

Using Auth0 in Spring Cloud Gateway, problem with getting ID Token

We have problems with obtaining ID token using auth0 SDK.
We have API Gateway based on Spring Cloud Gateway (version 3.1.4) where we try to use your auth0 platform to authenticate the users and then route the exchange to our micro services. To do it we would like to use ID Token and get email from it and pass this email to our micro services.
We log in by hitting oauth2/authorization/auth0 endpoint, we are being redirected to auth0 login page, where we provide credentials, then we get redirect back to our app.
When we configuire endpoints directly in API Gateway and mark them with #AuthenticationPrincipal OidcUser user it works and we have full user details as well as ID token.
When we proxy the exchange to different service we have Authorisation header in the request, which contains only header & signature part without payload in the ID token.
We would need the payload in ID Token in order to fetch user email for mapping the user with our internal DB in our micro services.
What do you think would be the proper workflow in this case and how can we solve this issue?
We tried to use Rules & Actions, which I paste below, but it didn’t helped us.
Our configuration looks like this:
#Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/test").authenticated()
.anyExchange().authenticated()
.and().oauth2Login()
.and().logout().logoutSuccessHandler(logoutSuccessHandler())
.and().build();
}
In the RouteLocator in GatewayConfiguration we have filter for TokenRelay.
Our Action looks like this:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'http://test.{our_local_development_route}:8888';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/claims/email`, event.user.email);
api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
}
};
And Rule:
function addEmailToAccessToken(user, context, callback) {
// This rule adds the authenticated user's email address to the access token.
const namespace = 'http://test.{our_local_development_route}:8888';
context.idToken[namespace + 'email'] = user.upn;
context.accessToken[namespace + 'email'] = user.email;
return callback(null, user, context);
}
I recently created a microservices architecture with Spring Cloud Gateway and Auth0. I wrote about how I created it on the Auth0 blog. That's not the interesting part. The interesting part is JHipster generates a ReactiveJwtDecoder that calls a /userinfo endpoint if some claims aren't available in the access token. This way, the access token is enriched with identity information before it's relayed to downstream microservices.
#Bean
ReactiveJwtDecoder jwtDecoder(ReactiveClientRegistrationRepository registrations) {
Mono<ClientRegistration> clientRegistration = registrations.findByRegistrationId("oidc");
return clientRegistration
.map(oidc ->
createJwtDecoder(
oidc.getProviderDetails().getIssuerUri(),
oidc.getProviderDetails().getJwkSetUri(),
oidc.getProviderDetails().getUserInfoEndpoint().getUri()
)
)
.block();
}
private ReactiveJwtDecoder createJwtDecoder(String issuerUri, String jwkSetUri, String userInfoUri) {
NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return new ReactiveJwtDecoder() {
#Override
public Mono<Jwt> decode(String token) throws JwtException {
return jwtDecoder.decode(token).flatMap(jwt -> enrich(token, jwt));
}
private Mono<Jwt> enrich(String token, Jwt jwt) {
// Only look up user information if identity claims are missing
if (jwt.hasClaim("given_name") && jwt.hasClaim("family_name")) {
return Mono.just(jwt);
}
// Retrieve user info from OAuth provider if not already loaded
return users.get(
jwt.getSubject(),
s -> {
WebClient webClient = WebClient.create();
return webClient
.get()
.uri(userInfoUri)
.headers(headers -> headers.setBearerAuth(token))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.map(userInfo ->
Jwt
.withTokenValue(jwt.getTokenValue())
.subject(jwt.getSubject())
.audience(jwt.getAudience())
.headers(headers -> headers.putAll(jwt.getHeaders()))
.claims(claims -> {
String username = userInfo.get("preferred_username").toString();
// special handling for Auth0
if (userInfo.get("sub").toString().contains("|") && username.contains("#")) {
userInfo.put("email", username);
}
// Allow full name in a name claim - happens with Auth0
if (userInfo.get("name") != null) {
String[] name = userInfo.get("name").toString().split("\\s+");
if (name.length > 0) {
userInfo.put("given_name", name[0]);
userInfo.put("family_name", String.join(" ", Arrays.copyOfRange(name, 1, name.length)));
}
}
claims.putAll(userInfo);
})
.claims(claims -> claims.putAll(jwt.getClaims()))
.build()
);
}
);
}
};
}

Spring security returns WWW-Authenticate header and shows http basic window when httpBasic is disabled

I have security configuration for my webflux server:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
#Bean
fun userDetailsService(): ReactiveUserDetailsService {
val user: UserDetails = User.builder()
.username(userName)
.password(passwordEncoder().encode(password))
.roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user)
}
#Bean
fun passwordEncoder() = BCryptPasswordEncoder()
#Bean("CustomAuth")
fun authManager():
ReactiveAuthorizationManager<AuthorizationContext> {
return ReactiveAuthorizationManager<AuthorizationContext> { mono, context ->
val request = context.exchange.request
val mutateExchange = context.exchange.mutate()
val token = request.headers[AUTHORIZATION] ?: throw
AccessDeniedException(ERROR_MESSAGE)
mono
// go to other service to check token
.then(webClient.checkToken(token.first().toString()))
.doOnError {
throw AccessDeniedException(ERROR_MESSAGE)
}
.cast(ResponseEntity::class.java)
.map { it.body as AuthDto }
.doOnNext { auth ->
mutateExchange.request {
it.header(USER_ID, auth.userId.toString())
it.header(AUTH_SYSTEM, auth.authSystem)
}
}
.map { AuthorizationDecision(true) }
}
}
As you can see httpBasic() option is disabled. When I go to any secure url, browser shows http basic window. Then I can enter valid or INVALID login and password and if authManager returns good result authentication will be successful or 401 will thrown in other case and auth window in browser will reopen.
Why it happens? Is it bug?
P.S. Spring boot version 2.5.5
The solution helped me:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.exceptionHandling()
.authenticationEntryPoint { exchange, _ ->
val response = exchange.response
response.statusCode = HttpStatus.UNAUTHORIZED
response.headers.set(HttpHeaders.WWW_AUTHENTICATE, "None")
exchange.mutate().response(response)
Mono.empty()
}
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
We need change authenticationEntryPoint in part of status code and disabling HttpHeaders.WWW_AUTHENTICATE. Then return changed response.

Dynamically set registration id from query param

I have the following controller to get registration ClientA when the endpoint is called.
#GetMapping("/token")
fun token(
#RegisteredOAuth2AuthorizedClient("clientA") authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
I want to have the client as a query param and dynamically start the OAuth2 process. How could I achieve it? sth like the following:
#GetMapping("/token?client={client}")
fun token(
#RegisteredOAuth2AuthorizedClient(${client}) authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
My solution is to use the default authorization uri and defaultSuccessUrl("/token")
The goal is to scale the client easily, and this solution can achieve the same.
Solution:
Register clientA in application.yml
clientA:
client-id: Any
redirect-uri: http://localhost/index
provider: clientA-provider
scope: launch
client-name: clientA
client-authentication-method: none
authorization-grant-type: authorization_code
Now the endpoint /oauth2/authorization/clientA is created automatically by spring security. For getting a token, call the above url.
After successful authorization, the endpoint goes to /token, due to defaultSuccessUrl("/token").
The following shows the related code snippet:
override fun configure(http: HttpSecurity) {
http.csrf()
.disable()
.oauth2Login()
.authorizationEndpoint()
.and()
.tokenEndpoint()
.and()
.defaultSuccessUrl("/token")
#Controller
#RequestMapping("/token")
class LaunchController(private val tokenService: TokenService) {
#GetMapping
fun token(): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity.ok(token)
}
}

Spring to Ping: how to configure Spring Rest Service to use External Authorization Server PingFederate

Does anyone know how to configure a Spring Rest Service to use PingFederate as an External Authorization Server?
Asked this question before, it was closed for god knows why. But here is the answer that I found. I based this on a demo that uses Google as an external authorization server. The problem with the usual demos is that they all use the Spring Authorization Server. Here is the place to start https://arnoldgalovics.com/google-oauth-with-spring-security-as-separated-resource-server/ Then modify the GoogleAccessTokenValidator like this (below). Questions, fire away...
private HttpHeaders createHeaders(final String username, final String password){
return new HttpHeaders() {{
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")) );
String authHeader = "Basic " + new String( encodedAuth );
set( "Authorization", authHeader );
}};
}
#SuppressWarnings("unchecked")
private Map<String, ?> getPingResponse(String accessToken) {
//Ping speaks text/html
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof StringHttpMessageConverter) {
StringHttpMessageConverter stringConverter = (StringHttpMessageConverter) converter;
stringConverter.setSupportedMediaTypes(ImmutableList.of(new MediaType("text", "html", StringHttpMessageConverter.DEFAULT_CHARSET)));
}
}
//URL
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(checkTokenUrl)
.queryParam("grant_type", "urn:pingidentity.com:oauth2:grant_type:validate_bearer")
.queryParam("token", accessToken);
String url = builder.build().encode().toUri().toString();
//Basic Auth (from Ping, OAuth Settings, Manage Clients
HttpEntity<Object> requestEntity = new HttpEntity<Object>(createHeaders("my-trusted-client", "secret"));
//unused Spring exchange variables
Map<String, String> variables = ImmutableMap.of("ping does not", "use this"); //token only in queryParam above
//validation call to Ping
Map map = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class, variables).getBody();
return (Map<String, Object>) map;
}
I tried this using jose4j library
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.6</version>
</dependency>
Now, following is the code which validates the JWT and get claims.
String jwtToken = "<token>"
HttpsJwks httpsJkws = new HttpsJwks("<Ping Server Public cert URL>");
HttpsJwksVerificationKeyResolver httpsJwksKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setAllowedClockSkewInSeconds(30)
.setRequireSubject()
.setExpectedIssuer("<Issuer URL>")
.setExpectedAudience("<audience>")
.setVerificationKeyResolver(httpsJwksKeyResolver)
.setJwsAlgorithmConstraints(
AlgorithmConstraints.ConstraintType.PERMIT, AlgorithmIdentifiers.RSA_USING_SHA256)
.build();
try
{
JwtClaims jwtClaims = jwtConsumer.processToClaims(jwtToken);
} catch (InvalidJwtException e) {
System.out.println("Invalid JWT! " + e);
if (e.hasExpired())
{
System.out.println("JWT expired at " + e.getJwtContext().getJwtClaims().getExpirationTime());
}
if (e.hasErrorCode(ErrorCodes.AUDIENCE_INVALID))
{
System.out.println("JWT had wrong audience: " + e.getJwtContext().getJwtClaims().getAudience());
}
}
We can integrate above code via SpringBoot interceptor by extracting the JWT token received in HTTP header.

Spring security and Stomp not seeing headers

I have a spring security application and I'm trying implement websockets using Stomp.
The application is mainly REST based, using tokens for security. All requests coming in have to a security token in the header.
The problem is when setting up a simple Stomp client using basic html, spring appears to not be seeing any headers.
The client works fine if I disable the security, in which case no headers are passed in.
var socket = new SockJS('http://localhost:8080/project/ws/wsendpoint');
var headers = {'Auth': 'some_auth_token'}
writeConsole("Created socket");
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
window.onbeforeunload = disconnectClient;
Heres the relevant spring configuration
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.anyRequest().authenticated() authenticated.
.and()
.anonymous().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
http.addFilterBefore(authenticationTokenFilter(), BasicAuthenticationFilter.class);
}
The doFilter in the authenticationTokenFilter class should see the header field 'Auth', as set in the client, however nothing is there.
Instead of sending header, you can replace sessionId with your own ID.
var sessionId = utils.random_string(36);
var socket = new SockJS('/socket', [], {
sessionId: () => {
return sessionId
}
});
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
Stomp cannot send any custom headers during the initial authentication stage. The way round this was to send the authentication token as a query parameter (would not recommended for non-closed systems).

Resources