The main data information and information in my API is linked to a project(Entity),
What is good approach for password flow: to manage specific permission linked to a project with spring security and OAuth2?
In this application you have 5 micro service:
UAA microservice : the authorization server
Catalog microservice
Order microservice
Invoice microservice
Customer microservice
Zoom permission :
Each user can have many project, and can have the permission for each project:
CAN_MANAGE_CATALOG
CAN_VIEW_CATALOG
CAN_MANAGE_ORDER
CAN_VIEW_ORDER
CAN_MANAGE_INVOICE
CAN_VIEW_INVOICE
...
I have many idea but i am not sure if i have the good approach :
USE CASE : I want to securise the endpoint :
http://catalog-service/{project_key}/catalogs
Only USER who have permission VIEW_CATALOG OR MANAGE_CATALOG for the project {project_key} can list all catalog present in the project
My first idea : USE ProjectAccessExpression with preauthorize
CatalogController.java
#Controller
public class CatalogController {
#PreAuthorize("#projectAccessExpression.hasPermission(#projectKey, 'manageCatalog', principal)" +
" or #projectAccessExpression.hasPermission(#projectKey, 'viewCatalog', principal)")
#RequestMapping(
value = "/{projectKey}/catalogs",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE
)
public #ResponseBody List<Catalog> findByProject(#PathVariable("projectKey") String projectKey) {
return catalogService.find();
}
}
ProjectAccessExpression.java
#Component
public class ProjectAccessExpression {
private RestTemplate restTemplate;
public boolean havePermission(String projectKey, String permission , String username) {
Boolean havePermission = restTemplate.getForObject(String.format("http://uaa-service/permission/check?project=%1&permission=%2&username=%3",
projectKey, permission, username
), Boolean.class);
return havePermission;
}
}
The inconvenient : is need to call UAA Service for each time
Second idea : USE USER_ROLE
With user_role
username | role
mylogin1 | SHOP1.CAN_MANAGE_CATALOG
mylogin1 | SHOP1.CAN_VIEW_CATALOG
mylogin1 | SHOP2.CAN_MANAGE_CATALOG
mylogin1 | SHOP2.CAN_VIEW_CATALOG
mylogin1 | SHOP2.CAN_MANAGE_ORDER
mylogin1 | SHOP2.CAN_VIEW_ORDER
...
SHOP1 is SHOP2 is projectKey
The inconvenient : i am not sure but if user change permission, i need to revoke all token associate
Third idea : add specific permission in authentication blob
I don't know how to do for storing...
And in controller a annotation :
#PreAuthorize("#ProjectAccessExpression.hasPermission(authentication, 'manageCatalog||viewCatalog', #projectKey)
The inconvenient : same inconvenient at second idea
It basically looks like your just trying to leverage roles with OAuth 2.0 for your project. Here is an excerpt of some spring documentation on OAuth 2.0
Mapping User Roles to Scopes: http://projects.spring.io/spring-security-oauth/docs/oauth2.html
It is sometimes useful to limit the scope of tokens not only by the scopes assigned to the client, but also according to the user's own permissions. If you use a DefaultOAuth2RequestFactory in your AuthorizationEndpoint you can set a flag checkUserScopes=true to restrict permitted scopes to only those that match the user's roles. You can also inject an OAuth2RequestFactory into the TokenEndpoint but that only works (i.e. with password grants) if you also install a TokenEndpointAuthenticationFilter - you just need to add that filter after the HTTP BasicAuthenticationFilter. Of course, you can also implement your own rules for mapping scopes to roles and install your own version of the OAuth2RequestFactory. The AuthorizationServerEndpointsConfigurer allows you to inject a custom OAuth2RequestFactory so you can use that feature to set up a factory if you use #EnableAuthorizationServer.
All this basically boils down to is that you can protect your endpoints with different scopes by mapping scopes to your own custom roles. This will allow you to get really fine grained with your security.
I found a pretty good walk-through you can use as a reference: (Obviously you'll have to configure the settings to your own use case)
https://raymondhlee.wordpress.com/2014/12/21/implementing-oauth2-with-spring-security/
This solution i use and work fine
** 1 - Load Business Logic Security when user sign **
This example find user with role persist in database, and add all role depending project. After the operation i have authentication token with
GrantedAuthority : ROLE_USER, ROLE_MANAGE_CATALOG:project1, ROLE_VIEW_PROFILE:project1, ROLE_MANAGE_PROJECT:project2, ...
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = userService.findByLogin(username);
if (!user.isPresent()) {
Object args[] = {username};
throw new UsernameNotFoundException(
messageSource.getMessage("user.notexist", args, "User {0} doesn't not exist", LocaleContextHolder.getLocale())
);
}
if (!user.get().isActivated()) {
//throw new UserNotActivatedException(String.format("User %s was not activated!", username));
Object args[] = {username};
throw new UserNotActivatedException(
messageSource.getMessage("user.notactivated", args, "User {0} was not activated", LocaleContextHolder.getLocale()));
}
//Here implement your proper logic
//Add busness logic security Roles
// eg ROLE_MANAGE_PROJECT:{project_key}, ROLE_MANAGE_CATALOG:{project_key}
List<Role> bRoles = projectService.getRolesForUser(username)
user.get().getRoles().addAll(
bRoles
);
UserRepositoryUserDetails userDetails = new UserRepositoryUserDetails(user.get());
return userDetails;
}
}
** 2 check security with preauthorize expression **
In this example only user who have this permission can do this operation:
ROLE_ADMIN OR
ROLE_MANAGE_PROJECT:{projectKey}
#PreAuthorize("#oauthUserAccess.hasPermission(authentication, '"+Constants.PP_MANAGE_PROJECT+"', #projectKey)")
#RequestMapping(
value="/projects/{projectKey}",
method = RequestMethod.PUT,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity updateProject(#PathVariable("projectKey") String projectKey,#Valid #RequestBody Project project)
The OauthUserAccess class :
#Component("oauthUserAccess")
public class OauthUserAccess {
/**
* Check if it is the administrator of the application IMASTER
* #param authentication
* #param projectKey
* #return
*/
public boolean hasAdminPermission(OAuth2Authentication authentication, String projectKey) {
if(authentication.getOAuth2Request().getAuthorities().contains("ROLE_ADMIN")) return true;
return false;
}
/**
*
* #param authentication
* #param permissionType
* #param projectKey
* #return
*/
public boolean hasPermission(OAuth2Authentication authentication, String permissionType, String projectKey) {
if (!ProjectPermissionType.exist(permissionType) ||
projectKey.isEmpty() ||
!projectKey.matches(Constants.PROJECT_REGEX))
return false;
if (authentication.isClientOnly()) {
//TODO check scope permission
if(authentication.getOAuth2Request().getScope().contains(permissionType+":"+projectKey)) return true;
}
if (hasAdminPermission(authentication, projectKey)) return true;
String projectPermission = "ROLE_" + permissionType + ":" + projectKey;
String projectPermissionManage = "ROLE_" + permissionType.replace("VIEW", "MANAGE") + ":" + projectKey;
String manageProject = "ROLE_" + Constants.PP_MANAGE_PROJECT + ":" + projectKey;
Predicate<GrantedAuthority> p = r -> r.getAuthority().equals(projectPermission) || r.getAuthority().equals(projectPermissionManage) || r.getAuthority().equals(manageProject);
if (authentication.getAuthorities().stream().anyMatch(p)) {
return true;
};
return false;
}
}
3 - Advantage / disadvantage
Advantage
the business logic permission is loaded only when user login to the application and not every times so it is powerfull solution for microservice architecture.
Disadvantage
Need to update authentication token or revoke the token when permission change else
when you update the permission for user, the user required to logout and login. But you have same issue without this security logic, for example when user is disabled or enable.
My solution i use for example in a controller :
newAuthorities = projectService.getRolesForUser(username);
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), newAuthorities);
OAuth2Authentication authentication = (OAuth2Authentication)SecurityContextHolder.getContext().getAuthentication();
Collection<OAuth2AccessToken> accessTokens = tokenStore.findTokensByUserName(principal.getName());
OAuth2Authentication auth2 = new OAuth2Authentication(authentication.getOAuth2Request(), newAuth);
accessTokens.forEach(token -> {
if (!token.isExpired()) {
tokenStore.storeAccessToken(token, auth2);
}
});
Related
I have a protected resource on Keycloak created via remote API with this code:
private fun ProtectedEntity.protect(location: URI, principal: Principal) {
val newResource = ResourceRepresentation()
newResource.name = this.id
newResource.displayName = this.name
newResource.type = "urn:$appName:resources:${this.javaClass.simpleName.toLowerCase().replace("entity", "")}"
newResource.owner = ResourceOwnerRepresentation(this.creator)
newResource.ownerManagedAccess = true
newResource.uris = setOf(location.path.toString())
authzClient.protection().resource().create(newResource)
}
The resource owner is the user who invoked the method and he can manage his own resources.
Now I have to check if a user has permission to access a resource and if not, I guess I should return a ticket in the case the user would like request access to the resource. I tried with this but authorize() throws "Error creating permission ticket".
override fun read(id: ID, principal: Principal): Mono<ResponseEntity<E>> {
val currentUserToken = principal!!.getCurrentUserToken()
val resource = authzClient.protection().resource().findByName(id.toString(), currentUserToken.getUsername())
val token = currentUserToken.tokenValue
val request = AuthorizationRequest()
request.addPermission(resource.id)
request.setRpt(token)
// This returns Error creating permission ticket !?
val response = authzClient.authorization().authorize(request)
val result = authzClient.protection().introspectRequestingPartyToken(response.token)
println(result.active)
if (result.active) {
return super.read(id, principal)
} else throw RuntimeException("Result token RPT is not active!")
}
How I would delegate on Keycloak permission evaluating using authzClient?
The solution is to replace .findByName() by .findByUri(). This endpoint not take into account the resource's owner. I created a gist just in case someone else may need it: ResourceAccessKeycloakWebFilter
For my Bachelor's-thesis I have to implement different kinds of Authentication and Authorization with different Frameworks.
Currently im at the OAuth2 chapter and have to implement it in the Play Framework (in Java). (I have already implemented it with Spring Boot)
While researching on how to approach this Problem, so far, I could not find a lot of helpful tips.
One of the main Questions I have is: after the Client authenticatet itselfe with the users credentials and has gotten the Token, how do I best verify the Token?
Basicly: What is the Play- counterpart to the "#PreAuthorize" annotation of Spring?
Any tip or link to a helpful website is appreciated.
So I think I solved my Problem. On the off chance that someone stumbles on the same question I'm gonna post the solution here:
As written in the Play-Docs (https://www.playframework.com/documentation/2.6.x/JavaOAuth) with OAuth2 and especially with the Password flow it is pretty simple.
First you need an Authorization-Service, here the implementation is straight forward. Just implement three methods:
POST /oauth/token for passing the user-credentials and recieving an access- and refresh-token
POST /oauth/refresh for when the access-token is no longer valid. Here the refresh-token is passed and a new acces token returned
POST /oauth/check_token for authorization. Here the access Token is passed and in my case I return the role, the user has. Alternativly it would also be possible and maybe eaven better to do the authorization process in the authorization-service. For this you would need to change the "check_token" method and pass the required role.
I just generated uuids as tokens and stored them in a database. I guess one could also use for example jwts and put the needed information (for example the expiration date) in the token.
Then my main question was about the annotations. I found this
https://github.com/bekce/oauthly
and just lookt at their implementation.
You basicly just need a class and a interface:
The Interface:
#With(AuthorizationServerAuthAction.class)
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface AuthorizationServerSecure {
boolean requireAdmin() default false;
boolean requirePersonnel() default false;
boolean requireGuest() default false;
}
The Class:
private WSClient ws;
private final String url = "http://localhost:9001/oauth/check_token";
#Inject
public AuthorizationServerAuthAction(WSClient ws) {
this.ws = ws;
}
private CompletionStage<JsonNode> callApi(String accessToken) {
CompletionStage<WSResponse> eventualResponse = ws.url(url).setContentType("application/x-www-form-urlencoded").setRequestTimeout(Duration.ofSeconds(10))
.addHeader("Authorization" , accessToken).post("none");
return eventualResponse.thenApply(WSResponse::asJson);
}
#Override
public CompletionStage<Result> call(Http.Context ctx) {
Optional<String> accessTokenOptional = ctx.request().header("Authorization");
JsonNode result = null;
if(!accessTokenOptional.isPresent()){
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "No token found in header!")
));
}
CompletionStage<JsonNode> apiResponse = callApi(accessTokenOptional.get());
try {
result = apiResponse.toCompletableFuture().get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
if(result == null) {
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "an error occurred")
));
}
String role = result.get("role").asText();
if(configuration.requireAdmin()){
if(role.equals("admin")) {
return delegate.call(ctx);
} else {
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "The user is not authorized to perform this action!")
));
}
} else if(configuration.requirePersonnel()) {
if(role.equals("personnel") || role.equals("admin")) {
return delegate.call(ctx);
} else {
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "The user is not authorized to perform this action!")
));
}
} else if(configuration.requireGuest()) {
if(role.equals("guest") || role.equals("personnel") || role.equals("admin")) {
return delegate.call(ctx);
} else {
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "The user is not authorized to perform this action!")
));
}
}
return CompletableFuture.completedFuture(unauthorized(Json.newObject()
.put("message", "an error occurred")
));
}
}
I use #SubscribeMapping in a Spring Boot application which heavily relies on WebSockets for data exchange. The application is secured with Spring Security.
Client-side I use Stomp over WebSocket:
this.socket = new WebSocket(this.socketUrl);
this.stompClient = Stomp.over(this.socket);
this.stompClient.debug = null;
this.stompClient.connect({},
function(frame) {
this.$.chartLoader.generateRequest();
}.bind(this),
function(e) {
window.setTimeout(function() {
this.connectWs();
}.bind(this), 2500);
}.bind(this)
);
this.stompClient.subscribe('/topic/chart/' + chart.id,
function(message, headers) {
this.setChartData(chart, JSON.parse(message.body));
}.bind(this), {
"id" : "" + chart.id
}
);
Server-side, how can I get the currently logged user in the annotated methods ?
#SubscribeMapping("/chart/{id}")
public void subscribeMapping(#DestinationVariable("id") final short id) {
// Here I would need the current user...
messagingTemplate.convertAndSend("/topic/chart/" + id, chartService.getChartData(account, sensor, new Date(), new Date()));
}
I have tried SecurityContextHolder.getContext().getAuthentication(), but it returns null.
You can try to add a Principal object as a method parameter. This interface is extended by Authentication which has several implementations available (Spring will inject the good one according to your configuration).
public void subscribeMapping(#DestinationVariable("id") final short id, Principal principal) {
principal.getName();
}
This article may also help you.
I've written some code using Spring Boot and Lombok. I've got two objects, user and loggedInUser both are based on User.class. I assign a different value to the each of the objects userEmail field. I got the following code. The user is what is in the request from the endpoint while the loggedInUser is the user performing the request.
This is the User.class:
#Component
#Data
public class User {
private String userEmail;
}
This is the class where I'm using User.class
private User user;
private User loggedInUser;
private SearchParameters searchParameters;
public UserController(
User user,
User loggedInUser,
SearchParameters searchParameters, {
this.user = user;
this.loggedInUser = loggedInUser;
this.searchParameters = searchParameters;
}
#GetMapping(value = "${apiVersion.v_1}" + "/users")
public ResponseEntity getUsers(
#RequestParam("userEmail") String userEmail,
#RequestParam("direction") String direction,
#RequestParam("limit") String limit) {
Jws<Claims> jwsClaims = (Jws<Claims>) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
loggedInUser.setUserEmail(jwsClaims.getBody().getSubject()); //From a JWT the logged in user's e-mail is picked up. In this case joe#example.com
log.info("loggedInUser.getUserEmail #1: " + loggedInUser.getUserEmail());
user.setUserEmail(userEmail); // From the request parameters the email is that will be used for the search is picked up. In this case smith#example.com
searchParameters.setDirection(direction); //Can be "u" or "d" that corresponds to "SELECT * FROM user_tbl WHERE < user.getEmail()" or "SELECT * FROM user_tbl WHERE > user.getEmail()" in the SQL that will run in another class
searchParameters.setLimit(limit); //An integer that will limit the number of returned results from the user_tbl
log.info("loggedInUser.getUserEmail #2: " + loggedInUser.getUserEmail());
//The search is done in another class that is taking user.getEmail() as an argument and returns a List of users
return (new ResponseEntity(users, headers, HttpStatus.OK));
}
It gives me the following output.
loggedInUser.getUserEmail #1: joe#example.com
loggedInUser.getUserEmail #2: smith#example.com
I would have thought that user and loggedInUser are two separate objects since they have different names and that the dependency injection is instantiate a new object each time but it seems like user is overwriting loggedInUser. What is it I'm missing here?
I have an issue with using mobile app that I created in Ionic and back-end APIs that I coded in play framework, my issue simply is I need way to handle security matter for calling APIs that need to be secured, mean user must be logged in to do actions, as example guest can view group but can join only if logged in.
My issue that I believe that cookies is not supported for Mobile, i have code checking session that stored in cookies, its working for website, but it will not work for mobile, right?
Currently I'm trying to send a token that generated in back-end and returned with login response and stored in localStorage in Ionic, but my issue is that I can't sent token to be validated with request.
Front End:
I have the following http interceptor :
angular
.module('app.core')
.factory('sessionInjector', sessionInjector);
/** #ngInject */
function sessionInjector($q, sessionService, $rootScope) {
return {
request: function (config) {
$rootScope.$broadcast('loading:show');
if (!sessionService.isAnonymous())
config.headers['x-session-token'] = sessionService.getToken();
}
return config;
}
}
Back-End:
Controller:
#Security.Authenticated(Secure.class)
public Result joinOrganization() {
// Do some business
}
Secure.java :
#Override
public String getUsername(Http.Context ctx) {
// Is this correct way? I get null here
String str = ctx.request().getHeader("x-session-token");
String userId = ctx.session().get("usedId");
if (userId == null) {
return null;
}
User user = Play.application().injector().instanceOf(UserService.class).findUserById(Integer.parseInt(userId));
if (user != null && user.isActive) {
return user.id;
} else {
return null;
}
}
#Override
public Result onUnauthorized(Http.Context ctx) {
return unauthorized(results);
}
Note: Tokens stored in database:
Entity:
#Entity
#Table(name = "AUTHTOKEN")
public class AuthToken extends BaseModel {
#OneToOne(targetEntity = User.class, cascade = CascadeType.REFRESH, optional = false)
public User user;
#Column(nullable = false)
public String token;
#Column
public long expiration;
public AuthToken() {
}
}
For cookies working, but need to remove cookies and use tokens, or use them together cookies for website, tokens for mobile .
Mobile is no different to otherwise in regards of cookies. There are restrictions if AJAX, or cross-domain requests are used, and specially with Apple stuff, but they apply to non-mobile too. If your cookies work on a PC/Mac, they should do on a mobile device just as well. It's more of a form factor than anything else...
I found solution and it was complicated because there are many issues starting from that ngResource does not apply request interceptor its an issue opened from long time.
Second issue was how to send the token with ngResource, its simply with adding headers, the another issue here is getting dynamically the token, this "dynamically" means because the localStorage in memory getting lost when refresh so you need to get back it, this can be done with service, and function call for getting the token, something like this :
$resource('/user/:userId/card/:cardId', {userId:123, cardId:'#id'}, {
charge: {method:'POST', params:{charge:true}, headers = {
'x-session-token': function () {
return sessionService.getToken()
}}
});
Inside sessionService :
// this to recreate the cache in memory once the user refresh, it will keep the data if exisit but will point to it again in memory
if (CacheFactory.get('profileCache') == undefined) {
//if there is no cache already then create new one and assign it to profileCache
CacheFactory.createCache('profileCache');
}
function getCurrentSession() {
var profileCache = CacheFactory.get('profileCache');
if (profileCache !== undefined && profileCache.get('Token') !== undefined) {
return profileCache.get('Token');
}
return null;
}
function getToken() {
var currentSession = getCurrentSession();
if (currentSession != null && currentSession != '') {
return currentSession.token;
}
return null;
}
And then this method will work inside Secure.java
protected User getUser(Http.Context ctx) {
String token = ctx.request().getHeader("x-session-token");
if (token != null) {
return securityService.validateToken(token);
}
return null;
}