Is there a best practice approach to proper authorization rules for protected content in a firebase app
using firepad specifically
By protected content I mean where a user creates a document and only shares it with certain other users).
Also I need to be able to query firebase for all documents that i have access to (docs that I created and docs other users shared with me)
Some of my research thus far:
Method 1: Secret URL
I need to know the URL to be able to view/edit the document
Not real authorization, as any logged in user that has access to that URL could edit/modify it.
Cant index all the docs i have access to
Method 2: Using firebase authorization rules to add users to a document and check if user is document.users before reading/writing.
Taken From:
Protected content in Firebase possible?
{
"documents": {
"$documents_id": {
// any friend can read my post
".read": "auth.id === data.child('owner').val() || root.child('users/'+data.child.owner.val()+'/users/'+auth.id).exists()",
// any friend can edit my post
".write": "auth.id === data.child('owner').val() || root.child('users/'+data.child.owner.val()+'/users/'+auth.id).exists()"
},
users:{
// List of user.ids that have access to this document
}
}
}
Pros:
Proper authorization/authentication. Only authenticated users who have been granted access can view/edit.
Cons:
Cannot query for all documents a user is allowed to edit (those that I own or have been shared with me) (Is this assumption correct?)
Method 3: Firebase authorization rules (method 2), plus a redundant store of users with array of document_ids each users has access to. This user store would only be used to query all the documents a user has access to.
ie:
{
"documents": {
"$documents_id": {
// any friend can read my post
".read": "auth.id === data.child('owner').val() || root.child('users/'+data.child.owner.val()+'/users/'+auth.id).exists()",
// any friend can edit my post
".write": "auth.id === data.child('owner').val() || root.child('users/'+data.child.owner.val()+'/users/'+auth.id).exists()"
}
},
"users":{
"$user":{
".read": "auth.id=$user.id",
".write": "auth.id=$user.id"
"$documents":{
// All the documents i have access to. This list gets ammended whenever I am granted/stripped access to a document.
}
}
}
}
Pros:
Proper authentication/authorization
Cons:
Duplicate data, have to deal with synchronization issues between two data stores. This just doesnt seem like a good idea.
Method 4: Groups
Using groups per Granting access to Firebase locations to a group of users
We have a group for every document in data store
Cant easily query firebase for all the docs a user can access
Is there a better way to do this?
You've done a good job of enumerating the options, and you're definitely on the right track. As you've discovered, there's no way to query based on security rules. This was done intentionally, since (depending on your security rules) this could be quite expensive (Firebase avoids complex queries in general for this reason).
So your method 3 is the exact right way to do this. Duplicating data for these sorts of situations is actually a very common practice. See Denormalizing Your Data is Normal for a blog post that goes into more detail on this.
You could also do method 1 with the duplicated document list. This is especially useful if you want to be able to "invite" somebody to a document just with a URL (that contains the secret ID). Or you could do a combination of the two (have some documents be "public but unlisted" and some be "private to invited friends" or whatever.)
Related
We want to display customer (actually customer-group) specific information on product detail pages in Shopware 6.
There seems to be the HTTP cache and we are afraid that the page would be cached if a specific customer group displays the page and the information would be leaked to non-customers.
Is this assumption correct?
The documentation does not reveal much information about this.
Is there a way to set specific cache tags, so that the information is only displayed to the correct customer group?
Or do we need to fetch the data dynamically via AJAX?
Bonus question: Can the HTTP cache be simulated in automatic tests to ensure the functionality works?
What I found out so far:
The is annotation #httpCache for controller, which seems to control whether a page is cached or not
The cache key is generated in \Shopware\Storefront\Framework\Cache\HttpCacheKeyGenerator::generate. It take the full request URI into account, and some cacheHash which is injected. I believe it would not take the customer group into account
Maybe this generate() method could be decorated, but I am not sure if that is the right way.
There is a cookie being set sw-cache-hash which influences the caching. It takes the customer into account.
sw-cache-hash is created here:
if ($context->getCustomer() || $cart->getLineItems()->count() > 0) {
$cookie = Cookie::create(self::CONTEXT_CACHE_COOKIE, $this->buildCacheHash($context));
$cookie->setSecureDefault($request->isSecure());
$response->headers->setCookie($cookie);
} else {
$response->headers->removeCookie(self::CONTEXT_CACHE_COOKIE);
$response->headers->clearCookie(self::CONTEXT_CACHE_COOKIE);
}
So as soon you are logged in or have some items in the cart, a different cache hash is used. This depends on the following, but not on the customer group it self:
private function buildCacheHash(SalesChannelContext $context): string
{
return md5(json_encode([
$context->getRuleIds(),
$context->getContext()->getVersionId(),
$context->getCurrency()->getId(),
]));
}
Additionally there is the notion of cache-invalidation states, that describe when the caching should not be used.
You can configure that inside the shopware.yaml config file for the http-cache as a whole or on route level for the store-api routes.
From the default config inside platform:
shopware:
cache:
invalidation:
http_cache: ['logged-in', 'cart-filled']
product_listing_route: []
As you can see by default the http-cache won't be used if a user logs in or has something in his cart.
As you can see in the last code snippet, it takes into account the active Rule ids.
This means that if you create a rule (through Settings > Rule Builder) that is active on a certain group, but not on another or no group, it will be taken into account & create a different cache hash for the different customer groups.
I would like to secure my rest endpoints in the backend. For example an author can query his books like this:
/books?authorId=5&login=username
#GetMapping("/books")
#Timed
public ResponseEntity<List<Book>> getAllBooks(
#RequestParam(value="authorId", required = false) String authorId,
#RequestParam(value="login", required = false) String login) {
if(!login.equals(SecurityUtils.getCurrentUserLogin().get())){
return ResponseEntity.status(401).build();
}
List<Book> result;
if(authorId!= null)
result = bookService.findByAuthorId(authorId);
else if("admin".equals(SecurityUtils.getCurrentUserLogin().get()))
result = bookService.findAll();
else return ResponseEntity.status(401).build();
return ResponseEntity.ok().body(result);
}
Preferable I would like to only pass the authorId in the params
/books?authorId=5
but since SecurityUtils only gives me the loginName I can't compare them and identify the user in the backend.
Also since it's a microservice I can't access the AccountService.java which is handled by the gateway.
It all works fine but it seems wrong? Is there a better way to allow certain querys only for certain users? Should I make another rest endpoint which handles specifally requests to get books for specific users?
Thank you
You are addressing 2 use cases: one for authors (list my books) and one for management (list all books) for security reasons but usually you may also want to return different data based on use case. It could be a good idea to have 2 different resources: /api/my_books for authors and /api/books for management, you could even use nested resources.
For returning different data (also for security reasons) you can use the DTO option of JHipster with a service layer to map them from entities rather than exposing entities in your REST controllers.
Also don't pass the user id as a request param, you should modify TokenProvider to add it to the token as a claim. If you don't want to add user id to the token, you should modify book entity in your service so that it references user login rather than internal id, as long as it is immutable it does not make a difference.
I need help for creating the REST endpoints. There are couple of activities :
To change the email there are 3 URL requests required:
/changeemail : Here one time password (OTP) is sent to the user's mobile
/users/email : the user sends the one time password from previous step and system sends the email to the new user to click on the email activate link
/activateemail : user clicks on the link in the new email inbox and server updates the new email
To change password :
/users/password (PATCH) : user submits old password and new password and system accordingly updates the new password
Similarly, there are other endpoints to change profile (field include bday, firstname and last name)
after reading online I believe my system as only users as the resource --> so to update the attributes I was thinking of using a single PATCH for change email and change password and along with that something like operation field so the above two features will look like :
For changing email :
operation : 'sendOTPForEmailChange'
operation : 'sendEmailActivationLink'
operation : 'activateEmail'
For changing password :
operation : 'changePassword'
and I will have only one endpoint for all the above operations that is (in nodejs) :
app.patch('/users', function (req, res) {
// depending upon the operation I delegate it to the respective method
if (req.body.operation === 'sendOTPForEmailChange') {
callMethodA();
} else if (req.body.operation === 'sendEmailActivationLink') {
callMethodB();
} else if (req.body.operation === 'activateEmail') {
callMethodC();
} else if (req.body.operation === 'changePassword') {
callMethodC();
} else sendReplyError();
});
Does this sound a good idea ? If not, someone can help me form the endpoints for changeemail and changepassword.
Answer :
I finally settled for using PATCH with operation field in the HTTP Request Body to indicate what operation has to be performed.
Since I was only modifying a single field of the resource I used the PATCH method.
Also, I wanted to avoid using Verbs in the URI so using 'operation' field looked better.
Some references I used in making this decision :
Wilts answer link here
Mark Nottingham' blog link article
and finally JSON MERGE PATCH link RFC
You should make the links that define the particular resource, avoid using PATCH and adding all the logic in one link keep things simple and use separation of concern in the API
like this
1- /users/otp with HTTP Verb: GET -> to get OTP for any perpose
2- /users/password/otp with HTTP Verb: POST -> to verify OTP for password and sending link via email
3- /users/activate with HTTP Verb: POST to activate the user
4- /users/password with HTTP Verb: PUT to update users password
Hashing Security is a must read, IMHO, should you ever want to implement your own user account system.
Two-factor identification should always be considered, at least as an opt-in feature. How would you integrate it into your login scheme ?
What about identity federation ? Can your user leverage their social accounts to use your app ?
A quick look at Google yielded this and this, as well as this.
Unless you have an excellent reason to do it yourself, I'd spend time integrating a solution that is backed by a strong community for the utility aspects of the project, and focus my time on implementing the business value for your customers.
NB: my text was too long for the comments
Mostly agree with Ghulam's reply, separation of concerns is key. I suggest slightly different endpoints as following:
1. POST /users/otp -> as we are creating a new OTP which should be returned with 200 response.
2. POST /users/email -> to link new email, request to include OTP for verification.
3. PUT /users/email -> to activate the email.
4. PUT /users/password -> to update users password.
I use the security.yml with access_control to secure the API paths based on the user role. This works fine, but how do I secure specific parameters like /api/project/:id?
Different users have access to different project ids. Therefore a database call has to be made to check if this user has access to this project.
I tried to use $this->denyAccessUnlessGranted('GET', $projectId, 'Unauthorized access!'); in the ProjectController, which calls a custom Voter to check the database and therefore the access.
public function getProjectAction(Request $request, $id)
{
$this->denyAccessUnlessGranted('GET', $id, 'Unauthorized access!');
This works, but it seems very unpractical to add this code to 10+ actions in the ProjectController alone and also in many parts of the API.
Therefore my question: What is the best pratice to secure a REST api with symfony2, fosUserBundle and fosRestBundle
I would suggest introducing security voters.
http://symfony.com/doc/current/cookbook/security/voters_data_permission.html
Also create some kind of exception handler / listener, to catch your exceptions and make a specific error response.
http://symfony.com/doc/current/cookbook/service_container/event_listener.html
I'm having trouble finding documentation on the request object argument used in replication filters ('req' in the sample below):
function(doc, req) {
// what is inside req???
return false;
}
This old CouchBase blog post has a little code snippet that shows the userCtx variable being a part of the request object:
What is this userCtx? When you make an authenticated request against
CouchDB, either using HTTP basic auth, secure cookie auth or OAuth,
CouchDB will verify the user’s credentials. If they match a CouchDB
user, it populates the req.userCtx object with information about the
user.
This userCtx object is extremely useful for restricting replication of documents to the owner of the document. Check out this example:
function(doc, req) {
// require a valid request user that owns the current doc
if (!req.userCtx.name) {
throw("Unauthorized!");
}
if(req.userCtx.name == doc.owner) {
return true;
}
return false;
}
But the problem now is that CouchDB requires the filter method to be explicitly chosen by the initiator of the replication (in this case, the initiator is a mobile user of my web app):
curl -X POST http://127.0.0.1:5984/_replicate \
-d '{"source":"database", \
"target":"http://example.com:5984/database", \
"filter":"example/filtername"
}'
The Question
Is there a way to enforce a specific filter by default so that users are restricted to replicating only their own data? I'm thinking the best way to do this is to use a front end to CouchDB, like Nginx, and restrict all replication requests to ones that include that filter. Thoughts? Would love a way to do this without another layer in front of CouchDB.
Data replication stands right with user ability to read data. Since if your users shares data within single database all of them has right to replicate all of them to their local couches. So you couldn't apply any documents read restriction unless you've split single shared database into several personal ones - this is common use case for such situations.
There is no any way to enforce apply changes feed filter or other parameters like views has. However, you can use rewrites to wraps requests to some resources with predefined query parameters or even with dynamic ones. This is a little not solution that you'd expected, but still better that nginx and some logic at his side: probably, you'd to allow users to specify custom filters with custom query parameters and enforce you're own only if nothing specified, right?
P.S. Inside req object is very useful about current request. Partially it was described at wiki, but it's a little out of date. However, it's easily to view it with simple show function:
function(doc, req){
return {json: req}
}