1<?php
2
3/**
4 * Supplies Redis server store backend for OpenID servers and consumers.
5 * Uses Predis library {@see https://github.com/nrk/predis}.
6 * Requires PHP >= 5.3.
7 *
8 * LICENSE: See the COPYING file included in this distribution.
9 *
10 * @package OpenID
11 * @author Ville Mattila <ville@eventio.fi>
12 * @copyright 2008 JanRain Inc., 2013 Eventio Oy / Ville Mattila
13 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
14 * Contributed by Eventio Oy <http://www.eventio.fi/>
15 */
16
17/**
18 * Import the interface for creating a new store class.
19 */
20require_once 'Auth/OpenID/Interface.php';
21
22/**
23 * Supplies Redis server store backend for OpenID servers and consumers.
24 * Uses Predis library {@see https://github.com/nrk/predis}.
25 * Requires PHP >= 5.3.
26 *
27 * @package OpenID
28 */
29class Auth_OpenID_PredisStore extends Auth_OpenID_OpenIDStore {
30
31    /**
32     * @var \Predis\Client
33     */
34    protected $redis;
35
36    /**
37     * Prefix for Redis keys
38     * @var string
39     */
40    protected $prefix;
41
42    /**
43     * Initializes a new {@link Auth_OpenID_PredisStore} instance.
44     *
45     * @param \Predis\Client $redis  Predis client object
46     * @param string         $prefix Prefix for all keys stored to the Redis
47     */
48    function __construct(\Predis\Client $redis, $prefix = '')
49    {
50        $this->prefix = $prefix;
51        $this->redis = $redis;
52    }
53
54    /**
55     * Store association until its expiration time in Redis server.
56     * Overwrites any existing association with same server_url and
57     * handle. Handles list of associations for every server.
58     */
59    function storeAssociation($server_url, $association)
60    {
61        // create Redis keys for association itself
62        // and list of associations for this server
63        $associationKey = $this->associationKey($server_url,
64            $association->handle);
65        $serverKey = $this->associationServerKey($server_url);
66
67        // save association to server's associations' keys list
68        $this->redis->lpush(
69            $serverKey,
70            $associationKey
71        );
72
73        // Will touch the association list expiration, to avoid filling up
74        $newExpiration = ($association->issued + $association->lifetime);
75
76        $expirationKey = $serverKey.'_expires_at';
77        $expiration = $this->redis->get($expirationKey);
78        if (!$expiration || $newExpiration > $expiration) {
79            $this->redis->set($expirationKey, $newExpiration);
80            $this->redis->expireat($serverKey, $newExpiration);
81            $this->redis->expireat($expirationKey, $newExpiration);
82        }
83
84        // save association itself, will automatically expire
85        $this->redis->setex(
86            $associationKey,
87            $newExpiration - time(),
88            serialize($association)
89        );
90    }
91
92    /**
93     * Read association from Redis. If no handle given
94     * and multiple associations found, returns latest issued
95     */
96    function getAssociation($server_url, $handle = null)
97    {
98        // simple case: handle given
99        if ($handle !== null) {
100            return $this->getAssociationFromServer(
101                $this->associationKey($server_url, $handle)
102            );
103        }
104
105        // no handle given, receiving the latest issued
106        $serverKey = $this->associationServerKey($server_url);
107        $lastKey = $this->redis->lindex($serverKey, -1);
108        if (!$lastKey) {
109            // no previous association with this server
110            return null;
111        }
112
113        // get association, return null if failed
114        return $this->getAssociationFromServer($lastKey);
115    }
116
117    /**
118     * Function to actually receive and unserialize the association
119     * from the server.
120     */
121    private function getAssociationFromServer($associationKey)
122    {
123        $association = $this->redis->get($associationKey);
124        return $association ? unserialize($association) : null;
125    }
126
127    /**
128     * Immediately delete association from Redis.
129     */
130    function removeAssociation($server_url, $handle)
131    {
132        // create Redis keys
133        $serverKey = $this->associationServerKey($server_url);
134        $associationKey = $this->associationKey($server_url,
135            $handle);
136
137        // Removing the association from the server's association list
138        $removed = $this->redis->lrem($serverKey, 0, $associationKey);
139        if ($removed < 1) {
140            return false;
141        }
142
143        // Delete the association itself
144        return $this->redis->del($associationKey);
145    }
146
147    /**
148     * Create nonce for server and salt, expiring after
149     * $Auth_OpenID_SKEW seconds.
150     */
151    function useNonce($server_url, $timestamp, $salt)
152    {
153        global $Auth_OpenID_SKEW;
154
155        // save one request to memcache when nonce obviously expired
156        if (abs($timestamp - time()) > $Auth_OpenID_SKEW) {
157            return false;
158        }
159
160        // SETNX will set the value only of the key doesn't exist yet.
161        $nonceKey = $this->nonceKey($server_url, $salt);
162        $added = $this->redis->setnx($nonceKey, "1");
163        if ($added) {
164            // Will set expiration
165            $this->redis->expire($nonceKey, $Auth_OpenID_SKEW);
166            return true;
167        } else {
168            return false;
169        }
170    }
171
172    /**
173     * Build up nonce key
174     */
175    private function nonceKey($server_url, $salt)
176    {
177        return $this->prefix .
178               'openid_nonce_' .
179               sha1($server_url) . '_' . sha1($salt);
180    }
181
182    /**
183     * Key is prefixed with $prefix and 'openid_association_' string
184     */
185    function associationKey($server_url, $handle = null)
186    {
187        return $this->prefix .
188               'openid_association_' .
189               sha1($server_url) . '_' . sha1($handle);
190    }
191
192    /**
193     * Key is prefixed with $prefix and 'openid_association_server_' string
194     */
195    function associationServerKey($server_url)
196    {
197        return $this->prefix .
198               'openid_association_server_' .
199               sha1($server_url);
200    }
201
202    /**
203     * Report that this storage doesn't support cleanup
204     */
205    function supportsCleanup()
206    {
207        return false;
208    }
209
210}
211
212