TLDR
There is a need in the Java Web Application I develop, to implement user distinction over the REST services. I know that there are some annotations (#RolesAllowed, #PermitAll, #DenyAll), that can describe which role can use the service. My question is, how can I match the roles from #RolesAllowed with user role enumaration, that user is a persisted object stored in a DB??
Many tutorials explain the #RolesAllowed, but I found none to match those roles, with already created ones
Explanation
How to validate a user, by checking his role (found by session ID) automatically? I know Jersey do this already, by registering RolesAllowedDynamicFeature.class. I already managed to check that this RolesAllowedDynamicFeature.class works, by putting the #DenyAll annotation into a method, and returned a 403 error.
First of all, let's start with what is a user. Users are DB Entities, using Ebean to persist them from and to Java objects. Here is a sample from a User.class:
/*
* A sample that describes the fields that I ask for and to understand the concept
*/
#Entity
#Table(name = "users")
public class User extends Model
{
#Id
#GeneratedValue
#Column(name = "id")
private long id;
#Enumerated(EnumType.STRING)
#Column(name = "role", nullable = false)
private UserRole role;
// contructors getters setters helper methods etc
/**
* Fetch a user from DB
*
* #param id the id to search for
* #return a Person.class object or may return null
*/
public static User getUserById(Long id)
{
return Ebean.find(User.class, id);
}
/* and here is the UserRole enum that define the roles every user can have */
public static enum UserRole
{
Administrator, User, Manager;
}
}
All above code, works fine, user are stored correctly and I can fetch them easily.
Every User, at login, is authenticated with a similar service and a ConnectedUser object is created with a unique session ID (using UUID) for each one.
After each service call, there is an authentication running, that Authenticates if a user can use this service by searching, if there is a connected user entry for this session (which is stored as a cookie):
#Secured
#Provider
#Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter
{
#Context
private HttpServletRequest request;
/**
* Authenticates a user's access with every request that is made via a token.
*
* #param requestContext The request that is sent to the server.
* #throws IOException
*/
#Override
public void filter(ContainerRequestContext requestContext) throws IOException
{
boolean isValidated;
Cookie sessionCookie = null;
Cookie[] cookies = request.getCookies();
if (cookies.length != 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("CookieName")) {
sessionCookie = cookie;
}
}
}
if (sessionCookie != null) {
// UserValidationHandler checks if user is in connected_users table
isValidated = UserValidationHandler.validateUser(sessionCookie.getValue(), request.getRemoteAddr());
}
else {
MultivaluedMap pathParameters = requestContext.getUriInfo().getQueryParameters();
// UserValidationHandler checks if user is in connected_users table
isValidated = UserValidationHandler.validateUser((String)pathParameters.getFirst("token"), request.getRemoteAddr());
}
if (!isValidated) {
LOGGER.warn("[Authorization filter] Unauthorized user.");
URI indexURI = URI.create("http://login.jsp");
requestContext.setRequestUri(indexURI);
}
}
}
Notes:
Note1: Most implementations suggest to apply the roles into web.xml file. This is not doable though I think, for my case.
Note2: Also, where is the right place to authorize a user to use a service? I found that I can create ContainerRequestFilter class, with #Priority(Priorities.AUTHORIZATION). Which is better to do?
I'm kinda lost here. I read many of the Q/As or samples out there, but nothing explain it thorough.
You should separate the concerns and do the authentication in one filter and authorization in one filter. Generally how this is accomplished is by setting the SecurityContext from inside the authentication filter, and then retrieving it from inside the authorization filter.
The SecurityContext has a isUserInRole method that you override. This method should be called in the authorization filter. Generally you will have the roles as a member of the SecurityContext, and just iterate the roles
static class MySecurityContext implements SecurityContext {
private final String[] userRoles;
public MySecurityContext(String[] roles, String user) {
this.userRoles = roles;
}
#Override
public Principal getUser() {
return new Principal() {
#Override
public String getName() {
return name;
}
}
}
public boolean isUserInRole(String role) {
for (String userRole: userRoles) {
if (role.equals(userRole) {
return true;
}
}
return false;
}
// more methods to override
}
In the authentication filter, you just call the requestContext.setSecurityContext method passing in the new SecurityContext.
In the authorization filter, you would get the #RolesAllowed annotation by using the ResourceInfo. For example
class AuthorizationFilter implement ContainerRequestContext {
#Context
private ResourceInfo info;
#Override
public void filter(ContainerRequestContext request) {
SecurityContext sc = request.getSecurityContext();
RolesAllowed anno = info.getResourceMethod().getAnnotation(RolesAllowed.class);
String rolesAllowed = anno.value();
for (role: rolesAllowed) {
if (sc.isUserInRole(role)) {
return;
}
}
request.abortWith(Response.status(403).build());
}
}
Or something along those lines.
If you are using Jersey2 though, you don't need to implement the authorization yourself. This is already implemented in the RolesAllowedDynamicFeature. You just need to register the feature with your application. It works the same as I mentioned previously; A SecurityContext is expected to be filled prior to reaching the authorization filter, the the filter will check the roles and either authorize or reject.
My implementation is different from that of peeskillet but it has worked for me.
The code implementing security might look something like this
public class ServiceFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext req) throws IOException {
//user name and password are obtained from the header
String auth = req.getHeaderString(HttpHeaders.AUTHORIZATION);
if(auth == null) {
throw new WebApplicationException(Status.UNAUTHORIZED);
}
//user name and password
String[] credentials = auth.substring(1, auth.length()-1).split(":");
String user = credentials[0];//user name
String password = credentials[1];//password
if(user == null || password == null)
throw new WebApplicationException(Status.UNAUTHORIZED);
ServiceSecurity ss = null;
//user name and password are hardcoded here but you better put them in a DB or file
if(user.equals("servUser") && password.equals("service"))
ss = new ServiceSecurity(new ServiceUser("servUser"));
else if(user.equals("servAdmin") && password.equals("admin"))
ss = new ServiceSecurity(new ServiceUser("servAdmin"));
else
throw new WebApplicationException(Status.UNAUTHORIZED);
req.setSecurityContext(ss);
}
}
The security context
import java.security.Principal;
import javax.ws.rs.core.SecurityContext;
public class ServiceSecurity implements SecurityContext {
private ServiceUser sUser;
public ServiceSecurity(ServiceUser sUser){
this.sUser = sUser;
}
#Override
public String getAuthenticationScheme() {
// TODO Auto-generated method stub
return SecurityContext.DIGEST_AUTH;
}
#Override
public Principal getUserPrincipal() {
// TODO Auto-generated method stub
return sUser;
}
#Override
public boolean isSecure() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isUserInRole(String role) {
// TODO Auto-generated method stub
return sUser.getRole().equals(role) ? true : false;
}
}
The security context implementation class is initialized with a class implementing Principal (ServiceUser), which is used to get the role for this particular user.
import java.security.Principal;
public class ServiceUser implements Principal {
private String role;
public ServiceUser(String role){
this.role = role;
}
public ServiceUser(){
}
public String getRole(){
return role;
}
public void setRole(String role){
this.role = role;
}
public String getName(){
return "some name";
}
}
In this case, you'd add the following to your server method
#RolesAllowed("servUser")
The credentials (user name and password) should be supplied in the request header (as is evident from the filter method above)
Related
I'm running a quarkus setup with keycloak. To start of I am trying to implement the tutorial into my code. I have to note here that i didn't copy the realm that is used. The reason for this is that we received a realm from our tutors.
This is the code I am trying to implement. The problem with is that it doesn't return the user.
#Path("/api/users")
public class UserResource {
#Inject
SecurityIdentity identity;
#GET
#Path("/me")
#NoCache
public User me() {
return new User(identity);
}
public static class User {
private final String userName;
User(SecurityIdentity identity) {
this.userName = identity.getPrincipal().getName();
}
public String getUserName() {
return userName;
}
}
}
When I enabled policy enforcer, I wasn't even able to login, all request became 403 or 401.
quarkus.keycloak.policy-enforcer.enable=true
How can we solve this issue?
Another question would be if it is possible to retrieve some sort of userId from keycloak?
I'm trying to get UserDetails object like below. But, I have some difficulties and impossible to get UserDetails object, so there is only JSONObject in authentication.getAttributes(). Is there any alternative way in micronaut to get UserDetails object?
Custom UserDetails object:
public class MyUserPrincipal implements UserDetails {
private Account account;
public MyUserPrincipal(Account account) {
this.account = account;
}
public Account getAccount() {
return getAccount();
}
}
Rest api:
//micronaut
#Post(value = "/echo")
#Status(HttpStatus.OK)
public Long echo(#Nullable Authentication authentication) {
Long accountId = (Long)((JSONObject)authentication.getAttributes().get("account")).get("id");
return accountId;
}
For example in Spring Security it is easy with #AuthenticationPrincipal annotation in parameter.
Rest api:
#GET
public ResponseEntity<?> echo(#AuthenticationPrincipal MyUserPrincipal user) {
return new ResponseEntity<>(user.getAccount().getAccountId(), HttpStatus.OK);
}
If you are still looking for a solution, here is what works.
You have to provide an implementation of JwtAuthenticationFactory and replace default DefaultJwtAuthenticationFactory.
Something like this (code below is in Kotlin):
#Singleton
#Replaces(bean = DefaultJwtAuthenticationFactory::class)
class CustomJwtAuthenticationFactory() : JwtAuthenticationFactory {
override fun createAuthentication(token: JWT?): Optional<Authentication> {
try {
val builder = JWTClaimsSet.Builder()
builder.claim("username", token?.jwtClaimsSet?.getStringClaim("username"))
return Optional.of(AuthenticationJWTClaimsSetAdapter(jwtClaims))
} catch (e: Exception) {
throw RuntimeException("ParseException creating authentication", e)
}
}
}
All claims added using the builder will get added in the Authentication object and can be accessed in any controller eg:
#Get("/hello-world")
fun hello(authentication: Authentication): String =
authentication["username"] as String
If you are using Kotlin, use could also add extension methods on Authentication method to fetch attributes that you add to Authentication class eg:
fun Authentication.username(): String = this.attributes["username"]
Note: username is just an example. It is available as name instance variable on instance of Authentication.
UserDetails does not exist after authentication. The only object available is the Authentication. If you want to standardize the code you have above you could create a bean that handles injection of that specific property.
You could use an annotation to designate the injection by creating an annotation along with an implementation of AnnotatedRequestArgumentBinder. Something like the following:
public class Temp implements AnnotatedRequestArgumentBinder<YourAnnotation, Long> {
#Override
public Class<YourAnnotation> getAnnotationType() {
return YourAnnotation.class;
}
#Override
public BindingResult<Long> bind(ArgumentConversionContext<Long> context, HttpRequest<?> source) {
if (source.getAttributes().contains(OncePerRequestHttpServerFilter.getKey(SecurityFilter.class))) {
final Optional<Authentication> authentication = source.getUserPrincipal(Authentication.class);
if (authentication.isPresent()) {
return () -> (Long)((JSONObject)authentication.getAttributes().get("account")).get("id");
}
}
return ArgumentBinder.BindingResult.EMPTY;
}
}
I`m creating my first Spring blog and trying to set logged user full name in navbar. Unfortunately sec:authentication="name" gives me user email and ${user.fullname) does not render anything. I figured out to put following code inside in an Article controller
#GetMapping("/")
public String index(Model model) {
List<Article> articles = this.articleRepository.findAll();
Object currentUser = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if ("anonymousUser".equals(currentUser.toString())) {
model.addAttribute("username","GUEST");
} else {
UserDetails user = (UserDetails) currentUser;
String name = this.userRepository.findByEmail(user.getUsername()).getFullName();
model.addAttribute("username",name);
}
model.addAttribute("view", "home/index");
model.addAttribute("articles", articles);
return "base-layout";
}
And it worked. However i want to use it everywhere and now I can use it only in "/". Please advice how can I modify the code, so I`ll be able to use username in all templates.
You should create a class annotated with #ControllerAdvice which indicates the annotated class assists your Controller. Then create a method #ModelAttribute which will set the common attributes, shared by all (or most) of your controllers.
For example:
#ControllerAdvice
public class UserControllerAdvice {
#ModelAttribute
public void addUserAttribute() {
Object currentUser = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if ("anonymousUser".equals(currentUser.toString())) {
model.addAttribute("username","GUEST");
} else {
UserDetails user = (UserDetails) currentUser;
String name = this.userRepository.findByEmail(user.getUsername()).getFullName();
model.addAttribute("username",name);
}
}
I've just created my own custom authentication on my google app engine Java app. And it wasn't that much of a trouble as is the next thing I'm trying to do.
Authentication works fine but now I'm trying to add some additional fields to the default User object so that I wouldn't have to make so many calls to the server.
So what I've done so far is created a custom class that implements Authenticator. Based on whether the user is authenticated or not the authenticate method returns the User object or null. User object is then accessible to my API endpoints.
To extend my app functionality I've tried extending the default User object, making some new fields, and then passing it to endpoints. However, since the User object accessible by endpoints is not the same kind as the one I extended from I can't get the extra fields.
MyAuthenticator.java
import com.google.api.server.spi.auth.common.User;
public class MyAuthenticator implements Authenticator {
#Override
public User authenticate(HttpServletRequest request) {
// some code
return new AuthUser(...)
}
AuthUser.java
import com.google.api.server.spi.auth.common.User;
public class AuthUser extends User {
private String newToken;
public AuthUser(String email) {
super(email);
}
public AuthUser(String id, String email) {
super(id, email);
}
public AuthUser(String id, String email, String newToken) {
super(id, email);
this.newToken = newToken;
}
public String getNewToken() {
return newToken;
}
}
UserEndpoint.java
import com.google.appengine.api.users.User;
#Api(authenticators = MyAuthenticator.class)
public class UserEndpoint {
#ApiMethod(httpMethod = "GET")
public final Response sth(User user)
throws UnauthorizedException {
EndpointUtil.throwIfNotAuthenticated(user);
// ...
}
Notice different class imports.
I can't use AuthUser in UserEndpoint sth method because then API expects me to post that object with my call to server.
How can I pass extra data from authenticator to my endpoint method?
AppEngine docs say the injected types are the following:
com.google.appengine.api.users.User
javax.servlet.http.HttpServletRequest
javax.servlet.ServletContext
However, it doesn't mention com.google.api.server.spi.auth.common.User, but it works for sure. I just tried with AppEngine Java SDK 1.9.32. I don't know if it's a bug or feature.
So in UserEndpoint.java, you have to import com.google.api.server.spi.auth.common.User, then you can cast it to AuthUser.
import com.google.api.server.spi.auth.common.User;
#Api(authenticators = MyAuthenticator.class)
public class UserEndpoint {
#ApiMethod(httpMethod = "GET")
public final Response sth(User user)
throws UnauthorizedException {
EndpointUtil.throwIfNotAuthenticated(user);
((AuthUser)user).getNewToken();
// ...
}
I have a model for logging in user in my REST API, corresponds to User table (email and password as table columns)
#Entity
public class User {
#Id
#GeneratedValues
private Long id;
private String email;
private String password;
+GET , +SET
}
Then there is #Controller which is making call to above User Entity using JPAService
#Controller
#RequestMapping("/rest/auths")
public class AuthController {
#Autowired
private UserService authService;
#RequestMapping(value = "/login", method = RequestMethod.POST)
public #ResponseBody ResponseEntity<AuthLoginFormResource> login(#RequestBody AuthLoginFormResource sentAuth) {
User user = authService.login(sentAuth.toUser());
AuthLoginFormResource res = new AuthLoginFormResourceAsm().toResource(user);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(res.getLink("self").getHref()));
return new ResponseEntity<AuthLoginFormResource>(res, HttpStatus.OK);
}
}
AuthLoginFormResource : -
public class AuthLoginFormResource extends ResourceSupport {
private String email;
private String password;
private boolean success;
public User toUser() {
User user = new User();
user.setEmail(email);
user.setPassword(password);
//user.setSuccess(false);
return user;
}
+GET, +SET
}
AuthLoginFormResourceAsm : -
public class AuthLoginFormResourceAsm extends ResourceAssemblerSupport<User, AuthLoginFormResource> {
public AuthLoginFormResourceAsm() {
super(User.class, AuthLoginFormResource.class);
}
#Override
public AuthLoginFormResource toResource(User user) {
AuthLoginFormResource res = new AuthLoginFormResource();
res.setEmail(user.getEmail());
res.setPassword(user.getPassword());
//res.setSuccess(user.isSuccess()); // Success is not existing in USER
res.add(linkTo(AuthController.class).withSelfRel());
return res;
}
}
There are 2 issues -
I need to send a success flag as boolean in response for which i have added a boolean success to AuthLoginFormResource. But, AuthLoginFormResource gets set
only from AuthLoginFormResourceAsm.toResource method , which in turn does
it from entity User. As User entity models database where there is
no success column, I am not able to set success at this place.
So, should I add dummy success field to User Entity and set that from service
method , though there is no such field in database or create a new Entity representing Login Form here and return that ?
Same problem with another field that is a token for authentication
which does not exist in database but is part of response.
What is correct place for setting such fields in ResourceSupport object - inside database Entity and return from Service / creating another Form Model entity on top of Domain Model and return from service.
This is basic question I am facing in many places where data model and forms don't match one to one.
I strongly recommend the following;
Modify UserService.login method to return true or false based on successfull authentication instead of retrieved user object from database.
Return only true or false with status OK and FAIL, as part of the response not the entire AuthLoginFormResource. This is a bad practice because you are sending out the username and password as part of the request and response, back and forth in a roundtrip. If someone is evesdropping they can easily figure out what username passwords work and what don't.
Or
Consider using Basic Authorization, Digest Authorization or OAuth if you fancy than this custom Authentication Implementation. Using Spring Security you can achieve any of the aforementioned really easily.