As I promised earlier there will be some news from battlefield 😉
SecureSocial is a plugin for Play! Framework which provides support for authentication and authorization (through both classic user/pass and OAuth providers).
It requires presence of CacheAPI implementation plugin but don’t worry – Play comes with default implementation based on Ehcache, which works very well.
But we want to try also alternative solution – Memcached, a distributed memory object caching system.
Maciej add play2-memcached plugin to project dependencies and configure it. I also have to install memcached daemon on my local machines.
Application starts without any warnings and everything goes as expected until I fire secured action.
This was a point where the problems begins 😦
Key is empty
When I tried to fire some simple SecuredAction I get an following exception:
java.lang.IllegalArgumentException: Key must contain at least one character.
I look closer to the stacktrace and start my investigation. Key was empty as exception message says, so my first direction was to find how the keys are built.
I quickly discovered that play2-memcached can prepend so called namespace to the given key to allow sharing the same storage between many application instances without key collisions.
In that moment little, yellow light bulb shines above my head 😉 I can provide workaround by setting memcached.namespace
value in application.conf
. I was awared that this is only workaround and the problem still lies somwhere in between chair and memcached plugin.
Key is too long
That works perfectly – when I wasn’t authenticated SecureSocial redirects me to a appropriate login page.
When I was going to register a new user everything goes right until I commit second registration step (filling the user details form after receive e-mail with activation link).
I got IllegalArgumentException one more time, but with different cause:
java.lang.IllegalArgumentException: Key is too long (maxlen = 250)
At this moment I think something like “if this would be a human being then it will be a very disgruntled woman” 😉
As before I started my investigation from a top of stacktrace:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
java.lang.IllegalArgumentException: Key is too long (maxlen = 250) | |
at net.spy.memcached.util.StringUtils.validateKey(StringUtils.java:69) ~[spymemcached-2.8.9.jar:2.8.9] | |
at net.spy.memcached.MemcachedConnection.enqueueOperation(MemcachedConnection.java:640) ~[spymemcached-2.8.9.jar:2.8.9] | |
at net.spy.memcached.MemcachedClient.asyncStore(MemcachedClient.java:301) ~[spymemcached-2.8.9.jar:2.8.9] | |
at net.spy.memcached.MemcachedClient.set(MemcachedClient.java:704) ~[spymemcached-2.8.9.jar:2.8.9] | |
at com.github.mumoshu.play2.memcached.MemcachedPlugin$$anon$2.set(MemcachedPlugin.scala:129) ~[play2-memcached_2.10-0.3.0.2.jar:0.3.0.2] | |
at play.api.cache.Cache$.set(Cache.scala:56) ~[play_2.10-2.1.3.jar:2.1.3] |
Unfortunately I can’t find cause in any of these method calls.
At the same moment I remind myself that few days ago I was looking for cause of logging off when Play! is reloaded after some code changes in dev mode and I found answer on stackoverflow.
Jorge explained there that AuthenticatorStore’s default implementation persists user information using Play!’s cache abstraction.
I study its source code, especially DefaultAuthenticatorStore and I discovered two important things:
- An authenticator id is used as a cache key. Id is generated by IdGenerator trait implementation,
- Method find never checks that given key is empty, it always forwards them to the cache plugin. That was a cause of my first problem
I also found that DefaultIdGenerator produces 256 characters long identifiers – it is too long for Memcached.
My solution
Update: Solution described below is outdated. I succeed with providing a more straightforward solution, which I described in “SecureSocial & Memcached – Never that easy”.
I create my custom implementations of IdGenerator and AuthenticatorStore which restricts the invariants:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package service.auth | |
import securesocial.core.IdGenerator | |
import play.api.Application | |
import java.security.SecureRandom | |
import play.api.libs.Codecs | |
class MemcachedIdGenerator(app: Application) extends IdGenerator(app) { | |
val random = new SecureRandom() | |
val memcachedNsLen = app.configuration.getString("memcached.namespace").map(_.length).getOrElse(0) | |
val idSizeInBytes = 125 – (memcachedNsLen / 2) – (memcachedNsLen % 2) | |
/** | |
* Generates a new id using SecureRandom | |
* | |
* @return the generated id | |
*/ | |
def generate: String = { | |
val randomValue = new Array[Byte](idSizeInBytes) | |
random.nextBytes(randomValue) | |
Codecs.toHexString(randomValue) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package service.auth | |
import securesocial.core.{Authenticator, DefaultAuthenticatorStore} | |
import play.api.Application | |
class MemcachedAuthenticatorStore(app: Application) extends DefaultAuthenticatorStore(app) { | |
override def find(id: String): Either[Error, Option[Authenticator]] = | |
if (id.isEmpty) | |
Right(None) | |
else | |
super.find(id) | |
} |
I replace problematic implementations in play.plugins
with these new ones and now everything works as expected 🙂
Also memcached namespace isn’t needed any more to avoid empty key problem.
Conclusion
There is a lot of joy in such investigations (I fixed the problem, oh yeah!).
I decide to make a pull request to SecureSocial repository which will provides memcache support “out of the box”.
Question
What do you think – which element should be responsible for checking if given key is empty when covered implementation doesn’t support such keys?
Does client (secure social) have to do that or maybe the middleware (memcached play plugin) in between framework abstraction and concrete 3rd party solution?
Don’t hesitate yourself to discuss in comments 🙂
I hope there will be answer in the next post 🙂
Thanks for your workaround. I faced the same problem today and came across the same solution for a long-key-problem. I’m quite sure that securesocial plugin is a wrong place for a fix with an empty-key-problem, while an empty key is a general case. Its not possible to use an empty key in memcached, so the memcached plugin should return None on quering it with empty key. I’ve reported an issue: https://github.com/mumoshu/play2-memcached/issues/25
I totally agree with you. Few days ago I committed fix which works as you described 😉
This worked for me! +1