3 # Copyright 2007, Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 An OpenIDStore implementation that uses the datastore as its backing store.
19 Stores associations, nonces, and authentication tokens.
21 OpenIDStore is an interface from JanRain's OpenID python library:
22 http://openidenabled.com/python-openid/
24 For more, see openid/store/interface.py in that library.
29 from openid
.association
import Association
as OpenIDAssociation
30 from openid
.store
.interface
import OpenIDStore
31 from openid
.store
import nonce
32 from google
.appengine
.ext
import db
34 # number of associations and nonces to clean up in a single request.
35 CLEANUP_BATCH_SIZE
= 50
38 class Association(db
.Model
):
39 """An association with another OpenID server, either a consumer or a provider.
41 url
= db
.LinkProperty()
42 handle
= db
.StringProperty()
43 association
= db
.TextProperty()
44 created
= db
.DateTimeProperty(auto_now_add
=True)
47 class UsedNonce(db
.Model
):
48 """An OpenID nonce that has been used.
50 server_url
= db
.LinkProperty()
51 timestamp
= db
.DateTimeProperty()
52 salt
= db
.StringProperty()
55 class DatastoreStore(OpenIDStore
):
56 """An OpenIDStore implementation that uses the datastore. See
57 openid/store/interface.py for in-depth descriptions of the methods.
59 They follow the OpenID python library's style, not Google's style, since
60 they override methods defined in the OpenIDStore class.
63 def storeAssociation(self
, server_url
, association
):
65 This method puts a C{L{Association <openid.association.Association>}}
66 object into storage, retrievable by server URL and handle.
68 assoc
= Association(url
=server_url
,
69 handle
=association
.handle
,
70 association
=association
.serialize())
73 def getAssociation(self
, server_url
, handle
=None):
75 This method returns an C{L{Association <openid.association.Association>}}
76 object from storage that matches the server URL and, if specified, handle.
77 It returns C{None} if no such association is found or if the matching
78 association is expired.
80 If no handle is specified, the store may return any association which
81 matches the server URL. If multiple associations are valid, the
82 recommended return value for this method is the one that will remain valid
83 for the longest duration.
85 query
= Association
.all().filter('url', server_url
)
87 query
.filter('handle', handle
)
89 results
= query
.fetch(1)
91 association
= OpenIDAssociation
.deserialize(results
[0].association
)
92 if association
.getExpiresIn() > 0:
98 def removeAssociation(self
, server_url
, handle
):
100 This method removes the matching association if it's found, and returns
101 whether the association was removed or not.
103 query
= Association
.gql('WHERE url = :1 AND handle = :2',
105 return self
._delete
_first
(query
)
107 def useNonce(self
, server_url
, timestamp
, salt
):
108 """Called when using a nonce.
110 This method should return C{True} if the nonce has not been
111 used before, and store it for a while to make sure nobody
112 tries to use the same value again. If the nonce has already
113 been used or the timestamp is not current, return C{False}.
115 You may use L{openid.store.nonce.SKEW} for your timestamp window.
117 @change: In earlier versions, round-trip nonces were used and
118 a nonce was only valid if it had been previously stored
119 with C{storeNonce}. Version 2.0 uses one-way nonces,
120 requiring a different implementation here that does not
121 depend on a C{storeNonce} call. (C{storeNonce} is no
122 longer part of the interface.)
124 @param server_url: The URL of the server from which the nonce
127 @type server_url: C{str}
129 @param timestamp: The time that the nonce was created (to the
130 nearest second), in seconds since January 1 1970 UTC.
131 @type timestamp: C{int}
133 @param salt: A random string that makes two nonces from the
134 same server issued during the same second unique.
137 @return: Whether or not the nonce was valid.
141 query
= UsedNonce
.gql(
142 'WHERE server_url = :1 AND salt = :2 AND timestamp >= :3',
143 server_url
, salt
, self
._expiration
_datetime
())
144 return query
.fetch(1) == []
146 def cleanupNonces(self
):
147 """Remove expired nonces from the store.
149 Discards any nonce from storage that is old enough that its
150 timestamp would not pass L{useNonce}.
152 This method is not called in the normal operation of the
153 library. It provides a way for store admins to keep
154 their storage from filling up with expired data.
156 @return: the number of nonces expired.
159 query
= UsedNonce
.gql('WHERE timestamp < :1', self
._expiration
_datetime
())
160 return self
._cleanup
_batch
(query
)
162 def cleanupAssociations(self
):
163 """Remove expired associations from the store.
165 This method is not called in the normal operation of the
166 library. It provides a way for store admins to keep
167 their storage from filling up with expired data.
169 @return: the number of associations expired.
172 query
= Association
.gql('WHERE created < :1', self
._expiration
_datetime
())
173 return self
._cleanup
_batch
(query
)
176 """Shortcut for C{L{cleanupNonces}()}, C{L{cleanupAssociations}()}.
178 This method is not called in the normal operation of the
179 library. It provides a way for store admins to keep
180 their storage from filling up with expired data.
182 return self
.cleanupNonces(), self
.cleanupAssociations()
184 def _delete_first(self
, query
):
185 """Deletes the first result for the given query.
187 Returns True if an entity was deleted, false if no entity could be deleted
188 or if the query returned no results.
190 results
= query
.fetch(1)
201 def _cleanup_batch(self
, query
):
202 """Deletes the first batch of entities that match the given query.
204 Returns the number of entities that were deleted.
206 to_delete
= list(query
.fetch(CLEANUP_BATCH_SIZE
))
208 # can't use batch delete since they're all root entities :/
209 for entity
in to_delete
:
212 return len(to_delete
)
214 def _expiration_datetime(self
):
215 """Returns the current expiration date for nonces and associations.
217 return datetime
.datetime
.now() - datetime
.timedelta(seconds
=nonce
.SKEW
)