I am using spring-boot-1.5.10 and I am using spring-security in my application. I would like to create a custom annotation and which should using securityContext holder...Let me elaborate my question with sample code.
curl -X GET -H "roles: READ" -H "Content-Type: application/json" -H
"Accept: application/json" -H "app-name: sample" -H "app-id: sample"
-H "customer-id: 123" -H "market: EN" -H "country-code: EN" -H "Accept-Language: application/json" -H "Cache-Control: no-cache"
"http://localhost:9992/api/v1/apps"
Controller
#GetMapping("/apps")
#PreAuthorize("hasAnyAuthority('ROLE_READ', 'ROLE_WRITE')")
public ResponseEntity<List<Apps>> getApps(#AuthenticationPrincipal AppAuthentication appAuthentication) {
if(appAuthentication.isGrantedAnyOf("ROLE_READ") && isBlank(appAuthentication.getAppContext().customerId())) {
throw new IllegalArgumentException("Missing header customerId");
}
if(appAuthentication.isGrantedAnyOf("ROLE_WRITE") && isBlank(appAuthentication.getAppContext().customerId()) && isBlank(appAuthentication.getAppContext().appId())) {
throw new IllegalArgumentException("Missing header customerId & AppId");
}
//write business logic here
}
spring-security preAuthorize will only check if the roles are allowed.Also, I can enhance the preAuthorize annotation but it's common for many microservice and also i don't have permission to touch security realm. so, I would like to create a custom annotation. we should configure the roles & headers to validate for the particular roles. Like below
#GetMapping("/apps")
#PreAuthorize("hasAnyAuthority('ROLE_READ', 'ROLE_WRITE')")
#ValidateHeaders("role=ROLE_READ",value={"customerId","app-id"})
public ResponseEntity<List<Apps>> getApps(#AuthenticationPrincipal AppAuthentication appAuthentication) {
//write business logic here
}
Any hint would be really appreciable.
Disclamer - I was working with spring boot 2, so not everything might be applicable for you
This is a stripped down version of something I've implemented a while back.
I suggest implementing enums for roles and values.
You can't do redirects from #before anotations so you have to throw an exception and catch it with a global ex handler, redirect from there.
Consider also adding to the annotation optional fields - in case of multiple roles, match all or one,
redirect path, exception type to invoke in case of no access. Then in the exception handler you can do redirects depending on which exception was invoked. Since you are doing redirects from gloabl ex handler, if you add a redirect path, you'll have to bundle it with the exception you are throwing, meaning you'll need custom exceptions.
Annotation
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidateHeaders {
Roles[] roles();
String[] values();
}
Aspect Class
#Aspect
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired)) //Autowired annotated lombok generated constructor
public class ValidateHeadersAspect {
private final #NonNull HttpServletRequest request; //Inject request to have header access
private final #NonNull UserService userService;//Your user service here
//Aspect can be placed on clas or method
#Before("within(#com.org.package.ValidateHeaders *) || #annotation(com.org.package.ValidateHeaders)")
public void validateAspect(JoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
HasAccess validateHeaders = method.getAnnotation(ValidateHeaders.class);
if(validateHeaders == null) { //If null it was a class level annotation
Class annotatedClass = joinPoint.getSignature().getDeclaringType();
validateHeaders = (ValidateHeaders)annotatedClass.getAnnotation(ValidateHeaders.class);
}
Roles[] roles = validateHeaders.roles(); //Roles listed in annotation
String[] values = validateHeaders.values(); //Values listed in
//Validate request here ... determine isAuthorised
if( !isAuthorized ){
throw new HeaderAuthrizationException()
}
}
}
Exception Handler
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(HeaderAuthrizationException.class)
public RedirectView HeaderAuthrizationException(HeaderAuthrizationException ex) {
return new RedirectView("/redirect");
}
}
Related
I'm new to springboot and am working on a legacy project. I'v searched but didn't find answer.
The current working project uses #Valid #RequestBody to validate body, and #ExceptionHandler to catch the exception.
Now the new requirement is dealing with request headers, and I will use the headers(to log for example) regardless of body is valid or not.
The question is I don't konw how to get the headers when #ExceptionHandler hits. Similiar question is How to get the #RequestBody in an #ExceptionHandler (Spring REST) but I cannot figure out how to get headers from injected RequestContext(I even cannot resolve getRequestBody() method in the answer).
The minimal, working and reproducible code is pushed to github.
The Model:
public class Book {
#NotBlank(message = "Title is mandatory")
public String Title;
}
the Controller
#RestController
public class BooksController {
final Log log = LogFactory.getLog(getClass());
#PostMapping("/")
public String add(#RequestHeader("id") String value, #Valid #RequestBody final Book book) {
log.info(value); // want this value even when hit #ExceptionHandler
return "added " + book.Title;
}
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public String handleInvalidRequest(final MethodArgumentNotValidException e) {
//log headers; ??
return e.getMessage();
}
}
the Client
curl -H "id:123" -H "Content-Type: application/json" http://localhost:8080 --data '{"Title":""}'
Additonal Information
jdk 11, springboot 2.5.3, intellij idea 2021.
model's code is in a library that i cannot change, so the validation logic is unknown to me.
I guess there should be a lot of ways to solve, such as defining some customized middlewares or handles, but I'm not familiar with those and I want to see a solution with least code changes. Thanks!
Inside #ExceptionalHandler you can accept HttpServletRequest to get headers
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public String handleInvalidRequest(final MethodArgumentNotValidException e, final HttpServletRequest req) {
//log headers; ??
req.getHeader("My-Header");
return e.getMessage();
}
I am trying to process a POST request with body of plain text (utf-8) but it seems that spring does not like the plain text nature of the call. Could it be that it is not supported - or otherwise, am I coding it wrong?
#RestController
#RequestMapping(path = "/abc", method = RequestMethod.POST)
public class NlpController {
#PostMapping(path= "/def", consumes = "text/plain; charset: utf-8", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> doSomething(#RequestBody String bodyText)
{
...
return ResponseEntity.ok().body(responseObject);
}
}
Respond is:
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded' not supported]
I tested with curl command:
curl -s -X POST -H 'Content-Type: text/plain; charset: utf-8' --data-binary #text.txt localhost:8080/abc/def
The text.txt contains plain text (UTF-8 in Hebrew).
I would like to throw some light on the other part of the question of whether spring supports text/plain?
According to spring docs: what is "consumes" or Consumable Media Types in the #RequestMapping annotation
Definition :
Consumable Media Types
"You can narrow the primary mapping by specifying a list of consumable media types. The request will be matched only if the Content-Type request header matches the specified media type. For example:"
#Controller
#RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(#RequestBody Pet pet, Model model) {
// implementation omitted
}
Consumable media type expressions can also be negated as in !text/plain to match to all requests other than those with Content-Type of text/plain.
How spring does the media type matching internally?
Spring Request Driven Design is centred around a servlet called the dispatcher Servlet which uses special beans to process requests one of the bean is RequestMappingHandlerMapping which checks for the media type using
getMatchingCondition method of ConsumesRequestCondition class as shown below.
#Override
public ConsumesRequestCondition getMatchingCondition(ServerWebExchange exchange) {
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return PRE_FLIGHT_MATCH;
}
if (isEmpty()) {
return this;
}
Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<>(expressions);
result.removeIf(expression -> !expression.match(exchange));
return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null);
}
the get matching condition class uses static inner class ConsumesMediaType Expression which actually makes the check
#Override
protected boolean matchMediaType(ServerWebExchange exchange) throws UnsupportedMediaTypeStatusException {
try {
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
return getMediaType().includes(contentType);
}
catch (InvalidMediaTypeException ex) {
throw new UnsupportedMediaTypeStatusException("Can't parse Content-Type [" +
exchange.getRequest().getHeaders().getFirst("Content-Type") +
"]: " + ex.getMessage());
}}
This method returns false once the media type does not match and getMatchingCondition returns null which results in handleNoMatch method of RequestMappingInfoHandlerMapping being called and using PartialMatchHelper class we check for the what type of mismatch it has as shown below and spring throws HttpMediaTypeNotSupportedException error once its see consumes mismatch
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
Spring supports all media types as per the IANA https://www.iana.org/assignments/media-types/media-types.xhtml the problem lies only with the curl command as quoted by others.
#rumbz
Please refer to the below link it might solve your issue
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported for #RequestBody MultiValueMap
1 Using annotation
#RequestMapping(value = "/some-path", produces =
org.springframework.http.MediaType.TEXT_PLAIN)
public String plainTextAnnotation() {
return "<response body>";
}
where replace /some-path with whatever you'd like to use.
2 Setting content type in the response entity's HTTP headers:
public String plainTextResponseEntity() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(org.springframework.http.MediaType.TEXT_PLAIN);
return new ResponseEntity("<response body>", httpHeaders, HttpStatus.OK);
}
Per #m-deinum 's comment: The problem is not in the spring framework - but in the fact that curl adds "application/x-www-form-urlencoded" to the request ...
And just to make this question complete:
curl -s -X POST -H "Content-Type:" -H "Content-Type: text/plain; charset: utf-8" --data-binary #file.txt localhost:8080/abc/def
Am trying to use Spring Secruity's OAuth API to obtain an access token from an externally published API within a Spring MVC 4 based Web Services (not Spring Boot).
This curl command works (and its contents are all that I need to obtain an access token):
curl -X POST \
https://api.app.com/v1/oauth/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d'grant_type=client_credentials&client_id=bcfrtew123&client_secret=Y67493012'
Spring Security OAuth API:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
My code to obtain access token:
#RequestMapping(value = "/getAccessToken", method = RequestMethod.POST, consumes="application/x-www-form-urlencoded")
public OAuth2AccessToken getAccessToken(#RequestParam(value="client_id", required=true) String clientId, #RequestParam(value="client_secret", required=true) String clientSecret) throws Exception {
String tokenUri = "https://api.app.com/v1/oauth/token";
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(tokenUri);
resourceDetails.setClientId(clientId);
resourceDetails.setClientSecret(clientSecret);
resourceDetails.setGrantType("client_credentials");
resourceDetails.setScope(Arrays.asList("read", "write"));
DefaultOAuth2ClientContext clientContext = new DefaultOAuth2ClientContext();
oauth2RestTemplate = new OAuth2RestTemplate(resourceDetails, clientContext);
OAuth2AccessToken token = oauth2RestTemplate.getAccessToken();
return token;
}
When I invoke the getAccessToken call from my local tomcat instance:
access_denied
error_description=Unable to obtain a new access token for resource 'null'.
The provider manager is not configured to support it.
Am suspecting the reason is that my Http Header's Content-Type is not set for
application/x-www-form-urlencoded
How do I do set that for:
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
If you notice, I am trying to set in inside the #RequestMapping and don't think that its working:
#RequestMapping(consumes="application/x-www-form-urlencoded")
The http headers for accessing the token in Oauth2Restemplate in case of Client credentials are set in below method of ClientCredentialsAccessTokenProvider (since grant type is client credentials)
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
return retrieveToken(request, resource, getParametersForTokenRequest(resource), new HttpHeaders());
}
We can set the http headers by having new custom Access token provider for client credentials and modifying the method as follows:
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
HttpHeaders headers1 = new HttpHeaders();
headers1.add("Content-Type", "application/x-www-form-urlencoded");
return retrieveToken(request, resource, getParametersForTokenRequest(resource), headers1);
}
You can keep the class same as ClientCredentialsAccessTokenProvider and add just the header lines.
Last step will be to set this new class as access token in configuration of Oauth2RestTemplate.
oauth2RestTemplate.setAccessTokenProvider(new ClientCredentialsCustomAccessTokenProvider());
This worked for me!
Here's another variation on the answer just to override the default Accept Header interceptor using a Lambda expression:
#Bean
protected RestTemplate restTemplate() {
return new RestTemplate() {
#Override
public <T> RequestCallback acceptHeaderRequestCallback(Class<T> responseType) {
return request -> {
request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
};
}
};
}
If you are using Spring boot mention the authentication scheme as form, it will solve the issue.
security:
oauth2:
client:
clientAuthenticationScheme: form
I've created a self hosted Java applicataion and I would like to use Google sign in to log in into. I followed the follwong example:
https://developers.google.com/identity/sign-in/web/
That of course work, but now I'm getting a little confuse on how I can authorize the calls on the server. In the backend I'm using Grizzly+Jersey.
As described on the Google Sig-In documentation, you can use Google API Client Library for Java in order to check the authentication token on server side.
Client side
After a user successfully signs in, get the user's ID token:
function onSignIn(googleUser) {
var idToken = googleUser.getAuthResponse().id_token;
...
}
And send the idToken to the server in every request using the standard HTTP Authorization header.
Server side
You can use a filter to perform authentication and/or authorization.
To bind filters to your REST endpoints, JAX-RS provides the meta-annotation #NameBinding and can be used as following:
#NameBinding
#Retention(RUNTIME)
#Target({TYPE, METHOD})
public #interface Secured { }
The #Secured annotation will be used to decorate a filter class, which implements ContainerRequestFilter, allowing you to handle the request, get and validate the token.
The ContainerRequestContext helps you to extract information from the HTTP request.
The #Provider annotation marks an implementation of an extension interface that should be discoverable by JAX-RS/Jersey runtime during a provider scanning phase.
#Secured
#Provider
#Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the token header from the HTTP Authorization request header
String token =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Check if the token is present
if (token == null || token.isEmpty()) {
throw new NotAuthorizedException("Token must be provided");
}
// Validate the token
validateToken(token);
}
private void validateToken(String token) {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier
.Builder(new NetHttpTransport(), new GsonFactory())
.setAudience(Arrays.asList(CLIENT_ID))
.build();
GoogleIdToken idToken = verifier.verify(token);
if (idToken != null) {
Payload payload = idToken.getPayload();
System.out.println("User ID: " + payload.getSubject());
} else {
throw new NotAuthorizedException("Invalid token.");
}
}
}
To bind the filter to your endpoints methods or classes, annotate them with the #Secured annotation created above. For the methods and/or classes which are annotated, the filter will be executed.
#Path("/example")
public class MyEndpoint {
#GET
#Path("{id}")
#Produces("application/json")
public Response myUnsecuredMethod(#PathParam("id") Long id) {
// This method is not annotated with #Secured
// The security filter won't be executed before invoking this method
...
}
#DELETE
#Secured
#Path("{id}")
#Produces("application/json")
public Response mySecuredMethod(#PathParam("id") Long id) {
// This method is annotated with #Secured
// The security filter will be executed before invoking this method
...
}
}
In the example above, the security filter will be executed only for mySecuredMethod(Long) because it's annotated with #Secured.
I am using JBoss AS and JAX-RS for creating REST endpoints.
Lets say my class looks like
#Path("/users")
public class UserResource {
#GET
public Response getAccount() {
return "hello";
}
}
Now getAccount is not authenticated at the moment
Wanted
- I would like to add authentication so that when code hits getAccount the user is authenticated
- I would like the authentication to be driven by annotations instead of XML configurations, if at all possible
- I would like to do the database comparison to see if the user is valid
Problem
- I have never done that so I have no idea how to implement it
- I have googled around a lot and found Jersey examples
UPDATE
- I would like to send authentication credentials with each request and not creating any session
Please guide me with one simple working example and I would try to extend from there
You need is a Stateless Spring Security configuration in front of your JAX RS end points.
I have addressed exact problem you are trying to solve but I don't have my own code to share..
Here is one project which has done the exact thing you are asking, Some wise man has done it all for you ;)
https://github.com/philipsorst/angular-rest-springsecurity
What is the magic ?
You have one unprotected URL which does the Authentication, and set the user roles as well..
Then you return some kind of Token, put it some where in cache which will be expected on every subsequent call..
Upon new request on other protected resources, you will check if the Token is present in your cache/session store ( you need some mechanism to keep track of valid tokens )
If token is resent and valid, you do the programmatic Log-in in Spring Security which ensures that you can use all the Security features spring provides, ( Annotations, JSTL Tags etc.. ) !
Once passed token validation you will get the logged in user details in your controllers ( aka JAX RS resources ) to deal with security further..
If the token was not valid or not present , it would be trapped by failure end point which would return appropriate response ( 401 )
Refer Following Link To Understand How Stateless Spring Security is configured..,
https://github.com/philipsorst/angular-rest-springsecurity/blob/master/src/main/resources/context.xml
See how a user is validated for the first time and a token is generated..
https://github.com/philipsorst/angular-rest-springsecurity/blob/master/src/main/java/net/dontdrinkandroot/example/angularrestspringsecurity/rest/resources/UserResource.java
Here is the class where programmatic login is performed on every request after token
check..
https://github.com/philipsorst/angular-rest-springsecurity/blob/master/src/main/java/net/dontdrinkandroot/example/angularrestspringsecurity/rest/AuthenticationTokenProcessingFilter.java
I solved this with following code.
note Token mechanism will be updated once I do that
I have solved this by modifying the interceptor I have, the following is code
Annotation
#Inherited
#InterceptorBinding
#Target({ ElementType.TYPE, ElementType.METHOD })
#Retention(RetentionPolicy.RUNTIME)
public #interface SecurityChecked {
}
Resource Class
public class SecureResource {
#GET
#SecurityChecked
public Response getUser() {
return Response.ok("authenticated successfully!").build();
}
}
Interceptor class
#Interceptor
#Provider
#ServerInterceptor
#SecurityChecked
public class SecurityCheckInterceptor implements PreProcessInterceptor, AcceptedByMethod {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityCheckInterceptor.class);
#Nullable
#Override
public ServerResponse preProcess(final HttpRequest request, final ResourceMethod method) throws Failure, WebApplicationException {
final List<String> authToken = request.getHttpHeaders().getRequestHeader("X-AUTH");
if (authToken == null || !isValidToken(authToken.get(0))) {
final ServerResponse serverResponse = new ServerResponse();
serverResponse.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
return serverResponse;
}
return null;
}
private static boolean isValidToken(#Nonnull final String authToken) {
LOGGER.info("validating token: " + authToken);
return true;
}
#SuppressWarnings("rawtypes")
#Override
public boolean accept(final Class declaring, final Method method) {
// return declaring.isAnnotationPresent(SecurityChecked.class); // if annotation on class
return method.isAnnotationPresent(SecurityChecked.class);
}
}
and then I run my Integration tests by deploying the resource class in JBoss and issuing following commands on command-line
curl --header 'X-AUTH: 1a629d035831feadOOO4uFReLyEW8aTmrCS' http://localhost:8080/market-1.0-SNAPSHOT/rest/login
curl --header 'InvalidHeader: InvalidHeaderValue' http://localhost:8080/market-1.0-SNAPSHOT/rest/login