i'm newbie with micronaut and can't get value in a body request.
I'm using "x-www-form-urlencoded", and i need get some values for use on authentication.
Controller:
#Controller
#Secured(SecurityRule.IS_AUTHENTICATED)
public class ControllerApi {
#Post("/hello")
#Produces(MediaType.TEXT_PLAIN)
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public String helloPost(){
return "POST hello ";
}
}
AuthenticationProviderForm:
#Singleton
public class AuthenticationProviderForm implements AuthenticationProvider {
#Override
public Publisher<AuthenticationResponse> authenticate(#Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authenticationRequest) {
httpRequest.getBody(); //This return is "empty", and i need the values here.
}
}
Request:
curl --location --request POST 'http://localhost:8080/hello' \
--header 'Origin: localhost' \
--header 'Authorization: Basic YWxhbjEyMzpjYWp1MTIz' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=TESTE' \
--data-urlencode 'client_id=alan123' \
--data-urlencode 'client_secret=caju123'
Thanks !
Try seting your header to accept urlencoded content type:
httpRequest.setHeader('Content-Type', 'application/x-www-form-urlencoded');
The body will always be empty inside of a AuthenticationProvider
Since version 2.5, the Micronaut Framework executes the filters
and then it reads the HTTP Request’s body.
SecurityFilter evaluates the beans of type SecurityRule.
Because of that, SecurityRule cannot rely on HTTP Request’s body
because the Micronaut Framework has not read the body yet.
I had a similar issue where I needed to get at a captcha that was included in the post body. Here's what I ended up doing to get access to it.
my post looks like this:
### Login with captcha
POST http://localhost:8081/login
Content-Type: application/x-www-form-urlencoded
username=myuser&password=mypass&captcha=12345
my authProvider's authenticate method:
#Override
public Publisher<AuthenticationResponse> authenticate(#Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authRequest)
{
if (httpRequest == null)
return Mono.empty();
final Optional<LoginCredentials> loginRequestOptional = httpRequest.getBody(LoginCredentials.class);
if (loginRequestOptional.isEmpty())
{
return Mono.empty();
}
else
{
final LoginCredentials loginRequest = loginRequestOptional.get();
return Flux.create(emitter -> {
final AuthenticationResponse authResponse = authenticateHavingCaptcha(loginRequest, httpRequest);
emitter.next(authResponse);
emitter.complete();
});
}
and the LoginCredentials class looks like this:
#Introspected
public class LoginCredentials extends UsernamePasswordCredentials
{
#NotBlank
#NotNull
private String captcha;
public String getCaptcha()
{
return captcha;
}
public void setCaptcha(String captcha)
{
this.captcha = captcha;
}
}
I don't know why you're getting an empty httpRequest, but hopefully this will help you on your way.
Related
This question already has answers here:
How to fix role in Spring Security?
(2 answers)
Closed 7 days ago.
A Spring UserDetailsService contains an admin with role ADMIN and an user with role USER:
#Bean
public UserDetailsService uds() {
UserDetails admin = User.builder()
.username("admin")
.password(encoder().encode("pwa"))
.roles("ADMIN")
.build();
UserDetails user = User.builder()
.username("user")
.password(encoder().encode("pwu"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
The repository extends CrudRepository
#RepositoryRestResource
public interface CountryRepo extends CrudRepository<Country, Long> { }
where POST requests map to the save method.
The SecurityFilterChain requires POST requests to be sent by clients with ADMIN role (see (*)):
#Bean
SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authCustomizer -> authCustomizer
.requestMatchers("/api/countries").permitAll()
.requestMatchers("/h2").permitAll()
.requestMatchers(HttpMethod.POST, "/api/countries").hasRole("ADMIN") // <-- (*)
.anyRequest().authenticated()
)
.csrf().disable()
.httpBasic(withDefaults());
http.headers().frameOptions().disable();
return http.build();
}
csrf and frameOptions are needed to use the H2 console in the browser.
An admin can create countries with CURL:
C:\Users\project>curl -H "Content-Type: application/json" -u admin:pwa -X POST http://localhost:8080/api/countries -d "{\"countryCode\":\"AUS\"}"
{
"countryCode" : "AUS",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/countries/4"
},
"country" : {
"href" : "http://localhost:8080/api/countries/4"
}
}
}
However, so can a user! I expected to get a 401 or 403 HTTP status code here.
C:\Users\mahed>curl -H "Content-Type: application/json" -u user:pwu -X POST http://localhost:8080/api/countries -d "{\"countryCode\":\"CAN\"}"
{
"countryCode" : "CAN",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/countries/5"
},
"country" : {
"href" : "http://localhost:8080/api/countries/5"
}
}
}
Why are the POST requests by user, who does not have the ADMIN role, successful?
The requestMatcher matching the POST request needs to be before the general requestMatcher. This filter chain works:
#Bean
SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authCustomizer -> authCustomizer
.requestMatchers(HttpMethod.POST, "/api/countries").hasRole("ADMIN")
.requestMatchers("/api/countries").permitAll()
.requestMatchers("/h2").permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.httpBasic(withDefaults());
http.headers().frameOptions().disable();
return http.build();
}
Here my security configuration:
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Bean
public ReactiveJwtDecoder reactiveJwtDecoder() throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec("JAC1O17W1F3QB9E8B4B1MT6QKYOQB36V".getBytes(), mac.getAlgorithm());
return NimbusReactiveJwtDecoder.withSecretKey(secretKey)
.macAlgorithm(MacAlgorithm.HS256)
.build();
}
#Bean
public ReactiveUserDetailsService userDetailsService(
UsuariRepository usuariRepository,
UserDetailsMapper userDetailsMapper
) {
return new GitUserDetailsService(usuariRepository, userDetailsMapper);
}
#Bean
SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http
) {
final CorsConfigurationSource configurationSource = serverWebExchange -> {
final var cc = new CorsConfiguration();
cc.addAllowedOrigin("*");
cc.addAllowedMethod("*");
cc.addAllowedHeader("*");
return cc;
};
Customizer<CorsSpec> corsCustomizer = (corsSpec) -> corsSpec.configurationSource(configurationSource);
return http
.httpBasic(HttpBasicSpec::disable)
.cors(corsCustomizer)
.csrf(CsrfSpec::disable)
.formLogin(FormLoginSpec::disable)
.anonymous(AnonymousSpec::disable)
.logout(LogoutSpec::disable)
.authorizeExchange((authorize) -> authorize
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/gicar/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
.build();
}
}
As you can see, I'm trying to use my UserDetailsService in order to load user details form database.
Into my methods, I'm using:
#RestController
#RequestMapping(value = "/qdcf")
public class QdCFController {
private final QdCFService qdcfService;
#PreAuthorize("hasRole('ADMIN')")
#GetMapping
public Mono<PageableResponseModel<QdCFPresenter>> all(Pageable pageable) {
return this.qdcfService.getQdCFs(pageable);
}
}
I'm trying to reach my code using this request:
curl -s -X GET "http://$BACKEND/qdcf" -H "Authorization: Bearer $JWT_TOKEN"
I was expecting ReactiveUserDetailsService was called. But it's ignored.
Any ideas?
Replace #EnableGlobalMethodSecurity(prePostEnabled = true) with #EnableReactiveMethodSecurity. First one is for non reactive spring security.
I am trying to invoke some backend system which is secured by a client_credentials grant type from a Feign client application.
The access token from the backend system can be retrieved with the following curl structure (just as an example):
curl --location --request POST '[SERVER URL]/oauth/grant' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: WebSessionID=172.22.72.1.1558614080219404; b8d49fdc74b7190aacd4ac9b22e85db8=2f0e4c4dbf6d4269fd3349f61c151223' \
--data-raw 'grant_type=client_credentials' \
--data-raw 'client_id=[CLIENT_ID]' \
--data-raw 'client_secret=[CLIENT_SECRET]'
{"accessToken":"V29C90D1917528E9C29795EF52EC2462D091F9DC106FAFD829D0FA537B78147E20","tokenType":"Bearer","expiresSeconds":7200}
This accessToken should then be set in a header to subsequent business calls to the backend system.
So now my question is, how to implement this using Feign and Spring Boot Security 5.
After some research I come to this solution (which doesn't work):
Define my client in the application.yml:
spring:
security:
oauth2:
client:
registration:
backend:
client-id:[CLIENT_ID]
client-secret: [CLIENT_SECRET]
authorization-grant-type: client_credentials
provider:
backend:
token-uri: [SERVER URL]/oauth/grant
Create a OAuth2AuthorizedClientManager Bean to be able to authorize (or re-authorize) an OAuth 2.0 client:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
return authorizedClientManager;
}
Create a Feign Request Interceptor that uses the OAuth2AuthorizedClientManager:
public class OAuthRequestInterceptor implements RequestInterceptor {
private OAuth2AuthorizedClientManager manager;
public OAuthRequestInterceptor(OAuth2AuthorizedClientManager manager) {
this.manager = manager;
}
#Override
public void apply(RequestTemplate requestTemplate) {
OAuth2AuthorizedClient client = this.manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("backend").principal(createPrincipal()).build());
String accessToken = client.getAccessToken().getTokenValue();
requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer" + accessToken);
}
private Authentication createPrincipal() {
return new Authentication() {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
#Override
public Object getCredentials() {
return null;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return this;
}
#Override
public boolean isAuthenticated() {
return false;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
return "backend";
}
};
}
}
Create a FeignConfig that uses the Interceptor:
public class FeignClientConfig {
#Bean
public OAuthRequestInterceptor repositoryClientOAuth2Interceptor(OAuth2AuthorizedClientManager manager) {
return new OAuthRequestInterceptor(manager);
}
}
And this is my Feign client:
#FeignClient(name = "BackendRepository", configuration = FeignClientConfig.class, url = "${BACKEND_URL}")
public interface BackendRepository {
#GetMapping(path = "/healthChecks", produces = MediaType.APPLICATION_JSON_VALUE)
public Info healthCheck();
}
When running this code, I get the error:
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [text/html;charset=utf-8]
Debugging the code it looks like the DefaultClientCredentialsTokenResponseClient is requesting the auth endpoint using Basic Authentication. Although I never set this up.
Any advise what I can do? Maybe there is a completely different approach to do this.
For this to work with Spring Security 5 and Feign you need to have
a working Spring Security config
a Feign interceptor
a Feign configuration using that interceptor
Working Spring Security Config
Here we will register a generic internal-api client for your oauth2 client credentials. This is where you specify the client-id,client-secret, scopes and grant type.
All basic Spring Security 5 stuff. This also involves setting up a provider (here I am using a custom OpenID Connect provider called "yourprovider"
spring:
security:
oauth2:
client:
registration:
internal-api:
provider: yourprovider
client-id: x
client-secret: y
scope:
- ROLE_ADMIN
authorization-grant-type: client_credentials
provider:
yourprovider:
issuer-uri: yourprovider.issuer-uri
resourceserver:
jwt:
issuer-uri: yourprovider.issuer-uri
Next you need your feign config. This will use a OAuth2FeignRequestInterceptor
public class ServiceToServiceFeignConfiguration extends AbstractFeignConfiguration {
#Bean
public OAuth2FeignRequestInterceptor requestInterceptor() {
return new OAuth2FeignRequestInterceptor(
OAuth2AuthorizeRequest.withClientRegistrationId("internal-api")
.principal(new AnonymousAuthenticationToken("feignClient", "feignClient", createAuthorityList("ROLE_ANONYMOUS")))
.build());
}
}
And a RequestInterceptor that looks like this :
The OAuth2AuthorizedClientManager is a bean that you can configure in your Configuration
public OAuth2AuthorizedClientManager authorizedClientManager(final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) {
return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
}
The OAuth2AuthorizeRequest is provided by the Feign Configuration above.
The oAuth2AuthorizedClientManager can authorize the oAuth2AuthorizeRequest, get you the access token, and provide it as an Authorization header to the underlying service
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
#Inject
private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
private OAuth2AuthorizeRequest oAuth2AuthorizeRequest;
OAuth2FeignRequestInterceptor(OAuth2AuthorizeRequest oAuth2AuthorizeRequest) {
this.oAuth2AuthorizeRequest = oAuth2AuthorizeRequest;
}
#Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION,getAuthorizationToken());
}
private String getAuthorizationToken() {
final OAuth2AccessToken accessToken = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken();
return String.format("%s %s", accessToken.getTokenType().getValue(), accessToken.getTokenValue());
}
}
I am quite experienced with Feign and OAuth2 and it took me some good hours to find how to do that.
First, let's say that my app is based on latest Spring libraries, so I am using the following dependencies (managed version for spring-cloud-starter-openfeign is 3.0.0)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
In my application.properties I have the following
security.oauth2.client.access-token-uri=https://api.twitter.com/oauth2/token
security.oauth2.client.client-id=my-secret-twitter-id
security.oauth2.client.client-secret=my-secret-twitter-secret
security.oauth2.client.grant-type=client_credentials
And finally my configuration beans
package es.spanishkangaroo.ttanalyzer.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import feign.RequestInterceptor;
#Configuration
public class FeignClientConfiguration {
#Bean
#ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(){
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
}
#Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
So then the Feign client is as simple as
package es.spanishkangaroo.ttanalyzer.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import es.clovelly.ttanalyzer.model.Trends;
#FeignClient(name = "twitterClient", url = "https://api.twitter.com/1.1/")
public interface TwitterClient {
#GetMapping("/trends/place.json")
Trends[] getTrendsById(#RequestParam Long id);
}
As you may have noticed, the code is automatically getting a token (a bearer token) before the client call. If you are using a non-expiring bearer token you can just use something like
#Bean
public OAuth2ClientContext oAuth2ClientContext() {
DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext();
context.setAccessToken(bearerToken);
return context;
}
I tried you're approach. Unfortunatelly without success. But this one worked for me: Spring cloud Feign OAuth2 request interceptor is not working. Looks like I use a lot of depredations now, but at least it does work.
when i connect to server for checking username and password like with this structure
{
"Username":"1",
"Password":"1"
}
i get 0 or 1, without any json format, first letters of username and password are Capital letters, Username,Password,
now i want to get this response with flutter and post http verb
UserInformation class
class UserInformation {
String Username;
String Password;
UserInformation(this.Username, this.Password);
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = new Map<String, dynamic>();
json['Username'] = this.Username;
json['Password'] = this.Password;
return json;
}
}
LoginRepository class:
class LoginRepository {
Future<int> authenticate(
{#required String username, #required String password}) async {
UserInformation userInformation = UserInformation(username, password);
/*this line print {Username: 1, Password: 1}*/
print(userInformation.toJson().toString());
final response = await http.post(
Constants.loginApi, body: userInformation.toJson());
final responseString = jsonDecode(response.body);
if (response.statusCode == 200) {
return responseString;
} else {
throw Exception('fail to get response');
}
}
}
and then this output is response from server:
[ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: FormatException: Unexpected character (at character 1)
<?xml version="1.0" encoding="utf-8"?>
First: Check for statusCode before accessing response.body. If the call fails, response.body will be an empty String and jsonDecode will fail.
Second: Since you don't get a json formated answer, there is no need to call jsonDecode with the response. final int responseInt = response.body should be enough (maybe you will need int.parse(response.body); needs to be tested.
I have to send a post request with xml data and then validate the response like example checking the status code and response body.
You can either pass a pojo (remember to set the content-type to application/xml) and rest assured will automatically transform it into XML. For example:
#XmlRootElement
public class Greeting {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
and then you can use it like this:
Greeting greeting = new Greeting();
greeting.setFirstName("John");
greeting.setLastName("Doe");
given().contentType(ContentType.XML).and().body(object).when().post("/somewhere"). ..
it'll send the following XML:
<greeting>
<firstName>John</firstName>
<lastName>Doe</lastName>
</greeting>
Let's say that the server is responding with the same XML as the one you see above then you can validate the response status code and body like this:
given().
contentType(ContentType.XML).
body(object).
when().
post("/somewhere").
then().
statusCode(200).
body("greeting.firstName", equalTo("John")).
body("greeting.lastName", equalTo("Doe"));
REST Assured will automatically understand the the response body is XML if the server returns an XML content-type. Note that equalTo is statically imported from org.hamcrest.Matchers#equalTo.