**Problem**: You want to create a system whereby when a user authenticates to `example.com`, they're also automatically logged in at `foo.com`, `bar.com`, `baz.com`, and any other domains that you decide to add to the list at a later date. Okay, great, that *seems* straightforward, except there's a complication: The Same Origin Policy prevents you from getting/setting cookies on domains other than the one you control. (Unless your users' browser [foolishly disables the Same Origin Policy like Comodo did](https://code.google.com/p/google-security-research/issues/detail?id=704)). Let's narrow it down a little bit further: Unlike a situation where e.g. "Login with Facebook" would be appropriate, you control all of the domains. They just happen to be different, so the Same Origin Policy kicks in. For simplicity, feel free to assume they're on the same server and application codebase, but you have a multi-site architecture in place where some of the sites have a different domain name. Let's work around this limitation with as few moving parts as possible. Before we go any further, first **make sure that [OAuth2 or SAML](https://www.mutuallyhuman.com/blog/2013/05/09/choosing-an-sso-strategy-saml-vs-oauth2/) don't already solve your exact use case** (particularly if you want users' consent or you don't already control every domain in scope of single sign-on). Generally: follow the standards before blog posts. That said, OAuth2 and SAML are trying to [solve the 99% problem, instead of the 50% problem](http://blog.ircmaxell.com/2014/10/an-open-letter-to-php-fig.html). Is there a way to solve this problem, securely, without dealing with OAuth, SAML, LDAP, JSONP (or JavaScript in general)? It turns out: Yes. We propose a simpler solution (which may *NOT* be right for you). Instead of mucking with HTTP headers and various types of tokens, or having to implement and secure a server-side XML parser (at minimum, with [`libxml_disable_entity_loader()`](https://secure.php.net/libxml_disable_entity_loader)), we propose a solution that only requires: 1. [Halite](https://paragonie.com/project/halite), our user-friendly [libsodium](https://github.com/jedisct1/libsodium) wrapper 2. A simple REST API endpoint that accepts Ed25519-signed JSON messages and optionally serves an image # Secure Automatic Login with Multiple Domains using Libsodium First, you need a secure login system. This means: * [Securely handling your users' passwords](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016) * If you need it, make sure your ["remember me" (persistent authentication) cookies](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#title.2.1) are securely implemented We recommend not writing it yourself to anyone who can't, off the top of their heads (i.e. with no reference materials), explain what `N`, `p`, and `r` mean in terms of scrypt. (Or in other words, isn't a cryptography engineering and secure software development expert.) > **Easy mode: Use [Gatekeeper](https://github.com/psecio/gatekeeper)**. Got it implemented? Good, here's where the fun part begins. ## The Setup and Design Phase First, we need to create what Halite calls a `SignatureKeyPair`. You can either generate one randomly and store it for long-term use, or (if you prefer) derive it from a password and salt. Since we're going for simplicity, we'll opt for the first approach: getSecretKey(); $sign_publickey = $sign_keypair->getPublicKey(); KeyFactory::save($sign_secretkey, '/path/to/secretkey'); KeyFactory::save($sign_publickey, '/path/to/publickey'); // To load one of the keys at run-time, use the appropriately named method: $sign_secretkey = KeyFactory::loadSignatureSecretKey('/path/to/secretkey'); $sign_publickey = KeyFactory::loadSignaturePublicKey('/path/to/publickey'); For every domain that needs to support transparent automatic authentication, we are going to: 1. Define a special URL endpoint (e.g. `/halite_auto_login`) 2. Make sure every endpoint has access to your public key. 3. Make sure every endpoint has access to the same data persistence layer, e.g. relational database. Otherwise, an OAuth workflow would make more sense. 4. (Optional) Share a few "status" icons, e.g. ![Success](https://paragonie.com/files/blog/icons/accept.png) ![Failure](https://paragonie.com/files/blog/icons/cross.png) (from the [Silk icon set by Mark James](http://www.famfamfam.com/lab/icons/silk/)), across each domain. ## The API Endpoint First thing's first, make sure `session_start()` is invoked (and you're [implementing secure PHP sessions](https://paragonie.com/blog/2015/04/fast-track-safe-and-secure-php-sessions)). We should expect three GET parameters, only two of them are required: 1. A `payload`, which is a hex-encoded JSON message. 2. A `signature`, which is a hex-encoded string that authenticates the `payload`. 3. (Optional) UUID that references the particular `SignaturePublicKey`, in case multiple sites can issue the automatic logins (this is purely an optimization). You can omit this if you only have one keypair. Handling the payload is straightforward: add(new DateInterval('PT05M')) // 5 Minutes ->format('Y-m-d\TH:i:s'); $stmt = $this->db->prepare( "INSERT INTO cross_site_logins (domain, user, nonce, expires) VALUES (?, ?, ?, ?);" ); $stmt->execute([$domain, $userID, $nonce, $expires]); $payload = json_encode([ 'nonce' => $nonce, 'expires' => $expires ]); $signature = AsymmetricCrypto::sign($payload, $this->signatureSecretKey); return ''; } // ... } This will create an image tag that will send a cross-domain HTTP request. If it succeeds, it will return an image indicating a success status. If it doesn't, it will return a different image. ## Security Considerations * The first thing your auto-login API handler should do is verify the Ed25519 signature. * Only allow a nonce to be used for a given domain. * After a successful automatic login, **delete** the nonce from the database. Only allow them to succeed once, to prevent replay attacks. * Expiration times should, ideally, be less than one minute. However, if your end users have a slow or unreliable Internet connection, this could create problems. The system above is only secure if the following assumptions hold true: * The system's random number generator (used by `random_bytes()`) is secure. * The signing key used to sign requests to the API is unknown to attackers. * All systems are using HTTPS in the most secure configuration possible (i.e. TLS 1.2 with AES-GCM or ChaCha20-Poly1305 and 2048-bit RSA or 256-bit ECDSA certificates with HSTS and HPKP). This is a prerequisite in order [for PHP sessions to be secure](https://paragonie.com/blog/2015/04/fast-track-safe-and-secure-php-sessions). Even if an attacker can arbitrarily insert valid "nonces" into the database for a valid user, without the signing key, they can't bypass the signature verification logic. However, if an attacker can arbitrarily alter database records, they can probably just reset arbitrary users' passwords so you have bigger problems.