documentation".
108 | #html_title = None
109 |
110 | # A shorter title for the navigation bar. Default is the same as html_title.
111 | #html_short_title = None
112 |
113 | # The name of an image file (relative to this directory) to place at the top
114 | # of the sidebar.
115 | #html_logo = None
116 |
117 | # The name of an image file (within the static path) to use as favicon of the
118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
119 | # pixels large.
120 | #html_favicon = None
121 |
122 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
123 | # using the given strftime format.
124 | #html_last_updated_fmt = '%b %d, %Y'
125 |
126 | # If true, SmartyPants will be used to convert quotes and dashes to
127 | # typographically correct entities.
128 | #html_use_smartypants = True
129 |
130 | # Custom sidebar templates, maps document names to template names.
131 | #html_sidebars = {}
132 |
133 | # Additional templates that should be rendered to pages, maps page names to
134 | # template names.
135 | #html_additional_pages = {}
136 |
137 | # If false, no module index is generated.
138 | #html_domain_indices = True
139 |
140 | # If false, no index is generated.
141 | #html_use_index = True
142 |
143 | # If true, the index is split into individual pages for each letter.
144 | #html_split_index = False
145 |
146 | # If true, links to the reST sources are added to the pages.
147 | #html_show_sourcelink = True
148 |
149 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
150 | #html_show_sphinx = True
151 |
152 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
153 | #html_show_copyright = True
154 |
155 | # If true, an OpenSearch description file will be output, and all pages will
156 | # contain a tag referring to it. The value of this option must be the
157 | # base URL from which the finished HTML is served.
158 | #html_use_opensearch = ''
159 |
160 | # This is the file name suffix for HTML files (e.g. ".xhtml").
161 | #html_file_suffix = None
162 |
163 | # Output file base name for HTML help builder.
164 | htmlhelp_basename = 'python-oauth2doc'
165 |
166 |
167 | # -- Options for LaTeX output --------------------------------------------------
168 |
169 | latex_elements = {
170 | # The paper size ('letterpaper' or 'a4paper').
171 | #'papersize': 'letterpaper',
172 |
173 | # The font size ('10pt', '11pt' or '12pt').
174 | #'pointsize': '10pt',
175 |
176 | # Additional stuff for the LaTeX preamble.
177 | #'preamble': '',
178 | }
179 |
180 | # Grouping the document tree into LaTeX files. List of tuples
181 | # (source start file, target name, title, author, documentclass [howto/manual]).
182 | latex_documents = [
183 | ('index', 'python-oauth2.tex', u'python-oauth2 Documentation',
184 | u'Markus Meyer', 'manual'),
185 | ]
186 |
187 | # The name of an image file (relative to this directory) to place at the top of
188 | # the title page.
189 | #latex_logo = None
190 |
191 | # For "manual" documents, if this is true, then toplevel headings are parts,
192 | # not chapters.
193 | #latex_use_parts = False
194 |
195 | # If true, show page references after internal links.
196 | #latex_show_pagerefs = False
197 |
198 | # If true, show URL addresses after external links.
199 | #latex_show_urls = False
200 |
201 | # Documents to append as an appendix to all manuals.
202 | #latex_appendices = []
203 |
204 | # If false, no module index is generated.
205 | #latex_domain_indices = True
206 |
207 |
208 | # -- Options for manual page output --------------------------------------------
209 |
210 | # One entry per manual page. List of tuples
211 | # (source start file, name, description, authors, manual section).
212 | man_pages = [
213 | ('index', 'python-oauth2', u'python-oauth2 Documentation',
214 | [u'Markus Meyer'], 1)
215 | ]
216 |
217 | # If true, show URL addresses after external links.
218 | #man_show_urls = False
219 |
220 |
221 | # -- Options for Texinfo output ------------------------------------------------
222 |
223 | # Grouping the document tree into Texinfo files. List of tuples
224 | # (source start file, target name, title, author,
225 | # dir menu entry, description, category)
226 | texinfo_documents = [
227 | ('index', 'python-oauth2', u'python-oauth2 Documentation',
228 | u'Markus Meyer', 'python-oauth2', 'One line description of project.',
229 | 'Miscellaneous'),
230 | ]
231 |
232 | # Documents to append as an appendix to all manuals.
233 | #texinfo_appendices = []
234 |
235 | # If false, no module index is generated.
236 | #texinfo_domain_indices = True
237 |
238 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
239 | #texinfo_show_urls = 'footnote'
240 |
241 | # If true, do not generate a @detailmenu in the "Top" node's menu.
242 | #texinfo_no_detailmenu = False
243 |
--------------------------------------------------------------------------------
/docs/error.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.error`` --- Error classes
2 | ==================================
3 |
4 | .. automodule:: oauth2.error
5 |
6 | .. autoclass:: AccessTokenNotFound
7 |
8 | .. autoclass:: AuthCodeNotFound
9 |
10 | .. autoclass:: ClientNotFoundError
11 |
12 | .. autoclass:: OAuthBaseError
13 |
14 | .. autoclass:: OAuthInvalidError
15 |
16 | .. autoclass:: UserNotAuthenticated
17 |
--------------------------------------------------------------------------------
/docs/examples/authorization_code_grant.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 | import urllib
5 | import urlparse
6 | import json
7 | import signal
8 |
9 | from multiprocessing.process import Process
10 | from wsgiref.simple_server import make_server, WSGIRequestHandler
11 |
12 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../'))
13 |
14 | from oauth2 import Provider
15 | from oauth2.error import UserNotAuthenticated
16 | from oauth2.store.memory import ClientStore, TokenStore
17 | from oauth2.tokengenerator import Uuid4
18 | from oauth2.web import AuthorizationCodeGrantSiteAdapter
19 | from oauth2.web.wsgi import Application
20 | from oauth2.grant import AuthorizationCodeGrant
21 |
22 |
23 | logging.basicConfig(level=logging.DEBUG)
24 |
25 |
26 | class ClientRequestHandler(WSGIRequestHandler):
27 | """
28 | Request handler that enables formatting of the log messages on the console.
29 |
30 | This handler is used by the client application.
31 | """
32 | def address_string(self):
33 | return "client app"
34 |
35 |
36 | class OAuthRequestHandler(WSGIRequestHandler):
37 | """
38 | Request handler that enables formatting of the log messages on the console.
39 |
40 | This handler is used by the python-oauth2 application.
41 | """
42 | def address_string(self):
43 | return "python-oauth2"
44 |
45 |
46 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter):
47 | """
48 | This adapter renders a confirmation page so the user can confirm the auth
49 | request.
50 | """
51 |
52 | CONFIRMATION_TEMPLATE = """
53 |
54 |
55 |
56 | confirm
57 |
58 |
59 | deny
60 |
61 |
62 |
63 | """
64 |
65 | def render_auth_page(self, request, response, environ, scopes, client):
66 | url = request.path + "?" + request.query_string
67 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url)
68 |
69 | return response
70 |
71 | def authenticate(self, request, environ, scopes, client):
72 | if request.method == "GET":
73 | if request.get_param("confirm") == "1":
74 | return
75 | raise UserNotAuthenticated
76 |
77 | def user_has_denied_access(self, request):
78 | if request.method == "GET":
79 | if request.get_param("confirm") == "0":
80 | return True
81 | return False
82 |
83 |
84 | class ClientApplication(object):
85 | """
86 | Very basic application that simulates calls to the API of the
87 | python-oauth2 app.
88 | """
89 | callback_url = "http://localhost:8081/callback"
90 | client_id = "abc"
91 | client_secret = "xyz"
92 | api_server_url = "http://localhost:8080"
93 |
94 | def __init__(self):
95 | self.access_token = None
96 | self.auth_token = None
97 | self.token_type = ""
98 |
99 | def __call__(self, env, start_response):
100 | if env["PATH_INFO"] == "/app":
101 | status, body, headers = self._serve_application(env)
102 | elif env["PATH_INFO"] == "/callback":
103 | status, body, headers = self._read_auth_token(env)
104 | else:
105 | status = "301 Moved"
106 | body = ""
107 | headers = {"Location": "/app"}
108 |
109 | start_response(status,
110 | [(header, val) for header,val in headers.iteritems()])
111 | return body
112 |
113 | def _request_access_token(self):
114 | print("Requesting access token...")
115 |
116 | post_params = {"client_id": self.client_id,
117 | "client_secret": self.client_secret,
118 | "code": self.auth_token,
119 | "grant_type": "authorization_code",
120 | "redirect_uri": self.callback_url}
121 | token_endpoint = self.api_server_url + "/token"
122 |
123 | result = urllib.urlopen(token_endpoint,
124 | urllib.urlencode(post_params))
125 | content = ""
126 | for line in result:
127 | content += line
128 |
129 | result = json.loads(content)
130 | self.access_token = result["access_token"]
131 | self.token_type = result["token_type"]
132 |
133 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type)
134 | print(confirmation)
135 | return "302 Found", "", {"Location": "/app"}
136 |
137 | def _read_auth_token(self, env):
138 | print("Receiving authorization token...")
139 |
140 | query_params = urlparse.parse_qs(env["QUERY_STRING"])
141 |
142 | if "error" in query_params:
143 | location = "/app?error=" + query_params["error"][0]
144 | return "302 Found", "", {"Location": location}
145 |
146 | self.auth_token = query_params["code"][0]
147 |
148 | print("Received temporary authorization token '%s'" % (self.auth_token,))
149 |
150 | return "302 Found", "", {"Location": "/app"}
151 |
152 | def _request_auth_token(self):
153 | print("Requesting authorization token...")
154 |
155 | auth_endpoint = self.api_server_url + "/authorize"
156 | query = urllib.urlencode({"client_id": "abc",
157 | "redirect_uri": self.callback_url,
158 | "response_type": "code"})
159 |
160 | location = "%s?%s" % (auth_endpoint, query)
161 |
162 | return "302 Found", "", {"Location": location}
163 |
164 | def _serve_application(self, env):
165 | query_params = urlparse.parse_qs(env["QUERY_STRING"])
166 |
167 | if ("error" in query_params
168 | and query_params["error"][0] == "access_denied"):
169 | return "200 OK", "User has denied access", {}
170 |
171 | if self.access_token is None:
172 | if self.auth_token is None:
173 | return self._request_auth_token()
174 | else:
175 | return self._request_access_token()
176 | else:
177 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type)
178 | return "200 OK", str(confirmation), {}
179 |
180 |
181 | def run_app_server():
182 | app = ClientApplication()
183 |
184 | try:
185 | httpd = make_server('', 8081, app, handler_class=ClientRequestHandler)
186 |
187 | print("Starting Client app on http://localhost:8081/...")
188 | httpd.serve_forever()
189 | except KeyboardInterrupt:
190 | httpd.server_close()
191 |
192 |
193 | def run_auth_server():
194 | try:
195 | client_store = ClientStore()
196 | client_store.add_client(client_id="abc", client_secret="xyz",
197 | redirect_uris=["http://localhost:8081/callback"])
198 |
199 | token_store = TokenStore()
200 |
201 | provider = Provider(
202 | access_token_store=token_store,
203 | auth_code_store=token_store,
204 | client_store=client_store,
205 | token_generator=Uuid4())
206 | provider.add_grant(
207 | AuthorizationCodeGrant(site_adapter=TestSiteAdapter())
208 | )
209 |
210 | app = Application(provider=provider)
211 |
212 | httpd = make_server('', 8080, app, handler_class=OAuthRequestHandler)
213 |
214 | print("Starting OAuth2 server on http://localhost:8080/...")
215 | httpd.serve_forever()
216 | except KeyboardInterrupt:
217 | httpd.server_close()
218 |
219 |
220 | def main():
221 | auth_server = Process(target=run_auth_server)
222 | auth_server.start()
223 | app_server = Process(target=run_app_server)
224 | app_server.start()
225 | print("Access http://localhost:8081/app in your browser")
226 |
227 | def sigint_handler(signal, frame):
228 | print("Terminating servers...")
229 | auth_server.terminate()
230 | auth_server.join()
231 | app_server.terminate()
232 | app_server.join()
233 |
234 | signal.signal(signal.SIGINT, sigint_handler)
235 |
236 | if __name__ == "__main__":
237 | main()
238 |
--------------------------------------------------------------------------------
/docs/examples/base_server.py:
--------------------------------------------------------------------------------
1 | from wsgiref.simple_server import make_server
2 | import oauth2
3 | import oauth2.grant
4 | import oauth2.error
5 | import oauth2.store.memory
6 | import oauth2.tokengenerator
7 | import oauth2.web.wsgi
8 |
9 |
10 | # Create a SiteAdapter to interact with the user.
11 | # This can be used to display confirmation dialogs and the like.
12 | class ExampleSiteAdapter(oauth2.web.AuthorizationCodeGrantSiteAdapter,
13 | oauth2.web.ImplicitGrantSiteAdapter):
14 | def authenticate(self, request, environ, scopes, client):
15 | # Check if the user has granted access
16 | if request.post_param("confirm") == "confirm":
17 | return {}
18 |
19 | raise oauth2.error.UserNotAuthenticated
20 |
21 | def render_auth_page(self, request, response, environ, scopes, client):
22 | response.body = '''
23 |
24 |
25 |
29 |
30 | '''
31 | return response
32 |
33 | def user_has_denied_access(self, request):
34 | # Check if the user has denied access
35 | if request.post_param("deny") == "deny":
36 | return True
37 | return False
38 |
39 | # Create an in-memory storage to store your client apps.
40 | client_store = oauth2.store.memory.ClientStore()
41 | # Add a client
42 | client_store.add_client(client_id="abc", client_secret="xyz",
43 | redirect_uris=["http://localhost/callback"])
44 |
45 | site_adapter = ExampleSiteAdapter()
46 |
47 | # Create an in-memory storage to store issued tokens.
48 | # LocalTokenStore can store access and auth tokens
49 | token_store = oauth2.store.memory.TokenStore()
50 |
51 | # Create the controller.
52 | provider = oauth2.Provider(
53 | access_token_store=token_store,
54 | auth_code_store=token_store,
55 | client_store=client_store,
56 | token_generator=oauth2.tokengenerator.Uuid4()
57 | )
58 |
59 | # Add Grants you want to support
60 | provider.add_grant(oauth2.grant.AuthorizationCodeGrant(site_adapter=site_adapter))
61 | provider.add_grant(oauth2.grant.ImplicitGrant(site_adapter=site_adapter))
62 |
63 | # Add refresh token capability and set expiration time of access tokens
64 | # to 30 days
65 | provider.add_grant(oauth2.grant.RefreshToken(expires_in=2592000))
66 |
67 | # Wrap the controller with the Wsgi adapter
68 | app = oauth2.web.wsgi.Application(provider=provider)
69 |
70 | if __name__ == "__main__":
71 | httpd = make_server('', 8080, app)
72 | httpd.serve_forever()
73 |
--------------------------------------------------------------------------------
/docs/examples/client_credentials_grant.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import signal
4 |
5 | from multiprocessing.process import Process
6 | from wsgiref.simple_server import make_server, WSGIRequestHandler
7 |
8 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../'))
9 |
10 | from oauth2 import Provider
11 | from oauth2.store.memory import ClientStore, TokenStore
12 | from oauth2.tokengenerator import Uuid4
13 | from oauth2.web.wsgi import Application
14 | from oauth2.grant import ClientCredentialsGrant
15 |
16 |
17 | class OAuthRequestHandler(WSGIRequestHandler):
18 | """
19 | Request handler that enables formatting of the log messages on the console.
20 |
21 | This handler is used by the python-oauth2 application.
22 | """
23 | def address_string(self):
24 | return "python-oauth2"
25 |
26 |
27 | def run_auth_server():
28 | try:
29 | client_store = ClientStore()
30 | client_store.add_client(client_id="abc", client_secret="xyz",
31 | redirect_uris=[])
32 |
33 | token_store = TokenStore()
34 | token_gen = Uuid4()
35 | token_gen.expires_in['client_credentials'] = 3600
36 |
37 | auth_controller = Provider(
38 | access_token_store=token_store,
39 | auth_code_store=token_store,
40 | client_store=client_store,
41 | token_generator=token_gen)
42 | auth_controller.add_grant(ClientCredentialsGrant())
43 |
44 | app = Application(provider=auth_controller)
45 |
46 | httpd = make_server('', 8080, app, handler_class=OAuthRequestHandler)
47 |
48 | print("Starting implicit_grant oauth2 server on http://localhost:8080/...")
49 | httpd.serve_forever()
50 | except KeyboardInterrupt:
51 | httpd.server_close()
52 |
53 | def main():
54 | auth_server = Process(target=run_auth_server)
55 | auth_server.start()
56 | print("To test getting an auth token, execute the following curl command:")
57 | print(
58 | "curl --ipv4 -v -X POST"
59 | " -d 'grant_type=client_credentials&client_id=abc&client_secret=xyz' "
60 | "http://localhost:8080/token"
61 | )
62 |
63 | def sigint_handler(signal, frame):
64 | print("Terminating server...")
65 | auth_server.terminate()
66 | auth_server.join()
67 |
68 | signal.signal(signal.SIGINT, sigint_handler)
69 |
70 | if __name__ == "__main__":
71 | main()
72 |
--------------------------------------------------------------------------------
/docs/examples/implicit_grant.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import signal
4 | import sys
5 |
6 | from multiprocessing import Process
7 | from wsgiref.simple_server import make_server
8 |
9 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../'))
10 |
11 | from oauth2 import Provider
12 | from oauth2.error import UserNotAuthenticated
13 | from oauth2.web import ImplicitGrantSiteAdapter
14 | from oauth2.web.wsgi import Application
15 | from oauth2.tokengenerator import Uuid4
16 | from oauth2.grant import ImplicitGrant
17 | from oauth2.store.memory import ClientStore, TokenStore
18 |
19 |
20 | logging.basicConfig(level=logging.DEBUG)
21 |
22 |
23 | class TestSiteAdapter(ImplicitGrantSiteAdapter):
24 | CONFIRMATION_TEMPLATE = """
25 |
26 |
27 |
28 | confirm
29 |
30 |
31 | deny
32 |
33 |
34 |
35 | """
36 |
37 | def render_auth_page(self, request, response, environ, scopes, client):
38 | url = request.path + "?" + request.query_string
39 | # Add check if the user is logged or a redirect to the login page here
40 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url)
41 |
42 | return response
43 |
44 | def authenticate(self, request, environ, scopes, client):
45 | if request.method == "GET":
46 | if request.get_param("confirm") == "1":
47 | return
48 | raise UserNotAuthenticated
49 |
50 | def user_has_denied_access(self, request):
51 | if request.method == "GET":
52 | if request.get_param("confirm") == "0":
53 | return True
54 | return False
55 |
56 |
57 | def run_app_server():
58 | def application(env, start_response):
59 | """
60 | Serves the local javascript client
61 | """
62 |
63 | js_app = """
64 |
65 |
66 | OAuth2 JS Test App
67 |
68 |
69 |
97 |
98 |
99 | """
100 |
101 | start_response("200 OK", [("Content-Type", "text/html")])
102 |
103 | return [js_app]
104 |
105 | try:
106 | httpd = make_server('', 8081, application)
107 |
108 | print("Starting implicit_grant app server on http://localhost:8081/...")
109 | httpd.serve_forever()
110 | except KeyboardInterrupt:
111 | httpd.server_close()
112 |
113 |
114 | def run_auth_server():
115 | try:
116 | client_store = ClientStore()
117 | client_store.add_client(client_id="abc", client_secret="xyz",
118 | redirect_uris=["http://localhost:8081/"])
119 |
120 | token_store = TokenStore()
121 |
122 | provider = Provider(
123 | access_token_store=token_store,
124 | auth_code_store=token_store,
125 | client_store=client_store,
126 | token_generator=Uuid4())
127 | provider.add_grant(ImplicitGrant(site_adapter=TestSiteAdapter()))
128 |
129 | app = Application(provider=provider)
130 |
131 | httpd = make_server('', 8080, app)
132 |
133 | print("Starting implicit_grant oauth2 server on http://localhost:8080/...")
134 | httpd.serve_forever()
135 | except KeyboardInterrupt:
136 | httpd.server_close()
137 |
138 |
139 | def main():
140 | auth_server = Process(target=run_auth_server)
141 | auth_server.start()
142 | app_server = Process(target=run_app_server)
143 | app_server.start()
144 | print("Access http://localhost:8081/ to start the auth flow")
145 |
146 | def sigint_handler(signal, frame):
147 | print("Terminating servers...")
148 | auth_server.terminate()
149 | auth_server.join()
150 | app_server.terminate()
151 | app_server.join()
152 |
153 | signal.signal(signal.SIGINT, sigint_handler)
154 |
155 | if __name__ == "__main__":
156 | main()
157 |
--------------------------------------------------------------------------------
/docs/examples/pyramid/README.md:
--------------------------------------------------------------------------------
1 | Pyramid integration example for python-oauth2
2 |
3 | Integrate the example:
4 |
5 | 1. Put classes in base.py in appropriate packages.
6 | 2. impl.py contains controller and site adapter. Also place both of them in appropriate packages.
7 | 3. Implement "password_auth" method in OAuth2SiteAdapter.
8 | 4. Modify "_get_token_store" and "_get_client_store" methods in UserAuthController
9 | 5. Add "config.add_route('authenticateUser', '/user/token')" to "\__init__\.py"
10 |
11 | Add new grant-type:
12 |
13 | 1. Implement auth method like "password_auth" in OAuth2SiteAdapter.
14 | 2. Call "add_grant" on your AuthController
15 |
16 |
17 | Will add working pyramid project soon.
18 |
19 |
--------------------------------------------------------------------------------
/docs/examples/pyramid/base.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | from pyramid.response import Response as PyramidResponse
4 | from oauth2.web import Response
5 | from oauth2.error import OAuthInvalidError, \
6 | ClientNotFoundError, OAuthInvalidNoRedirectError, UnsupportedGrantError, ParameterMissingError
7 | from oauth2.client_authenticator import ClientAuthenticator, request_body
8 | from oauth2.tokengenerator import Uuid4
9 |
10 |
11 | class Request():
12 | """
13 | Contains data of the current HTTP request.
14 | """
15 | def __init__(self, env):
16 | self.method = env.method
17 | self.params = env.json_body
18 | self.registry = env.registry
19 | self.headers = env.registry
20 |
21 | def post_param(self, name):
22 | return self.params.get(name)
23 |
24 |
25 | class BaseAuthController(object):
26 |
27 | def __init__(self, request, site_adapter):
28 | self.request = Request(request)
29 | self.site_adapter = site_adapter
30 | self.token_generator = Uuid4()
31 |
32 | self.client_store = self._get_client_store()
33 | self.access_token_store = self._get_token_store()
34 |
35 | self.client_authenticator = ClientAuthenticator(
36 | client_store=self.client_store,
37 | source=request_body
38 | )
39 |
40 | self.grant_types = [];
41 |
42 |
43 | @classmethod
44 | def _get_token_store(cls):
45 | NotImplementedError
46 |
47 | @classmethod
48 | def _get_client_store(cls):
49 | NotImplementedError
50 |
51 | def add_grant(self, grant):
52 | """
53 | Adds a Grant that the provider should support.
54 |
55 | :param grant: An instance of a class that extends
56 | :class:`oauth2.grant.GrantHandlerFactory`
57 | """
58 | if hasattr(grant, "expires_in"):
59 | self.token_generator.expires_in[grant.grant_type] = grant.expires_in
60 |
61 | if hasattr(grant, "refresh_expires_in"):
62 | self.token_generator.refresh_expires_in = grant.refresh_expires_in
63 |
64 | self.grant_types.append(grant)
65 |
66 |
67 | def _determine_grant_type(self, request):
68 | for grant in self.grant_types:
69 | grant_handler = grant(request, self)
70 | if grant_handler is not None:
71 | return grant_handler
72 | raise UnsupportedGrantError
73 |
74 |
75 | def authenticate(self):
76 | response = Response()
77 | grant_type = self._determine_grant_type(self.request)
78 | grant_type.read_validate_params(self.request)
79 | grant_type.process(self.request, response, {})
80 | return PyramidResponse(body=response.body, status=response.status_code, content_type="application/json")
81 |
--------------------------------------------------------------------------------
/docs/examples/pyramid/impl.py:
--------------------------------------------------------------------------------
1 |
2 | from pyramid.view import view_config
3 |
4 | from oauth2.error import UserNotAuthenticated, UserNotExist
5 | from oauth2.web import SiteAdapter
6 | from oauth2.store.redisdb import TokenStore, ClientStore
7 | from oauth2.grant import ResourceOwnerGrant
8 |
9 | from base import BaseAuthController
10 |
11 | import os
12 | import sys
13 | import pyramid
14 |
15 | class OAuth2SiteAdapter(SiteAdapter):
16 |
17 | def authenticate(self, request, environ, scopes):
18 | if request.method == "POST":
19 | if request.post_param("grant_type") == 'password':
20 | return self.password_auth(request)
21 | raise UserNotAuthenticated
22 |
23 | def user_has_denied_access(self, request):
24 | if request.method == "POST":
25 | if request.post_param("confirm") is "0":
26 | return True
27 | return False
28 |
29 | # implement this for resource owner grant
30 | def password_auth(self, request):
31 | session = DBSession()
32 | try:
33 | #validate user credentials
34 | user_id = 123
35 | if True:
36 | return None, user_id
37 | raise UserNotAuthenticated
38 | except:
39 | raise
40 |
41 |
42 | class UserAuthController(BaseAuthController):
43 |
44 | def __init__(self, request):
45 | super(UserAuthController, self).__init__(request, OAuth2SiteAdapter())
46 | self.add_grant(ResourceOwnerGrant(unique_token=True))
47 |
48 | @classmethod
49 | def _get_token_store(cls):
50 | settings = get_current_registry().settings
51 | return TokenStore(
52 | host = 127.0.0.1,
53 | port = 6379,
54 | db = 1,
55 | )
56 |
57 | @classmethod
58 | def _get_client_store(cls):
59 | settings = get_current_registry().settings
60 | return ClientStore(
61 | host = 127.0.0.1,
62 | port = 6379,
63 | db = 2,
64 | )
65 |
66 | # add this route in __init__.py
67 | @view_config(route_name="authenticateUser", renderer="json", request_method="POST")
68 | def authenticate(self):
69 | return super(UserAuthController, self).authenticate()
70 |
71 |
--------------------------------------------------------------------------------
/docs/examples/resource_owner_grant.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import signal
5 | import sys
6 | import urllib2
7 |
8 | from multiprocessing.process import Process
9 | from urllib2 import HTTPError
10 | from wsgiref.simple_server import make_server
11 |
12 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../'))
13 |
14 | from oauth2.compatibility import parse_qs, urlencode
15 | from oauth2 import Provider
16 | from oauth2.error import UserNotAuthenticated
17 | from oauth2.store.memory import ClientStore, TokenStore
18 | from oauth2.tokengenerator import Uuid4
19 | from oauth2.web import ResourceOwnerGrantSiteAdapter
20 | from oauth2.web.wsgi import Application
21 | from oauth2.grant import ResourceOwnerGrant
22 |
23 |
24 | logging.basicConfig(level=logging.DEBUG)
25 |
26 |
27 | class ClientApplication(object):
28 | """
29 | Very basic application that simulates calls to the API of the
30 | python-oauth2 app.
31 | """
32 | client_id = "abc"
33 | client_secret = "xyz"
34 | token_endpoint = "http://localhost:8080/token"
35 |
36 | LOGIN_TEMPLATE = """
37 |
38 | Test Login
39 |
40 | {failed_message}
41 |
42 |
53 |
54 | """
55 |
56 | SERVER_ERROR_TEMPLATE = """
57 |
58 | OAuth2 server responded with an error
59 | Error type: {error_type}
60 | Error description: {error_description}
61 |
62 | """
63 |
64 | TOKEN_TEMPLATE = """
65 |
66 | Access token: {access_token}
67 |
70 |
71 | """
72 |
73 | def __init__(self):
74 | self.token = None
75 | self.token_type = ""
76 |
77 | def __call__(self, env, start_response):
78 | if env["PATH_INFO"] == "/login":
79 | status, body, headers = self._login(failed=env["QUERY_STRING"] == "failed=1")
80 | elif env["PATH_INFO"] == "/":
81 | status, body, headers = self._display_token()
82 | elif env["PATH_INFO"] == "/request_token":
83 | status, body, headers = self._request_token(env)
84 | elif env["PATH_INFO"] == "/reset":
85 | status, body, headers = self._reset()
86 | else:
87 | status = "301 Moved"
88 | body = ""
89 | headers = {"Location": "/"}
90 |
91 | start_response(status,
92 | [(header, val) for header,val in headers.iteritems()])
93 | return body
94 |
95 | def _display_token(self):
96 | """
97 | Display token information or redirect to login prompt if none is
98 | available.
99 | """
100 | if self.token is None:
101 | return "301 Moved", "", {"Location": "/login"}
102 |
103 | return ("200 OK",
104 | self.TOKEN_TEMPLATE.format(
105 | access_token=self.token["access_token"]),
106 | {"Content-Type": "text/html"})
107 |
108 | def _login(self, failed=False):
109 | """
110 | Login prompt
111 | """
112 | if failed:
113 | content = self.LOGIN_TEMPLATE.format(failed_message="Login failed")
114 | else:
115 | content = self.LOGIN_TEMPLATE.format(failed_message="")
116 | return "200 OK", content, {"Content-Type": "text/html"}
117 |
118 | def _request_token(self, env):
119 | """
120 | Retrieves a new access token from the OAuth2 server.
121 | """
122 | params = {}
123 |
124 | content = env['wsgi.input'].read(int(env['CONTENT_LENGTH']))
125 | post_params = parse_qs(content)
126 | # Convert to dict for easier access
127 | for param, value in post_params.items():
128 | decoded_param = param.decode('utf-8')
129 | decoded_value = value[0].decode('utf-8')
130 | if decoded_param == "username" or decoded_param == "password":
131 | params[decoded_param] = decoded_value
132 |
133 | params["grant_type"] = "password"
134 | params["client_id"] = self.client_id
135 | params["client_secret"] = self.client_secret
136 | # Request an access token by POSTing a request to the auth server.
137 | try:
138 | response = urllib2.urlopen(self.token_endpoint, urlencode(params))
139 | except HTTPError, he:
140 | if he.code == 400:
141 | error_body = json.loads(he.read())
142 | body = self.SERVER_ERROR_TEMPLATE\
143 | .format(error_type=error_body["error"],
144 | error_description=error_body["error_description"])
145 | return "400 Bad Request", body, {"Content-Type": "text/html"}
146 | if he.code == 401:
147 | return "302 Found", "", {"Location": "/login?failed=1"}
148 |
149 | self.token = json.load(response)
150 |
151 | return "301 Moved", "", {"Location": "/"}
152 |
153 | def _reset(self):
154 | self.token = None
155 |
156 | return "302 Found", "", {"Location": "/login"}
157 |
158 |
159 | class TestSiteAdapter(ResourceOwnerGrantSiteAdapter):
160 | def authenticate(self, request, environ, scopes, client):
161 | username = request.post_param("username")
162 | password = request.post_param("password")
163 | # A real world application could connect to a database, try to
164 | # retrieve username and password and compare them against the input
165 | if username == "foo" and password == "bar":
166 | return
167 |
168 | raise UserNotAuthenticated
169 |
170 |
171 | def run_app_server():
172 | app = ClientApplication()
173 |
174 | try:
175 | httpd = make_server('', 8081, app)
176 |
177 | print("Starting Client app on http://localhost:8081/...")
178 | httpd.serve_forever()
179 | except KeyboardInterrupt:
180 | httpd.server_close()
181 |
182 |
183 | def run_auth_server():
184 | try:
185 | client_store = ClientStore()
186 | client_store.add_client(client_id="abc", client_secret="xyz",
187 | redirect_uris=[])
188 |
189 | token_store = TokenStore()
190 |
191 | provider = Provider(
192 | access_token_store=token_store,
193 | auth_code_store=token_store,
194 | client_store=client_store,
195 | token_generator=Uuid4())
196 |
197 | provider.add_grant(
198 | ResourceOwnerGrant(site_adapter=TestSiteAdapter())
199 | )
200 |
201 | app = Application(provider=provider)
202 |
203 | httpd = make_server('', 8080, app)
204 |
205 | print("Starting OAuth2 server on http://localhost:8080/...")
206 | httpd.serve_forever()
207 | except KeyboardInterrupt:
208 | httpd.server_close()
209 |
210 |
211 | def main():
212 | auth_server = Process(target=run_auth_server)
213 | auth_server.start()
214 | app_server = Process(target=run_app_server)
215 | app_server.start()
216 | print("Visit http://localhost:8081/ in your browser")
217 |
218 | def sigint_handler(signal, frame):
219 | print("Terminating servers...")
220 | auth_server.terminate()
221 | auth_server.join()
222 | app_server.terminate()
223 | app_server.join()
224 |
225 | signal.signal(signal.SIGINT, sigint_handler)
226 |
227 | if __name__ == "__main__":
228 | main()
229 |
--------------------------------------------------------------------------------
/docs/examples/tornado_server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json
3 | import os
4 | import signal
5 | import sys
6 | import urllib
7 | import urlparse
8 |
9 | from multiprocessing.process import Process
10 |
11 | sys.path.insert(0, os.path.abspath(os.path.realpath(__file__) + '/../../../'))
12 |
13 | from oauth2 import Provider
14 | from oauth2.error import UserNotAuthenticated
15 | from oauth2.grant import AuthorizationCodeGrant
16 | from oauth2.tokengenerator import Uuid4
17 | from oauth2.store.memory import ClientStore, TokenStore
18 | from oauth2.web import AuthorizationCodeGrantSiteAdapter
19 | from oauth2.web.tornado import OAuth2Handler
20 | from tornado.ioloop import IOLoop
21 | from tornado.web import Application, url
22 | from wsgiref.simple_server import make_server, WSGIRequestHandler
23 |
24 |
25 | logging.basicConfig(level=logging.DEBUG)
26 |
27 |
28 | class ClientRequestHandler(WSGIRequestHandler):
29 | """
30 | Request handler that enables formatting of the log messages on the console.
31 |
32 | This handler is used by the client application.
33 | """
34 | def address_string(self):
35 | return "client app"
36 |
37 |
38 | class OAuthRequestHandler(WSGIRequestHandler):
39 | """
40 | Request handler that enables formatting of the log messages on the console.
41 |
42 | This handler is used by the python-oauth2 application.
43 | """
44 | def address_string(self):
45 | return "python-oauth2"
46 |
47 |
48 | class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter):
49 | """
50 | This adapter renders a confirmation page so the user can confirm the auth
51 | request.
52 | """
53 |
54 | CONFIRMATION_TEMPLATE = """
55 |
56 |
57 |
58 | confirm
59 |
60 |
61 | deny
62 |
63 |
64 |
65 | """
66 |
67 | def render_auth_page(self, request, response, environ, scopes, client):
68 | url = request.path + "?" + request.query_string
69 | response.body = self.CONFIRMATION_TEMPLATE.format(url=url)
70 |
71 | return response
72 |
73 | def authenticate(self, request, environ, scopes, client):
74 | if request.method == "GET":
75 | if request.get_param("confirm") == "1":
76 | return
77 | raise UserNotAuthenticated
78 |
79 | def user_has_denied_access(self, request):
80 | if request.method == "GET":
81 | if request.get_param("confirm") == "0":
82 | return True
83 | return False
84 |
85 |
86 | class ClientApplication(object):
87 | """
88 | Very basic application that simulates calls to the API of the
89 | python-oauth2 app.
90 | """
91 | callback_url = "http://localhost:8081/callback"
92 | client_id = "abc"
93 | client_secret = "xyz"
94 | api_server_url = "http://localhost:8080"
95 |
96 | def __init__(self):
97 | self.access_token = None
98 | self.auth_token = None
99 | self.token_type = ""
100 |
101 | def __call__(self, env, start_response):
102 | if env["PATH_INFO"] == "/app":
103 | status, body, headers = self._serve_application(env)
104 | elif env["PATH_INFO"] == "/callback":
105 | status, body, headers = self._read_auth_token(env)
106 | else:
107 | status = "301 Moved"
108 | body = ""
109 | headers = {"Location": "/app"}
110 |
111 | start_response(status,
112 | [(header, val) for header,val in headers.iteritems()])
113 | return body
114 |
115 | def _request_access_token(self):
116 | print("Requesting access token...")
117 |
118 | post_params = {"client_id": self.client_id,
119 | "client_secret": self.client_secret,
120 | "code": self.auth_token,
121 | "grant_type": "authorization_code",
122 | "redirect_uri": self.callback_url}
123 | token_endpoint = self.api_server_url + "/token"
124 |
125 | result = urllib.urlopen(token_endpoint,
126 | urllib.urlencode(post_params))
127 | content = ""
128 | for line in result:
129 | content += line
130 |
131 | result = json.loads(content)
132 | self.access_token = result["access_token"]
133 | self.token_type = result["token_type"]
134 |
135 | confirmation = "Received access token '%s' of type '%s'" % (self.access_token, self.token_type)
136 | print(confirmation)
137 | return "302 Found", "", {"Location": "/app"}
138 |
139 | def _read_auth_token(self, env):
140 | print("Receiving authorization token...")
141 |
142 | query_params = urlparse.parse_qs(env["QUERY_STRING"])
143 |
144 | if "error" in query_params:
145 | location = "/app?error=" + query_params["error"][0]
146 | return "302 Found", "", {"Location": location}
147 |
148 | self.auth_token = query_params["code"][0]
149 |
150 | print("Received temporary authorization token '%s'" % (self.auth_token,))
151 |
152 | return "302 Found", "", {"Location": "/app"}
153 |
154 | def _request_auth_token(self):
155 | print("Requesting authorization token...")
156 |
157 | auth_endpoint = self.api_server_url + "/authorize"
158 | query = urllib.urlencode({"client_id": "abc",
159 | "redirect_uri": self.callback_url,
160 | "response_type": "code"})
161 |
162 | location = "%s?%s" % (auth_endpoint, query)
163 |
164 | return "302 Found", "", {"Location": location}
165 |
166 | def _serve_application(self, env):
167 | query_params = urlparse.parse_qs(env["QUERY_STRING"])
168 |
169 | if ("error" in query_params
170 | and query_params["error"][0] == "access_denied"):
171 | return "200 OK", "User has denied access", {}
172 |
173 | if self.access_token is None:
174 | if self.auth_token is None:
175 | return self._request_auth_token()
176 | else:
177 | return self._request_access_token()
178 | else:
179 | confirmation = "Current access token '%s' of type '%s'" % (self.access_token, self.token_type)
180 | return "200 OK", str(confirmation), {}
181 |
182 |
183 | def run_app_server():
184 | app = ClientApplication()
185 |
186 | try:
187 | httpd = make_server('', 8081, app, handler_class=ClientRequestHandler)
188 |
189 | print("Starting Client app on http://localhost:8081/...")
190 | httpd.serve_forever()
191 | except KeyboardInterrupt:
192 | httpd.server_close()
193 |
194 |
195 | def run_auth_server():
196 | client_store = ClientStore()
197 | client_store.add_client(client_id="abc", client_secret="xyz",
198 | redirect_uris=["http://localhost:8081/callback"])
199 |
200 | token_store = TokenStore()
201 |
202 | provider = Provider(access_token_store=token_store,
203 | auth_code_store=token_store, client_store=client_store,
204 | token_generator=Uuid4())
205 | provider.add_grant(AuthorizationCodeGrant(site_adapter=TestSiteAdapter()))
206 |
207 | try:
208 | app = Application([
209 | url(provider.authorize_path, OAuth2Handler, dict(provider=provider)),
210 | url(provider.token_path, OAuth2Handler, dict(provider=provider)),
211 | ])
212 |
213 | app.listen(8080)
214 | print("Starting OAuth2 server on http://localhost:8080/...")
215 | IOLoop.current().start()
216 |
217 | except KeyboardInterrupt:
218 | IOLoop.close()
219 |
220 |
221 | def main():
222 | auth_server = Process(target=run_auth_server)
223 | auth_server.start()
224 | app_server = Process(target=run_app_server)
225 | app_server.start()
226 | print("Access http://localhost:8081/app in your browser")
227 |
228 | def sigint_handler(signal, frame):
229 | print("Terminating servers...")
230 | auth_server.terminate()
231 | auth_server.join()
232 | app_server.terminate()
233 | app_server.join()
234 |
235 | signal.signal(signal.SIGINT, sigint_handler)
236 |
237 | if __name__ == "__main__":
238 | main()
239 |
--------------------------------------------------------------------------------
/docs/frameworks.rst:
--------------------------------------------------------------------------------
1 | Using ``python-oauth2`` with other frameworks
2 | =============================================
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | tornado.rst
8 |
--------------------------------------------------------------------------------
/docs/grant.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.grant`` --- Grant classes and helpers
2 | ==============================================
3 |
4 | .. automodule:: oauth2.grant
5 |
6 | Helpers and base classes
7 | ------------------------
8 |
9 | .. autoclass:: GrantHandlerFactory
10 |
11 | .. autoclass:: ScopeGrant
12 |
13 | .. autoclass:: Scope
14 | :members: parse
15 |
16 | .. autoclass:: SiteAdapterMixin
17 |
18 | Grant classes
19 | -------------
20 |
21 | .. autoclass:: AuthorizationCodeGrant
22 | :show-inheritance:
23 |
24 | .. autoclass:: ImplicitGrant
25 | :show-inheritance:
26 |
27 | .. autoclass:: ResourceOwnerGrant
28 | :show-inheritance:
29 |
30 | .. autoclass:: RefreshToken
31 | :show-inheritance:
32 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. title:: python-oauth2
2 |
3 | .. automodule:: oauth2
4 |
5 | Contents:
6 |
7 | .. toctree::
8 | :maxdepth: 2
9 |
10 | grant.rst
11 | store.rst
12 | oauth2.rst
13 | web.rst
14 | client_authenticator.rst
15 | token_generator.rst
16 | log.rst
17 | error.rst
18 | unique_token.rst
19 | migration.rst
20 | frameworks.rst
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :ref:`search`
28 |
--------------------------------------------------------------------------------
/docs/log.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.log`` --- Logging
2 | ==========================
3 |
4 | .. automodule:: oauth2.log
5 |
--------------------------------------------------------------------------------
/docs/migration.rst:
--------------------------------------------------------------------------------
1 | Migration
2 | =========
3 |
4 | 0.7.0 -> 1.0.0
5 | --------------
6 |
7 | One site adapter per grant
8 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
9 |
10 | Starting from ``1.0.0``, the grants
11 | :class:`oauth2.grant.AuthorizationCodeGrant`,
12 | :class:`oauth2.grant.ImplicitGrant` and
13 | :class:`oauth2.grant.ResourceOwnerGrant` expect the parameter ``site_adapter``
14 | to be passed to them.
15 |
16 | :class:`oauth2.Provider` does not accept the parameter ``site_adapter``
17 | anymore.
18 |
19 | The base class ``oauth2.web.SiteAdapter`` does not exist anymore.
20 |
21 | Code that looks like this in version ``0.7.0``
22 |
23 | .. code-block:: python
24 |
25 | from oauth2 import Provider
26 | from oauth2.web import SiteAdapter
27 | from oauth2.grant import AuthorizationCodeGrant
28 |
29 | class ExampleSiteAdapter(SiteAdapter):
30 | ...
31 |
32 | provider = Provider(
33 | ...,
34 | site_adapter=ExampleSiteAdapter(),
35 | ...
36 | )
37 | provider.add_grant(AuthorizationCodeGrant())
38 |
39 | has to be rewritten to look similar to the following
40 |
41 | .. code-block:: python
42 |
43 | from oauth2 import Provider
44 | from oauth2.web import AuthorizationCodeGrantSiteAdapter
45 | from oauth2.grant import AuthorizationCodeGrant
46 |
47 | class ExampleSiteAdapter(AuthorizationCodeGrantSiteAdapter):
48 | # Override the methods defined in AuthorizationCodeGrantSiteAdapter to suite your needs
49 | ...
50 |
51 | # No site_adapter anymore
52 | provider = Provider(...)
53 |
54 | provider.add_grant(AuthorizationCodeGrant(site_adapter=ExampleSiteAdapter()))
55 |
56 |
57 | WSGI adapter classes refactoring
58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | All code to connect ``python-oauth2`` with a WSGI compliant server has been
61 | moved to the module ``oauth2.web.wsgi``.
62 |
63 | Also the class ``Wsgi`` has been renamed to ``Application`` and now expects
64 | the parameter ``provider`` instead of ``server``.
65 |
66 | Before:
67 |
68 | .. code-block:: python
69 |
70 | from oauth2.web import Wsgi
71 |
72 | # Instantiating storage and provider...
73 |
74 | app = Wsgi(server=provider)
75 |
76 |
77 | After:
78 |
79 | .. code-block:: python
80 |
81 | from oauth2.web.wsgi import Application
82 |
83 | # Instantiating storage and provider...
84 |
85 | app = Application(provider=provider)
86 |
87 |
88 | Client passed to methods authenticate and render_auth_page of a Site Adapter
89 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90 |
91 | Before:
92 |
93 | .. code-block:: python
94 |
95 | class ExampleSiteAdapter(AuthenticatingSiteAdapter, UserFacingSiteAdapter):
96 | def authenticate(self, request, environ, scopes):
97 | # code
98 |
99 | def render_auth_page(self, request, response, environ, scopes):
100 | # code
101 |
102 |
103 | After:
104 |
105 | .. code-block:: python
106 |
107 | class ExampleSiteAdapter(AuthenticatingSiteAdapter, UserFacingSiteAdapter):
108 | def authenticate(self, request, environ, scopes, client):
109 | # code
110 |
111 | def render_auth_page(self, request, response, environ, scopes, client):
112 | # code
113 |
114 |
115 |
--------------------------------------------------------------------------------
/docs/oauth2.rst:
--------------------------------------------------------------------------------
1 | ``oauth2`` --- Provider Class
2 | =============================
3 |
4 | .. autoclass:: oauth2.Provider
5 | :members: scope_separator, add_grant, dispatch, enable_unique_tokens
6 |
--------------------------------------------------------------------------------
/docs/store.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store`` --- Storing and retrieving data
2 | ================================================
3 |
4 | .. automodule:: oauth2.store
5 |
6 | Data Types
7 | ----------
8 |
9 | .. autoclass:: oauth2.datatype.AccessToken
10 |
11 | .. autoclass:: oauth2.datatype.AuthorizationCode
12 |
13 | .. autoclass:: oauth2.datatype.Client
14 |
15 | Base Classes
16 | ------------
17 |
18 | .. autoclass:: AccessTokenStore
19 | :members:
20 |
21 | .. autoclass:: AuthCodeStore
22 | :members:
23 |
24 | .. autoclass:: ClientStore
25 | :members:
26 |
27 | Implementations
28 | ---------------
29 |
30 | .. toctree::
31 | :maxdepth: 2
32 |
33 | store/memcache.rst
34 | store/memory.rst
35 | store/mongodb.rst
36 | store/redisdb.rst
37 | store/dbapi.rst
38 | store/mysql.rst
39 |
--------------------------------------------------------------------------------
/docs/store/dbapi.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.dbapi`` --- PEP249 compatible stores
2 | ===================================================
3 |
4 | .. automodule:: oauth2.store.dbapi
5 |
6 | .. autoclass:: oauth2.store.dbapi.DatabaseStore
7 |
8 | .. autoclass:: oauth2.store.dbapi.DbApiAccessTokenStore
9 | :members:
10 |
11 | .. autoclass:: oauth2.store.dbapi.DbApiAuthCodeStore
12 | :members:
13 |
14 | .. autoclass:: oauth2.store.dbapi.DbApiClientStore
15 | :members:
16 |
--------------------------------------------------------------------------------
/docs/store/memcache.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.memcache`` --- Memcache store adapters
2 | =====================================================
3 |
4 | .. automodule:: oauth2.store.memcache
5 |
6 | .. autoclass:: TokenStore
7 |
--------------------------------------------------------------------------------
/docs/store/memory.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.memory`` --- In-memory store adapters
2 | =====================================================
3 |
4 | .. automodule:: oauth2.store.memory
5 |
6 | .. autoclass:: ClientStore
7 | :members:
8 |
9 | .. autoclass:: TokenStore
10 | :members:
11 |
--------------------------------------------------------------------------------
/docs/store/mongodb.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.mongodb`` --- Mongodb store adapters
2 | ===================================================
3 |
4 | .. automodule:: oauth2.store.mongodb
5 |
6 | .. autoclass:: oauth2.store.mongodb.MongodbStore
7 |
8 | .. autoclass:: oauth2.store.mongodb.AccessTokenStore
9 |
10 | .. autoclass:: oauth2.store.mongodb.AuthCodeStore
11 |
12 | .. autoclass:: oauth2.store.mongodb.ClientStore
13 |
--------------------------------------------------------------------------------
/docs/store/mysql.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.dbapi.mysql`` --- Mysql store adapters
2 | =====================================================
3 |
4 | .. automodule:: oauth2.store.dbapi.mysql
5 |
6 | .. autoclass:: MysqlAccessTokenStore
7 |
8 | .. autoclass:: MysqlAuthCodeStore
9 |
10 | .. autoclass:: MysqlClientStore
11 |
--------------------------------------------------------------------------------
/docs/store/redisdb.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.store.redisdb`` --- Redis store adapters
2 | =================================================
3 |
4 | .. automodule:: oauth2.store.redisdb
5 |
6 | .. autoclass:: oauth2.store.redisdb.TokenStore
7 |
8 | .. autoclass:: oauth2.store.redisdb.ClientStore
9 |
--------------------------------------------------------------------------------
/docs/token_generator.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.tokengenerator`` --- Generate Tokens
2 | =============================================
3 |
4 | .. automodule:: oauth2.tokengenerator
5 |
6 | Base Class
7 | ----------
8 |
9 | .. autoclass:: TokenGenerator
10 | :members:
11 |
12 | Implementations
13 | ---------------
14 |
15 | .. autoclass:: URandomTokenGenerator
16 | :members:
17 | :show-inheritance:
18 |
19 | .. autoclass:: Uuid4
20 | :members:
21 | :show-inheritance:
22 |
--------------------------------------------------------------------------------
/docs/tornado.rst:
--------------------------------------------------------------------------------
1 | Tornado
2 | =======
3 |
4 | .. automodule:: oauth2.web.tornado
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/unique_token.rst:
--------------------------------------------------------------------------------
1 | Unique Access Tokens
2 | ====================
3 |
4 | This page explains the concepts of unique access tokens and how to enable this
5 | feature.
6 |
7 | What are unique access tokens?
8 | ------------------------------
9 |
10 | When the use of unique access tokens is enabled the Provider will respond with
11 | an existing access token to subsequent requests of a client instead of issuing
12 | a new token on each request.
13 |
14 | An existing access token will be returned if the following conditions are
15 | met:
16 |
17 | * The access token has been issued for the requesting client
18 | * The access token has been issued for the same user as in the current request
19 | * The requested scope is the same as in the existing access token
20 | * The requested type is the same as in the existing access token
21 |
22 | .. note::
23 |
24 | Unique access tokens are currently supported by
25 | :class:`oauth2.grant.AuthorizationCodeGrant` and
26 | :class:`oauth2.grant.ResourceOwnerGrant`.
27 |
28 | Preconditions
29 | -------------
30 |
31 | As stated in the previous section, a unique access token is bound not only to a
32 | client but also to a user. To make this work the Provider needs some kind of
33 | identifier that is unique for each user (typically the ID of a user in the
34 | database). The identifier is stored along with all the other information of an
35 | access token. It has to be returned as the second item of a tuple by your
36 | implementation of :class:`oauth2.web.AuthenticatingSiteAdapter.authenticate`::
37 |
38 | class MySiteAdapter(SiteAdapter):
39 |
40 | def authenticate(self, request, environ, scopes):
41 | // Your logic here
42 |
43 | return None, user["id"]
44 |
45 | Enabling the feature
46 | --------------------
47 |
48 | Unique access tokens are turned off by default. They can be turned on for each
49 | grant individually::
50 |
51 | auth_code_grant = oauth2.grant.AuthorizationCodeGrant(unique_token=True)
52 | provider = oauth2.Provider() // Parameters omitted for readability
53 | provider.add_grant(auth_code_grant)
54 |
55 | or you can enable them for all grants that support this feature after
56 | initialization of :class:`oauth2.Provider`::
57 |
58 | provider = oauth2.Provider() // Parameters omitted for readability
59 | provider.enable_unique_tokens()
60 |
61 | .. note::
62 |
63 | If you enable the feature but forgot to make
64 | :class:`oauth2.web.AuthenticatingSiteAdapter.authenticate` return a user
65 | identifier, the Provider will respond with an error to requests for a
66 | token.
67 |
--------------------------------------------------------------------------------
/docs/web.rst:
--------------------------------------------------------------------------------
1 | ``oauth2.web`` --- Interaction over HTTP
2 | ========================================
3 |
4 | .. automodule:: oauth2.web
5 |
6 | Site adapters
7 | -------------
8 |
9 | .. autoclass:: UserFacingSiteAdapter
10 | :members:
11 |
12 | .. autoclass:: AuthenticatingSiteAdapter
13 | :members:
14 |
15 | .. autoclass:: AuthorizationCodeGrantSiteAdapter
16 | :members:
17 | :inherited-members:
18 | :show-inheritance:
19 |
20 | .. autoclass:: ImplicitGrantSiteAdapter
21 | :members:
22 | :inherited-members:
23 | :show-inheritance:
24 |
25 | .. autoclass:: ResourceOwnerGrantSiteAdapter
26 | :members:
27 | :inherited-members:
28 | :show-inheritance:
29 |
30 | HTTP flow
31 | ---------
32 |
33 | .. autoclass:: Request
34 | :members:
35 |
36 | .. autoclass:: Response
37 | :members:
38 |
--------------------------------------------------------------------------------
/oauth2/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | =============
3 | python-oauth2
4 | =============
5 |
6 | python-oauth2 is a framework that aims at making it easy to provide
7 | authentication via `OAuth 2.0 `_ within
8 | an application stack.
9 |
10 | Usage
11 | =====
12 |
13 | Example:
14 |
15 | .. literalinclude:: examples/base_server.py
16 |
17 | Installation
18 | ============
19 |
20 | python-oauth2 is available on
21 | `PyPI `_::
22 |
23 | pip install python-oauth2
24 |
25 | """
26 |
27 | import json
28 | from oauth2.client_authenticator import ClientAuthenticator, request_body
29 | from oauth2.error import OAuthInvalidError, \
30 | ClientNotFoundError, OAuthInvalidNoRedirectError, UnsupportedGrantError
31 | from oauth2.log import app_log
32 | from oauth2.web import Response
33 | from oauth2.tokengenerator import Uuid4
34 | from oauth2.grant import Scope, AuthorizationCodeGrant, ImplicitGrant, \
35 | ClientCredentialsGrant, ResourceOwnerGrant, RefreshToken
36 |
37 | VERSION = "1.1.1"
38 |
39 |
40 | class Provider(object):
41 | """
42 | Endpoint of requests to the OAuth 2.0 provider.
43 |
44 | :param access_token_store: An object that implements methods defined
45 | by :class:`oauth2.store.AccessTokenStore`.
46 | :type access_token_store: oauth2.store.AccessTokenStore
47 | :param auth_code_store: An object that implements methods defined by
48 | :class:`oauth2.store.AuthCodeStore`.
49 | :type auth_code_store: oauth2.store.AuthCodeStore
50 | :param client_store: An object that implements methods defined by
51 | :class:`oauth2.store.ClientStore`.
52 | :type client_store: oauth2.store.ClientStore
53 | :param token_generator: Object to generate unique tokens.
54 | :type token_generator: oauth2.tokengenerator.TokenGenerator
55 | :param client_authentication_source: A callable which when executed,
56 | authenticates a client.
57 | See :mod:`oauth2.client_authenticator`.
58 | :type client_authentication_source: callable
59 | :param response_class: Class of the response object.
60 | Defaults to :class:`oauth2.web.Response`.
61 | :type response_class: oauth2.web.Response
62 |
63 | .. versionchanged:: 1.0.0
64 | Removed parameter ``site_adapter``.
65 | """
66 | authorize_path = "/authorize"
67 | token_path = "/token"
68 |
69 | def __init__(self, access_token_store, auth_code_store, client_store,
70 | token_generator, client_authentication_source=request_body,
71 | response_class=Response):
72 | self.grant_types = []
73 | self._input_handler = None
74 |
75 | self.access_token_store = access_token_store
76 | self.auth_code_store = auth_code_store
77 | self.client_authenticator = ClientAuthenticator(
78 | client_store=client_store,
79 | source=client_authentication_source)
80 | self.response_class = response_class
81 | self.token_generator = token_generator
82 |
83 | def add_grant(self, grant):
84 | """
85 | Adds a Grant that the provider should support.
86 |
87 | :param grant: An instance of a class that extends
88 | :class:`oauth2.grant.GrantHandlerFactory`
89 | :type grant: oauth2.grant.GrantHandlerFactory
90 | """
91 | if hasattr(grant, "expires_in"):
92 | self.token_generator.expires_in[grant.grant_type] = grant.expires_in
93 |
94 | if hasattr(grant, "refresh_expires_in"):
95 | self.token_generator.refresh_expires_in = grant.refresh_expires_in
96 |
97 | self.grant_types.append(grant)
98 |
99 | def dispatch(self, request, environ):
100 | """
101 | Checks which Grant supports the current request and dispatches to it.
102 |
103 | :param request: The incoming request.
104 | :type request: :class:`oauth2.web.Request`
105 | :param environ: Dict containing variables of the environment.
106 | :type environ: dict
107 |
108 | :return: An instance of ``oauth2.web.Response``.
109 | """
110 | try:
111 | grant_type = self._determine_grant_type(request)
112 |
113 | response = self.response_class()
114 |
115 | grant_type.read_validate_params(request)
116 |
117 | return grant_type.process(request, response, environ)
118 | except OAuthInvalidNoRedirectError:
119 | response = self.response_class()
120 | response.add_header("Content-Type", "application/json")
121 | response.status_code = 400
122 | response.body = json.dumps({
123 | "error": "invalid_redirect_uri",
124 | "error_description": "Invalid redirect URI"
125 | })
126 |
127 | return response
128 | except OAuthInvalidError as err:
129 | response = self.response_class()
130 | return grant_type.handle_error(error=err, response=response)
131 | except UnsupportedGrantError:
132 | response = self.response_class()
133 | response.add_header("Content-Type", "application/json")
134 | response.status_code = 400
135 | response.body = json.dumps({
136 | "error": "unsupported_response_type",
137 | "error_description": "Grant not supported"
138 | })
139 |
140 | return response
141 | except:
142 | app_log.error("Uncaught Exception", exc_info=True)
143 | response = self.response_class()
144 | return grant_type.handle_error(
145 | error=OAuthInvalidError(error="server_error",
146 | explanation="Internal server error"),
147 | response=response)
148 |
149 | def enable_unique_tokens(self):
150 | """
151 | Enable the use of unique access tokens on all grant types that support
152 | this option.
153 | """
154 | for grant_type in self.grant_types:
155 | if hasattr(grant_type, "unique_token"):
156 | grant_type.unique_token = True
157 |
158 | @property
159 | def scope_separator(self, separator):
160 | """
161 | Sets the separator of values in the scope query parameter.
162 | Defaults to " " (whitespace).
163 |
164 | The following code makes the Provider use "," instead of " "::
165 |
166 | provider = Provider()
167 |
168 | provider.scope_separator = ","
169 |
170 | Now the scope parameter in the request of a client can look like this:
171 | `scope=foo,bar`.
172 | """
173 | Scope.separator = separator
174 |
175 | def _determine_grant_type(self, request):
176 | for grant in self.grant_types:
177 | grant_handler = grant(request, self)
178 | if grant_handler is not None:
179 | return grant_handler
180 |
181 | raise UnsupportedGrantError
182 |
--------------------------------------------------------------------------------
/oauth2/client_authenticator.py:
--------------------------------------------------------------------------------
1 | """
2 | Every client that sends a request to obtain an access token needs to
3 | authenticate with the provider.
4 |
5 | The authentication of confidential clients can be handled in several ways,
6 | some of which come bundled with this module.
7 | """
8 |
9 | from base64 import b64decode
10 | from oauth2.error import OAuthInvalidNoRedirectError, RedirectUriUnknown, \
11 | OAuthInvalidError, ClientNotFoundError
12 |
13 |
14 | class ClientAuthenticator(object):
15 | """
16 | Handles authentication of a client both by its identifier as well as by
17 | its identifier and secret.
18 |
19 | :param client_store: The Client Store to retrieve a client from.
20 | :type client_store: oauth2.store.ClientStore
21 | :param source: A callable that returns a tuple
22 | (, )
23 | :type source: callable
24 | """
25 | def __init__(self, client_store, source):
26 | self.client_store = client_store
27 | self.source = source
28 |
29 | def by_identifier(self, request):
30 | """
31 | Authenticates a client by its identifier.
32 |
33 | :param request: The incoming request
34 | :type request: oauth2.web.Request
35 |
36 | :return: The identified client
37 | :rtype: oauth2.datatype.Client
38 |
39 | :raises: :class OAuthInvalidNoRedirectError:
40 | """
41 | client_id = request.get_param("client_id")
42 |
43 | if client_id is None:
44 | raise OAuthInvalidNoRedirectError(error="missing_client_id")
45 |
46 | try:
47 | client = self.client_store.fetch_by_client_id(client_id)
48 | except ClientNotFoundError:
49 | raise OAuthInvalidNoRedirectError(error="unknown_client")
50 |
51 | redirect_uri = request.get_param("redirect_uri")
52 | if redirect_uri is not None:
53 | try:
54 | client.redirect_uri = redirect_uri
55 | except RedirectUriUnknown:
56 | raise OAuthInvalidNoRedirectError(
57 | error="invalid_redirect_uri")
58 |
59 | return client
60 |
61 | def by_identifier_secret(self, request):
62 | """
63 | Authenticates a client by its identifier and secret (aka password).
64 |
65 | :param request: The incoming request
66 | :type request: oauth2.web.Request
67 |
68 | :return: The identified client
69 | :rtype: oauth2.datatype.Client
70 |
71 | :raises OAuthInvalidError: If the client could not be found, is not
72 | allowed to to use the current grant or
73 | supplied invalid credentials
74 | """
75 | client_id, client_secret = self.source(request=request)
76 |
77 | try:
78 | client = self.client_store.fetch_by_client_id(client_id)
79 | except ClientNotFoundError:
80 | raise OAuthInvalidError(error="invalid_client",
81 | explanation="No client could be found")
82 |
83 | grant_type = request.post_param("grant_type")
84 | if client.grant_type_supported(grant_type) is False:
85 | raise OAuthInvalidError(error="unauthorized_client",
86 | explanation="The client is not allowed "
87 | "to use this grant type")
88 |
89 | if client.secret != client_secret:
90 | raise OAuthInvalidError(error="invalid_client",
91 | explanation="Invalid client credentials")
92 |
93 | return client
94 |
95 |
96 | def request_body(request):
97 | """
98 | Extracts the credentials of a client from the
99 | *application/x-www-form-urlencoded* body of a request.
100 |
101 | Expects the client_id to be the value of the ``client_id`` parameter and
102 | the client_secret to be the value of the ``client_secret`` parameter.
103 |
104 | :param request: The incoming request
105 | :type request: oauth2.web.Request
106 |
107 | :return: A tuple in the format of `(, )`
108 | :rtype: tuple
109 | """
110 | client_id = request.post_param("client_id")
111 | if client_id is None:
112 | raise OAuthInvalidError(error="invalid_request",
113 | explanation="Missing client identifier")
114 |
115 | client_secret = request.post_param("client_secret")
116 | if client_secret is None:
117 | raise OAuthInvalidError(error="invalid_request",
118 | explanation="Missing client credentials")
119 |
120 | return client_id, client_secret
121 |
122 |
123 | def http_basic_auth(request):
124 | """
125 | Extracts the credentials of a client using HTTP Basic Auth.
126 |
127 | Expects the ``client_id`` to be the username and the ``client_secret`` to
128 | be the password part of the Authorization header.
129 |
130 | :param request: The incoming request
131 | :type request: oauth2.web.Request
132 |
133 | :return: A tuple in the format of (, )`
134 | :rtype: tuple
135 | """
136 | auth_header = request.header("authorization")
137 |
138 | if auth_header is None:
139 | raise OAuthInvalidError(error="invalid_request",
140 | explanation="Authorization header is missing")
141 |
142 | auth_parts = auth_header.strip().encode("latin1").split(None)
143 |
144 | if auth_parts[0].strip().lower() != b'basic':
145 | raise OAuthInvalidError(
146 | error="invalid_request",
147 | explanation="Provider supports basic authentication only")
148 |
149 | client_id, client_secret = b64decode(auth_parts[1]).split(b':', 1)
150 |
151 | return client_id.decode("latin1"), client_secret.decode("latin1")
152 |
--------------------------------------------------------------------------------
/oauth2/compatibility.py:
--------------------------------------------------------------------------------
1 | """
2 | Ensures compatibility between python 2.x and python 3.x
3 | """
4 |
5 | import sys
6 |
7 | if sys.version_info >= (3, 0):
8 | from urllib.parse import parse_qs # pragma: no cover
9 | from urllib.parse import urlencode # pragma: no cover
10 | from urllib.parse import quote # pragma: no cover
11 | else:
12 | from urlparse import parse_qs # pragma: no cover
13 | from urllib import urlencode # pragma: no cover
14 | from urllib import quote # pragma: no cover
15 |
--------------------------------------------------------------------------------
/oauth2/datatype.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Definitions of types used by grants.
4 | """
5 |
6 | import time
7 | from oauth2.error import RedirectUriUnknown
8 |
9 |
10 | class AccessToken(object):
11 | """
12 | An access token and associated data.
13 | """
14 | def __init__(self, client_id, grant_type, token, data={}, expires_at=None,
15 | refresh_token=None, refresh_expires_at=None, scopes=[],
16 | user_id=None):
17 | self.client_id = client_id
18 | self.grant_type = grant_type
19 | self.token = token
20 | self.data = data
21 | self.expires_at = expires_at
22 | self.refresh_token = refresh_token
23 | self.refresh_expires_at = refresh_expires_at
24 | self.scopes = scopes
25 | self.user_id = user_id
26 |
27 | @property
28 | def expires_in(self):
29 | """
30 | Returns the time until the token expires.
31 |
32 | :return: The remaining time until expiration in seconds or 0 if the
33 | token has expired.
34 | """
35 | time_left = self.expires_at - int(time.time())
36 |
37 | if time_left > 0:
38 | return time_left
39 | return 0
40 |
41 | def is_expired(self):
42 | """
43 | Determines if the token has expired.
44 |
45 | :return: `True` if the token has expired. Otherwise `False`.
46 | """
47 | if self.expires_at is None:
48 | return False
49 |
50 | if self.expires_in > 0:
51 | return False
52 |
53 | return True
54 |
55 |
56 | class AuthorizationCode(object):
57 | """
58 | Holds an authorization code and additional information.
59 | """
60 | def __init__(self, client_id, code, expires_at, redirect_uri, scopes,
61 | data=None, user_id=None):
62 | self.client_id = client_id
63 | self.code = code
64 | self.data = data
65 | self.expires_at = expires_at
66 | self.redirect_uri = redirect_uri
67 | self.scopes = scopes
68 | self.user_id = user_id
69 |
70 | def is_expired(self):
71 | if self.expires_at < int(time.time()):
72 | return True
73 | return False
74 |
75 |
76 | class Client(object):
77 | """
78 | Representation of a client application.
79 | """
80 | def __init__(self, identifier, secret, authorized_grants=None,
81 | authorized_response_types=None, redirect_uris=None):
82 | """
83 | :param identifier: The unique identifier of a client.
84 | :param secret: The secret the clients uses to authenticate.
85 | :param authorized_grants: A list of grants under which the client can
86 | request tokens.
87 | All grants are allowed if this value is set
88 | to `None` (default).
89 | :param authorized_response_types: A list of response types of which
90 | the client can request tokens.
91 | All response types are allowed if
92 | this value is set to `None`
93 | (default).
94 | :redirect_uris: A list of redirect uris this client can use.
95 | """
96 | self.authorized_grants = authorized_grants
97 | self.authorized_response_types = authorized_response_types
98 | self.identifier = identifier
99 | self.secret = secret
100 |
101 | if redirect_uris is None:
102 | self.redirect_uris = []
103 | else:
104 | self.redirect_uris = redirect_uris
105 |
106 | self._redirect_uri = None
107 |
108 | @property
109 | def redirect_uri(self):
110 | if self._redirect_uri is None:
111 | # redirect_uri is an optional param.
112 | # If not supplied, we use the first entry stored in db as default.
113 | return self.redirect_uris[0]
114 | return self._redirect_uri
115 |
116 | @redirect_uri.setter
117 | def redirect_uri(self, value):
118 | if value not in self.redirect_uris:
119 | raise RedirectUriUnknown
120 | self._redirect_uri = value
121 |
122 | def grant_type_supported(self, grant_type):
123 | """
124 | Checks if the Client is authorized receive tokens for the given grant.
125 |
126 | :param grant_type: The type of the grant.
127 |
128 | :return: Boolean
129 | """
130 | if self.authorized_grants is None:
131 | return True
132 |
133 | return grant_type in self.authorized_grants
134 |
135 | def response_type_supported(self, response_type):
136 | """
137 | Checks if the client is allowed to receive tokens for the given
138 | response type.
139 |
140 | :param response_type: The response type.
141 |
142 | :return: Boolean
143 | """
144 | if self.authorized_response_types is None:
145 | return True
146 |
147 | return response_type in self.authorized_response_types
148 |
--------------------------------------------------------------------------------
/oauth2/error.py:
--------------------------------------------------------------------------------
1 | """
2 | Errors raised during the OAuth 2.0 flow.
3 | """
4 |
5 |
6 | class AccessTokenNotFound(Exception):
7 | """
8 | Error indicating that an access token could not be read from the
9 | storage backend by an instance of :class:`oauth2.store.AccessTokenStore`.
10 | """
11 | pass
12 |
13 |
14 | class AuthCodeNotFound(Exception):
15 | """
16 | Error indicating that an authorization code could not be read from the
17 | storage backend by an instance of :class:`oauth2.store.AuthCodeStore`.
18 | """
19 | pass
20 |
21 |
22 | class ClientNotFoundError(Exception):
23 | """
24 | Error raised by an implementation of :class:`oauth2.store.ClientStore` if
25 | a client does not exists.
26 | """
27 | pass
28 |
29 |
30 | class InvalidSiteAdapter(Exception):
31 | """
32 | Raised by :class:`oauth2.grant.SiteAdapterMixin` in case an invalid site
33 | adapter was passed to the instance.
34 | """
35 | pass
36 |
37 |
38 | class UserIdentifierMissingError(Exception):
39 | """
40 | Indicates that the identifier of a user is missing when the use of unique
41 | access token is enabled.
42 | """
43 | pass
44 |
45 |
46 | class OAuthBaseError(Exception):
47 | """
48 | Base class used by all OAuth 2.0 errors.
49 |
50 | :param error: Identifier of the error.
51 | :param error_uri: Set this to delivery an URL to your documentation that
52 | describes the error. (optional)
53 | :param explanation: Short message that describes the error. (optional)
54 | """
55 | def __init__(self, error, error_uri=None, explanation=None):
56 | self.error = error
57 | self.error_uri = error_uri
58 | self.explanation = explanation
59 |
60 | super(OAuthBaseError, self).__init__()
61 |
62 |
63 | class OAuthInvalidError(OAuthBaseError):
64 | """
65 | Indicates an error during validation of a request.
66 | """
67 | pass
68 |
69 |
70 | class OAuthInvalidNoRedirectError(OAuthInvalidError):
71 | """
72 | Indicates an error during validation of a request.
73 | The provider will not inform the client about the error by redirecting to
74 | it. This behaviour is required by the Authorization Request step of the
75 | Authorization Code Grant and Implicit Grant.
76 | """
77 | pass
78 |
79 |
80 | class UnsupportedGrantError(Exception):
81 | """
82 | Indicates that a requested grant is not supported by the server.
83 | """
84 | pass
85 |
86 |
87 | class RedirectUriUnknown(Exception):
88 | """
89 | Indicates that a redirect_uri is not associated with a client.
90 | """
91 | pass
92 |
93 |
94 | class UserNotAuthenticated(Exception):
95 | """
96 | Raised by a :class:`oauth2.web.SiteAdapter` if a user could not be
97 | authenticated.
98 | """
99 | pass
100 |
--------------------------------------------------------------------------------
/oauth2/log.py:
--------------------------------------------------------------------------------
1 | """
2 | Logging support
3 |
4 | There are two loggers available:
5 |
6 | * ``oauth2.application``: Logging of uncaught exceptions
7 | * ``oauth2.general``: General purpose logging of debug errors and warnings
8 |
9 | If logging has not been configured, you will likely see this error:
10 |
11 | .. code-block:: python
12 |
13 | No handlers could be found for logger "oauth2.application"
14 |
15 | Make sure that logging is configured to avoid this:
16 |
17 | .. code-block:: python
18 |
19 | import logging
20 | logging.basicConfig()
21 |
22 | """
23 | import logging
24 |
25 | app_log = logging.getLogger("oauth2.application")
26 | gen_log = logging.getLogger("oauth2.general")
27 |
--------------------------------------------------------------------------------
/oauth2/store/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Store adapters to persist and retrieve data during the OAuth 2.0 process or
3 | for later use.
4 | This module provides base classes that can be extended to implement your own
5 | solution specific to your needs.
6 | It also includes implementations for popular storage systems like memcache.
7 | """
8 |
9 |
10 | class AccessTokenStore(object):
11 | """
12 | Base class for persisting an access token after it has been generated.
13 |
14 | Used by two-legged and three-legged authentication flows.
15 | """
16 | def save_token(self, access_token):
17 | """
18 | Stores an access token and additional data.
19 |
20 | :param access_token: An instance of :class:`oauth2.datatype.AccessToken`.
21 |
22 | """
23 | raise NotImplementedError
24 |
25 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
26 | """
27 | Fetches an access token identified by its client id, type of grant and
28 | user id.
29 |
30 | This method must be implemented to make use of unique access tokens.
31 |
32 | :param client_id: Identifier of the client a token belongs to.
33 | :param grant_type: The type of the grant that created the token
34 | :param user_id: Identifier of the user a token belongs to.
35 | :return: An instance of :class:`oauth2.datatype.AccessToken`.
36 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be
37 | retrieved.
38 | """
39 | raise NotImplementedError
40 |
41 | def fetch_by_refresh_token(self, refresh_token):
42 | """
43 | Fetches an access token from the store using its refresh token to
44 | identify it.
45 |
46 | :param refresh_token: A string containing the refresh token.
47 | :return: An instance of :class:`oauth2.datatype.AccessToken`.
48 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be retrieved for
49 | given refresh_token.
50 | """
51 | raise NotImplementedError
52 |
53 | def delete_refresh_token(self, refresh_token):
54 | """
55 | Deletes an access token from the store using its refresh token to identify it.
56 | This invalidates both the access token and the refresh token.
57 |
58 | :param refresh_token: A string containing the refresh token.
59 | :return: None.
60 | :raises: :class:`oauth2.error.AccessTokenNotFound` if no data could be retrieved for
61 | given refresh_token.
62 | """
63 | raise NotImplementedError
64 |
65 |
66 | class AuthCodeStore(object):
67 | """
68 | Base class for persisting and retrieving an auth token during the
69 | Authorization Code Grant flow.
70 | """
71 | def fetch_by_code(self, code):
72 | """
73 | Returns an AuthorizationCode fetched from a storage.
74 |
75 | :param code: The authorization code.
76 | :return: An instance of :class:`oauth2.datatype.AuthorizationCode`.
77 | :raises: :class:`oauth2.error.AuthCodeNotFound` if no data could be retrieved for
78 | given code.
79 |
80 | """
81 | raise NotImplementedError
82 |
83 | def save_code(self, authorization_code):
84 | """
85 | Stores the data belonging to an authorization code token.
86 |
87 | :param authorization_code: An instance of
88 | :class:`oauth2.datatype.AuthorizationCode`.
89 | """
90 | raise NotImplementedError
91 |
92 | def delete_code(self, code):
93 | """
94 | Deletes an authorization code after it's use per section 4.1.2.
95 |
96 | http://tools.ietf.org/html/rfc6749#section-4.1.2
97 |
98 | :param code: The authorization code.
99 | """
100 | raise NotImplementedError
101 |
102 |
103 | class ClientStore(object):
104 | """
105 | Base class for handling OAuth2 clients.
106 | """
107 | def fetch_by_client_id(self, client_id):
108 | """
109 | Retrieve a client by its identifier.
110 |
111 | :param client_id: Identifier of a client app.
112 | :return: An instance of :class:`oauth2.datatype.Client`.
113 | :raises: :class:`oauth2.error.ClientNotFoundError` if no data could be retrieved for
114 | given client_id.
115 | """
116 | raise NotImplementedError
117 |
--------------------------------------------------------------------------------
/oauth2/store/dbapi/mysql.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Adapters to use mysql as the storage backend.
4 |
5 | This module uses the API defined in :mod:`oauth2.store.dbapi`.
6 | Therefore no logic is defined here. Instead all classes define the queries
7 | required by :mod:`oauth2.store.dbapi`.
8 |
9 | The queries have been created for the following SQL tables in mind:
10 |
11 | .. code-block:: sql
12 |
13 | CREATE TABLE IF NOT EXISTS `testdb`.`access_tokens` (
14 | `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier',
15 | `client_id` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client. Assuming it is an arbitrary text which is a maximum of 32 characters long.',
16 | `grant_type` ENUM('authorization_code', 'implicit', 'password', 'client_credentials', 'refresh_token') NOT NULL COMMENT 'The type of a grant for which a token has been issued.',
17 | `token` CHAR(36) NOT NULL COMMENT 'The access token.',
18 | `expires_at` TIMESTAMP NULL COMMENT 'The timestamp at which the token expires.',
19 | `refresh_token` CHAR(36) NULL COMMENT 'The refresh token.',
20 | `refresh_expires_at` TIMESTAMP NULL COMMENT 'The timestamp at which the refresh token expires.',
21 | `user_id` INT NULL COMMENT 'The identifier of the user this token belongs to.',
22 | PRIMARY KEY (`id`),
23 | INDEX `fetch_by_refresh_token` (`refresh_token` ASC),
24 | INDEX `fetch_existing_token_of_user` (`client_id` ASC, `grant_type` ASC, `user_id` ASC))
25 | ENGINE = InnoDB;
26 |
27 | CREATE TABLE IF NOT EXISTS `testdb`.`access_token_scopes` (
28 | `id` INT NOT NULL AUTO_INCREMENT,
29 | `name` VARCHAR(32) NOT NULL COMMENT 'The name of scope.',
30 | `access_token_id` INT NOT NULL COMMENT 'The unique identifier of the access token this scope belongs to.',
31 | PRIMARY KEY (`id`))
32 | ENGINE = InnoDB;
33 |
34 | CREATE TABLE IF NOT EXISTS `testdb`.`access_token_data` (
35 | `id` INT NOT NULL AUTO_INCREMENT,
36 | `key` VARCHAR(32) NOT NULL COMMENT 'The key of an entry converted to the key in a Python dict.',
37 | `value` VARCHAR(32) NOT NULL COMMENT 'The value of an entry converted to the value in a Python dict.',
38 | `access_token_id` INT NOT NULL COMMENT 'The unique identifier of the access token a row belongs to.',
39 | PRIMARY KEY (`id`))
40 | ENGINE = InnoDB;
41 |
42 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_codes` (
43 | `id` INT NOT NULL AUTO_INCREMENT,
44 | `client_id` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client. Assuming it is an arbitrary text which is a maximum of 32 characters long.',
45 | `code` CHAR(36) NOT NULL COMMENT 'The authorisation code.',
46 | `expires_at` TIMESTAMP NOT NULL COMMENT 'The timestamp at which the token expires.',
47 | `redirect_uri` VARCHAR(128) NULL COMMENT 'The redirect URI send by the client during the request of an authorisation code.',
48 | `user_id` INT NULL COMMENT 'The identifier of the user this authorisation code belongs to.',
49 | PRIMARY KEY (`id`),
50 | INDEX `fetch_code` (`code` ASC))
51 | ENGINE = InnoDB;
52 |
53 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_code_data` (
54 | `id` INT NOT NULL AUTO_INCREMENT,
55 | `key` VARCHAR(32) NOT NULL COMMENT 'The key of an entry converted to the key in a Python dict.',
56 | `value` VARCHAR(32) NOT NULL COMMENT 'The value of an entry converted to the value in a Python dict.',
57 | `auth_code_id` INT NOT NULL COMMENT 'The identifier of the authorisation code that this row belongs to.',
58 | PRIMARY KEY (`id`))
59 | ENGINE = InnoDB;
60 |
61 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_code_scopes` (
62 | `id` INT NOT NULL AUTO_INCREMENT,
63 | `name` VARCHAR(32) NOT NULL,
64 | `auth_code_id` INT NOT NULL,
65 | PRIMARY KEY (`id`))
66 | ENGINE = InnoDB;
67 |
68 | CREATE TABLE IF NOT EXISTS `testdb`.`clients` (
69 | `id` INT NOT NULL AUTO_INCREMENT,
70 | `identifier` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client.',
71 | `secret` VARCHAR(32) NOT NULL COMMENT 'The secret of a client.',
72 | PRIMARY KEY (`id`))
73 | ENGINE = InnoDB;
74 |
75 | CREATE TABLE IF NOT EXISTS `testdb`.`client_grants` (
76 | `id` INT NOT NULL AUTO_INCREMENT,
77 | `name` VARCHAR(32) NOT NULL,
78 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
79 | PRIMARY KEY (`id`))
80 | ENGINE = InnoDB;
81 |
82 | CREATE TABLE IF NOT EXISTS `testdb`.`client_redirect_uris` (
83 | `id` INT NOT NULL AUTO_INCREMENT,
84 | `redirect_uri` VARCHAR(128) NOT NULL COMMENT 'A URI of a client.',
85 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
86 | PRIMARY KEY (`id`))
87 | ENGINE = InnoDB;
88 |
89 | CREATE TABLE IF NOT EXISTS `testdb`.`client_response_types` (
90 | `id` INT NOT NULL AUTO_INCREMENT,
91 | `response_type` VARCHAR(32) NOT NULL COMMENT 'The response type that a client can use.',
92 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
93 | PRIMARY KEY (`id`))
94 | ENGINE = InnoDB;
95 | """
96 |
97 | from oauth2.store.dbapi import DbApiAccessTokenStore, DbApiAuthCodeStore, \
98 | DbApiClientStore
99 |
100 |
101 | class MysqlAccessTokenStore(DbApiAccessTokenStore):
102 | delete_refresh_token_query = """
103 | DELETE FROM
104 | `access_tokens`
105 | WHERE
106 | `refresh_token` = %s"""
107 |
108 | fetch_by_refresh_token_query = """
109 | SELECT
110 | `id`, `client_id`, `grant_type`, `token`,
111 | UNIX_TIMESTAMP(`expires_at`), `refresh_token`,
112 | UNIX_TIMESTAMP(`refresh_expires_at`), `user_id`
113 | FROM
114 | `access_tokens`
115 | WHERE
116 | `refresh_token` = %s
117 | LIMIT 1"""
118 |
119 | fetch_scopes_by_access_token_query = """
120 | SELECT
121 | `name`
122 | FROM
123 | `access_token_scopes`
124 | WHERE
125 | `access_token_id` = %s"""
126 |
127 | fetch_data_by_access_token_query = """
128 | SELECT
129 | `key`, `value`
130 | FROM
131 | `access_token_data`
132 | WHERE
133 | `access_token_id` = %s"""
134 |
135 | fetch_existing_token_of_user_query = """
136 | SELECT
137 | `id`, `client_id`, `grant_type`, `token`,
138 | UNIX_TIMESTAMP(`expires_at`), `refresh_token`,
139 | UNIX_TIMESTAMP(`refresh_expires_at`), `user_id`
140 | FROM
141 | `access_tokens`
142 | WHERE
143 | `client_id` = %s
144 | AND
145 | `grant_type` = %s
146 | AND
147 | `user_id` = %s
148 | ORDER BY
149 | `expires_at` DESC
150 | LIMIT 1"""
151 |
152 | create_access_token_query = """
153 | INSERT INTO `access_tokens` (
154 | `client_id`, `grant_type`, `token`, `expires_at`, `refresh_token`,
155 | `refresh_expires_at`, `user_id`
156 | ) VALUES (
157 | %s, %s, %s, FROM_UNIXTIME(%s), %s, FROM_UNIXTIME(%s), %s
158 | )"""
159 |
160 | create_data_query = """
161 | INSERT INTO `access_token_data` (
162 | `key`,`value`, `access_token_id`
163 | ) VALUES (
164 | %s, %s, %s
165 | )"""
166 |
167 | create_scope_query = """
168 | INSERT INTO `access_token_scopes` (
169 | `name`, `access_token_id`
170 | ) VALUES (
171 | %s, %s
172 | )"""
173 |
174 |
175 | class MysqlAuthCodeStore(DbApiAuthCodeStore):
176 | create_auth_code_query = """
177 | INSERT INTO `auth_codes` (
178 | `client_id`,`code`,`expires_at`,`redirect_uri`, `user_id`
179 | ) VALUES (
180 | %s, %s, FROM_UNIXTIME(%s), %s, %s
181 | )"""
182 |
183 | create_data_query = """
184 | INSERT INTO `auth_code_data` (
185 | `key`,`value`, `auth_code_id`
186 | ) VALUES (
187 | %s, %s, %s
188 | )"""
189 |
190 | create_scope_query = """
191 | INSERT INTO `auth_code_scopes` (
192 | `name`, `auth_code_id`
193 | ) VALUES (
194 | %s, %s
195 | )"""
196 |
197 | delete_code_query = """
198 | DELETE FROM `auth_codes` WHERE code = %s"""
199 |
200 | fetch_code_query = """
201 | SELECT
202 | `id`, `client_id`, `code`, UNIX_TIMESTAMP(`expires_at`),
203 | `redirect_uri`, `user_id`
204 | FROM
205 | `auth_codes`
206 | WHERE
207 | `code` = %s"""
208 |
209 | fetch_data_query = """
210 | SELECT
211 | `key`, `value`
212 | FROM
213 | `auth_code_data`
214 | WHERE
215 | `auth_code_id` = %s"""
216 |
217 | fetch_scopes_query = """
218 | SELECT
219 | `name`
220 | FROM
221 | `auth_code_scopes`
222 | WHERE
223 | `auth_code_id` = %s"""
224 |
225 |
226 | class MysqlClientStore(DbApiClientStore):
227 | fetch_client_query = """
228 | SELECT
229 | `id`,`identifier`, `secret`
230 | FROM
231 | `clients`
232 | WHERE
233 | `identifier` = %s"""
234 |
235 | fetch_grants_query = """
236 | SELECT
237 | `name`
238 | FROM
239 | `client_grants`
240 | WHERE
241 | `client_id` = %s"""
242 | fetch_redirect_uris_query = """
243 | SELECT
244 | `redirect_uri`
245 | FROM
246 | `client_redirect_uris`
247 | WHERE
248 | `client_id` = %s"""
249 |
250 | fetch_response_types_query = """
251 | SELECT
252 | `response_type`
253 | FROM
254 | `client_response_types`
255 | WHERE
256 | `client_id` = %s"""
257 |
--------------------------------------------------------------------------------
/oauth2/store/memcache.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import memcache
3 |
4 | from oauth2.datatype import AccessToken, AuthorizationCode
5 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound
6 | from oauth2.store import AccessTokenStore, AuthCodeStore
7 |
8 |
9 | class TokenStore(AccessTokenStore, AuthCodeStore):
10 | """
11 | Uses memcache to store access tokens and auth tokens.
12 |
13 | This Store supports ``python-memcached``. Arguments are passed to the
14 | underlying client implementation.
15 |
16 | Initialization by passing an object::
17 |
18 | # This example uses python-memcached
19 | import memcache
20 |
21 | # Somewhere in your application
22 | mc = memcache.Client(servers=['127.0.0.1:11211'], debug=0)
23 | # ...
24 | token_store = TokenStore(mc=mc)
25 |
26 | Initialization using ``python-memcached``::
27 |
28 | token_store = TokenStore(servers=['127.0.0.1:11211'], debug=0)
29 |
30 | """
31 | def __init__(self, mc=None, prefix="oauth2", *args, **kwargs):
32 | self.prefix = prefix
33 |
34 | if mc is not None:
35 | self.mc = mc
36 | else:
37 | self.mc = memcache.Client(*args, **kwargs)
38 |
39 | def fetch_by_code(self, code):
40 | """
41 | Returns data belonging to an authorization code from memcache or
42 | ``None`` if no data was found.
43 |
44 | See :class:`oauth2.store.AuthCodeStore`.
45 |
46 | """
47 | code_data = self.mc.get(self._generate_cache_key(code))
48 |
49 | if code_data is None:
50 | raise AuthCodeNotFound
51 |
52 | return AuthorizationCode(**code_data)
53 |
54 | def save_code(self, authorization_code):
55 | """
56 | Stores the data belonging to an authorization code token in memcache.
57 |
58 | See :class:`oauth2.store.AuthCodeStore`.
59 |
60 | """
61 | key = self._generate_cache_key(authorization_code.code)
62 |
63 | self.mc.set(key, {"client_id": authorization_code.client_id,
64 | "code": authorization_code.code,
65 | "expires_at": authorization_code.expires_at,
66 | "redirect_uri": authorization_code.redirect_uri,
67 | "scopes": authorization_code.scopes,
68 | "data": authorization_code.data,
69 | "user_id": authorization_code.user_id})
70 |
71 | def delete_code(self, code):
72 | """
73 | Deletes an authorization code after use
74 | :param code: The authorization code.
75 | """
76 | self.mc.delete(self._generate_cache_key(code))
77 |
78 | def save_token(self, access_token):
79 | """
80 | Stores the access token and additional data in memcache.
81 |
82 | See :class:`oauth2.store.AccessTokenStore`.
83 |
84 | """
85 | key = self._generate_cache_key(access_token.token)
86 | self.mc.set(key, access_token.__dict__)
87 |
88 | unique_token_key = self._unique_token_key(access_token.client_id,
89 | access_token.grant_type,
90 | access_token.user_id)
91 | self.mc.set(self._generate_cache_key(unique_token_key),
92 | access_token.__dict__)
93 |
94 | if access_token.refresh_token is not None:
95 | rft_key = self._generate_cache_key(access_token.refresh_token)
96 | self.mc.set(rft_key, access_token.__dict__)
97 |
98 | def delete_refresh_token(self, refresh_token):
99 | """
100 | Deletes a refresh token after use
101 | :param refresh_token: The refresh token to delete.
102 | """
103 | access_token = self.fetch_by_refresh_token(refresh_token)
104 | self.mc.delete(self._generate_cache_key(access_token.token))
105 | self.mc.delete(self._generate_cache_key(refresh_token))
106 |
107 | def fetch_by_refresh_token(self, refresh_token):
108 | token_data = self.mc.get(refresh_token)
109 |
110 | if token_data is None:
111 | raise AccessTokenNotFound
112 |
113 | return AccessToken(**token_data)
114 |
115 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
116 | data = self.mc.get(self._unique_token_key(client_id, grant_type,
117 | user_id))
118 |
119 | if data is None:
120 | raise AccessTokenNotFound
121 |
122 | return AccessToken(**data)
123 |
124 | def _unique_token_key(self, client_id, grant_type, user_id):
125 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id)
126 |
127 | def _generate_cache_key(self, identifier):
128 | return self.prefix + "_" + identifier
129 |
--------------------------------------------------------------------------------
/oauth2/store/memory.py:
--------------------------------------------------------------------------------
1 | """
2 | Read or write data from or to local memory.
3 |
4 | Though not very valuable in a production setup, these store adapters are great
5 | for testing purposes.
6 | """
7 |
8 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore
9 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, \
10 | ClientNotFoundError
11 | from oauth2.datatype import Client
12 |
13 |
14 | class ClientStore(ClientStore):
15 | """
16 | Stores clients in memory.
17 | """
18 | def __init__(self):
19 | self.clients = {}
20 |
21 | def add_client(self, client_id, client_secret, redirect_uris,
22 | authorized_grants=None, authorized_response_types=None):
23 | """
24 | Add a client app.
25 |
26 | :param client_id: Identifier of the client app.
27 | :param client_secret: Secret the client app uses for authentication
28 | against the OAuth 2.0 provider.
29 | :param redirect_uris: A ``list`` of URIs to redirect to.
30 |
31 | """
32 | self.clients[client_id] = Client(
33 | identifier=client_id,
34 | secret=client_secret,
35 | redirect_uris=redirect_uris,
36 | authorized_grants=authorized_grants,
37 | authorized_response_types=authorized_response_types)
38 |
39 | return True
40 |
41 | def fetch_by_client_id(self, client_id):
42 | """
43 | Retrieve a client by its identifier.
44 |
45 | :param client_id: Identifier of a client app.
46 | :return: An instance of :class:`oauth2.Client`.
47 | :raises: ClientNotFoundError
48 |
49 | """
50 | if client_id not in self.clients:
51 | raise ClientNotFoundError
52 |
53 | return self.clients[client_id]
54 |
55 |
56 | class TokenStore(AccessTokenStore, AuthCodeStore):
57 | """
58 | Stores tokens in memory.
59 |
60 | Useful for testing purposes or APIs with a very limited set of clients.
61 | Use memcache or redis as storage to be able to scale.
62 | """
63 | def __init__(self):
64 | self.access_tokens = {}
65 | self.auth_codes = {}
66 | self.refresh_tokens = {}
67 | self.unique_token_identifier = {}
68 |
69 | def fetch_by_code(self, code):
70 | """
71 | Returns an AuthorizationCode.
72 |
73 | :param code: The authorization code.
74 | :return: An instance of :class:`oauth2.datatype.AuthorizationCode`.
75 | :raises: :class:`AuthCodeNotFound` if no data could be retrieved for
76 | given code.
77 |
78 | """
79 | if code not in self.auth_codes:
80 | raise AuthCodeNotFound
81 |
82 | return self.auth_codes[code]
83 |
84 | def save_code(self, authorization_code):
85 | """
86 | Stores the data belonging to an authorization code token.
87 |
88 | :param authorization_code: An instance of
89 | :class:`oauth2.datatype.AuthorizationCode`.
90 |
91 | """
92 | self.auth_codes[authorization_code.code] = authorization_code
93 |
94 | return True
95 |
96 | def save_token(self, access_token):
97 | """
98 | Stores an access token and additional data in memory.
99 |
100 | :param access_token: An instance of :class:`oauth2.datatype.AccessToken`.
101 | """
102 | self.access_tokens[access_token.token] = access_token
103 |
104 | unique_token_key = self._unique_token_key(access_token.client_id,
105 | access_token.grant_type,
106 | access_token.user_id)
107 |
108 | self.unique_token_identifier[unique_token_key] = access_token.token
109 |
110 | if access_token.refresh_token is not None:
111 | self.refresh_tokens[access_token.refresh_token] = access_token
112 |
113 | return True
114 |
115 | def delete_code(self, code):
116 | """
117 | Deletes an authorization code after use
118 | :param code: The authorization code.
119 | """
120 | if code in self.auth_codes:
121 | del self.auth_codes[code]
122 |
123 | def delete_refresh_token(self, refresh_token):
124 | """
125 | Deletes a refresh token after use
126 | :param refresh_token: The refresh_token.
127 | """
128 | if refresh_token in self.refresh_tokens:
129 | del self.refresh_tokens[refresh_token]
130 |
131 | def fetch_by_refresh_token(self, refresh_token):
132 | """
133 | Find an access token by its refresh token.
134 |
135 | :param refresh_token: The refresh token that was assigned to an
136 | ``AccessToken``.
137 | :return: The :class:`oauth2.datatype.AccessToken`.
138 | :raises: :class:`oauth2.error.AccessTokenNotFound`
139 | """
140 | if refresh_token not in self.refresh_tokens:
141 | raise AccessTokenNotFound
142 |
143 | return self.refresh_tokens[refresh_token]
144 |
145 | def fetch_by_token(self, token):
146 | """
147 | Returns data associated with an access token or ``None`` if no data
148 | was found.
149 |
150 | Useful for cases like validation where the access token needs to be
151 | read again.
152 |
153 | :param token: A access token code.
154 | :return: An instance of :class:`oauth2.datatype.AccessToken`.
155 | """
156 | if token not in self.access_tokens:
157 | raise AccessTokenNotFound
158 |
159 | return self.access_tokens[token]
160 |
161 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
162 | try:
163 | key = self._unique_token_key(client_id, grant_type, user_id)
164 | token = self.unique_token_identifier[key]
165 | except KeyError:
166 | raise AccessTokenNotFound
167 |
168 | return self.fetch_by_token(token)
169 |
170 | def _unique_token_key(self, client_id, grant_type, user_id):
171 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id)
172 |
--------------------------------------------------------------------------------
/oauth2/store/mongodb.py:
--------------------------------------------------------------------------------
1 | """
2 | Store adapters to read/write data to from/to mongodb using pymongo.
3 | """
4 |
5 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore
6 | from oauth2.datatype import AccessToken, AuthorizationCode, Client
7 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, \
8 | ClientNotFoundError
9 | import pymongo
10 |
11 |
12 | class MongodbStore(object):
13 | """
14 | Base class extended by all concrete store adapters.
15 | """
16 |
17 | def __init__(self, collection):
18 | self.collection = collection
19 |
20 |
21 | class AccessTokenStore(AccessTokenStore, MongodbStore):
22 | """
23 | Create a new instance like this::
24 |
25 | from pymongo import MongoClient
26 |
27 | client = MongoClient('localhost', 27017)
28 |
29 | db = client.test_database
30 |
31 | access_token_store = AccessTokenStore(collection=db["access_tokens"])
32 |
33 | """
34 |
35 | def fetch_by_refresh_token(self, refresh_token):
36 | data = self.collection.find_one({"refresh_token": refresh_token})
37 |
38 | if data is None:
39 | raise AccessTokenNotFound
40 |
41 | return AccessToken(client_id=data.get("client_id"),
42 | grant_type=data.get("grant_type"),
43 | token=data.get("token"),
44 | data=data.get("data"),
45 | expires_at=data.get("expires_at"),
46 | refresh_token=data.get("refresh_token"),
47 | refresh_expires_at=data.get("refresh_expires_at"),
48 | scopes=data.get("scopes"))
49 |
50 | def delete_refresh_token(self, refresh_token):
51 | """
52 | Deletes (invalidates) an old refresh token after use
53 | :param refresh_token: The refresh token.
54 | """
55 | self.collection.remove({"refresh_token": refresh_token})
56 |
57 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
58 | data = self.collection.find_one({"client_id": client_id,
59 | "grant_type": grant_type,
60 | "user_id": user_id},
61 | sort=[("expires_at",
62 | pymongo.DESCENDING)])
63 |
64 | if data is None:
65 | raise AccessTokenNotFound
66 |
67 | return AccessToken(client_id=data.get("client_id"),
68 | grant_type=data.get("grant_type"),
69 | token=data.get("token"),
70 | data=data.get("data"),
71 | expires_at=data.get("expires_at"),
72 | refresh_token=data.get("refresh_token"),
73 | refresh_expires_at=data.get("refresh_expires_at"),
74 | scopes=data.get("scopes"),
75 | user_id=data.get("user_id"))
76 |
77 | def save_token(self, access_token):
78 | self.collection.insert({
79 | "client_id": access_token.client_id,
80 | "grant_type": access_token.grant_type,
81 | "token": access_token.token,
82 | "data": access_token.data,
83 | "expires_at": access_token.expires_at,
84 | "refresh_token": access_token.refresh_token,
85 | "refresh_expires_at": access_token.refresh_expires_at,
86 | "scopes": access_token.scopes,
87 | "user_id": access_token.user_id})
88 |
89 | return True
90 |
91 |
92 | class AuthCodeStore(AuthCodeStore, MongodbStore):
93 | """
94 | Create a new instance like this::
95 |
96 | from pymongo import MongoClient
97 |
98 | client = MongoClient('localhost', 27017)
99 |
100 | db = client.test_database
101 |
102 | access_token_store = AuthCodeStore(collection=db["auth_codes"])
103 |
104 | """
105 |
106 | def fetch_by_code(self, code):
107 | code_data = self.collection.find_one({"code": code})
108 |
109 | if code_data is None:
110 | raise AuthCodeNotFound
111 |
112 | return AuthorizationCode(client_id=code_data.get("client_id"),
113 | code=code_data.get("code"),
114 | expires_at=code_data.get("expires_at"),
115 | redirect_uri=code_data.get("redirect_uri"),
116 | scopes=code_data.get("scopes"),
117 | data=code_data.get("data"),
118 | user_id=code_data.get("user_id"))
119 |
120 | def save_code(self, authorization_code):
121 | self.collection.insert({
122 | "client_id": authorization_code.client_id,
123 | "code": authorization_code.code,
124 | "expires_at": authorization_code.expires_at,
125 | "redirect_uri": authorization_code.redirect_uri,
126 | "scopes": authorization_code.scopes,
127 | "data": authorization_code.data,
128 | "user_id": authorization_code.user_id})
129 |
130 | return True
131 |
132 | def delete_code(self, code):
133 | """
134 | Deletes an authorization code after use
135 | :param code: The authorization code.
136 | """
137 | self.collection.remove({"code": code})
138 |
139 |
140 | class ClientStore(ClientStore, MongodbStore):
141 | """
142 | Create a new instance like this::
143 |
144 | from pymongo import MongoClient
145 |
146 | client = MongoClient('localhost', 27017)
147 |
148 | db = client.test_database
149 |
150 | access_token_store = ClientStore(collection=db["clients"])
151 |
152 | """
153 |
154 | def fetch_by_client_id(self, client_id):
155 | client_data = self.collection.find_one({"identifier": client_id})
156 |
157 | if client_data is None:
158 | raise ClientNotFoundError
159 |
160 | return Client(
161 | identifier=client_data.get("identifier"),
162 | secret=client_data.get("secret"),
163 | redirect_uris=client_data.get("redirect_uris"),
164 | authorized_grants=client_data.get("authorized_grants"),
165 | authorized_response_types=client_data.get(
166 | "authorized_response_types"
167 | ))
168 |
--------------------------------------------------------------------------------
/oauth2/store/redisdb.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import redis
3 | import json
4 |
5 | from oauth2.datatype import AccessToken, AuthorizationCode, Client
6 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, \
7 | ClientNotFoundError
8 | from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore
9 |
10 |
11 | class RedisStore(object):
12 | """
13 | Uses redis to store access tokens and auth tokens.
14 |
15 | This Store supports ``redis``. Arguments are passed to the
16 | underlying client implementation.
17 |
18 | Initialization::
19 |
20 | import redisdb
21 |
22 | token_store = TokenStore(host="127.0.0.1",
23 | port=6379,
24 | db=0
25 | )
26 |
27 | """
28 | def __init__(self, rs=None, prefix="oauth2", *args, **kwargs):
29 | self.prefix = prefix
30 |
31 | if rs is not None:
32 | self.rs = rs
33 | else:
34 | self.rs = redis.StrictRedis(*args, **kwargs)
35 |
36 | def delete(self, name):
37 | cache_key = self._generate_cache_key(name)
38 |
39 | self.rs.delete(cache_key)
40 |
41 | def write(self, name, data):
42 | cache_key = self._generate_cache_key(name)
43 |
44 | self.rs.set(cache_key, json.dumps(data))
45 |
46 | def read(self, name):
47 | cache_key = self._generate_cache_key(name)
48 |
49 | data = self.rs.get(cache_key)
50 |
51 | if data is None:
52 | return None
53 |
54 | return json.loads(data.decode("utf-8"))
55 |
56 | def _generate_cache_key(self, identifier):
57 | return self.prefix + "_" + identifier
58 |
59 |
60 | class TokenStore(AccessTokenStore, AuthCodeStore, RedisStore):
61 | def fetch_by_code(self, code):
62 | """
63 | Returns data belonging to an authorization code from redis or
64 | ``None`` if no data was found.
65 |
66 | See :class:`oauth2.store.AuthCodeStore`.
67 |
68 | """
69 | code_data = self.read(code)
70 |
71 | if code_data is None:
72 | raise AuthCodeNotFound
73 |
74 | return AuthorizationCode(**code_data)
75 |
76 | def save_code(self, authorization_code):
77 | """
78 | Stores the data belonging to an authorization code token in redis.
79 |
80 | See :class:`oauth2.store.AuthCodeStore`.
81 |
82 | """
83 | self.write(authorization_code.code,
84 | {"client_id": authorization_code.client_id,
85 | "code": authorization_code.code,
86 | "expires_at": authorization_code.expires_at,
87 | "redirect_uri": authorization_code.redirect_uri,
88 | "scopes": authorization_code.scopes,
89 | "data": authorization_code.data,
90 | "user_id": authorization_code.user_id})
91 |
92 | def delete_code(self, code):
93 | """
94 | Deletes an authorization code after use
95 | :param code: The authorization code.
96 | """
97 | self.delete(code)
98 |
99 | def save_token(self, access_token):
100 | """
101 | Stores the access token and additional data in redis.
102 |
103 | See :class:`oauth2.store.AccessTokenStore`.
104 |
105 | """
106 | self.write(access_token.token, access_token.__dict__)
107 |
108 | unique_token_key = self._unique_token_key(access_token.client_id,
109 | access_token.grant_type,
110 | access_token.user_id)
111 | self.write(unique_token_key, access_token.__dict__)
112 |
113 | if access_token.refresh_token is not None:
114 | self.write(access_token.refresh_token, access_token.__dict__)
115 |
116 | def delete_refresh_token(self, refresh_token):
117 | """
118 | Deletes a refresh token after use
119 | :param refresh_token: The refresh token to delete.
120 | """
121 | access_token = self.fetch_by_refresh_token(refresh_token)
122 |
123 | self.delete(access_token.token)
124 |
125 | def fetch_by_refresh_token(self, refresh_token):
126 | token_data = self.read(refresh_token)
127 |
128 | if token_data is None:
129 | raise AccessTokenNotFound
130 |
131 | return AccessToken(**token_data)
132 |
133 | def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
134 | unique_token_key = self._unique_token_key(client_id=client_id,
135 | grant_type=grant_type,
136 | user_id=user_id)
137 | token_data = self.read(unique_token_key)
138 |
139 | if token_data is None:
140 | raise AccessTokenNotFound
141 |
142 | return AccessToken(**token_data)
143 |
144 | def _unique_token_key(self, client_id, grant_type, user_id):
145 | return "{0}_{1}_{2}".format(client_id, grant_type, user_id)
146 |
147 |
148 | class ClientStore(ClientStore, RedisStore):
149 | def add_client(self, client_id, client_secret, redirect_uris,
150 | authorized_grants=None, authorized_response_types=None):
151 | """
152 | Add a client app.
153 |
154 | :param client_id: Identifier of the client app.
155 | :param client_secret: Secret the client app uses for authentication
156 | against the OAuth 2.0 provider.
157 | :param redirect_uris: A ``list`` of URIs to redirect to.
158 |
159 | """
160 | self.write(client_id,
161 | {"identifier": client_id,
162 | "secret": client_secret,
163 | "redirect_uris": redirect_uris,
164 | "authorized_grants": authorized_grants,
165 | "authorized_response_types": authorized_response_types})
166 |
167 | return True
168 |
169 | def fetch_by_client_id(self, client_id):
170 | client_data = self.read(client_id)
171 |
172 | if client_data is None:
173 | raise ClientNotFoundError
174 |
175 | return Client(identifier=client_data["identifier"],
176 | secret=client_data["secret"],
177 | redirect_uris=client_data["redirect_uris"],
178 | authorized_grants=client_data["authorized_grants"],
179 | authorized_response_types=client_data["authorized_response_types"])
180 |
--------------------------------------------------------------------------------
/oauth2/test/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | # Enables unit tests to work under Python 2.6
4 | # Code copied from
5 | # https://github.com/facebook/tornado/blob/master/tornado/test/util.py
6 | if sys.version_info >= (2, 7):
7 | import unittest
8 | else:
9 | import unittest2 as unittest
10 |
--------------------------------------------------------------------------------
/oauth2/test/store/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wndhydrnt/python-oauth2/d1f75e321bac049291925b9ee345bf4218f5b7a9/oauth2/test/store/__init__.py
--------------------------------------------------------------------------------
/oauth2/test/store/test_memcache.py:
--------------------------------------------------------------------------------
1 | from mock import Mock, call
2 | from oauth2.datatype import AuthorizationCode, AccessToken
3 | from oauth2.error import AuthCodeNotFound, AccessTokenNotFound
4 | from oauth2.store.memcache import TokenStore
5 | from oauth2.test import unittest
6 |
7 | class MemcacheTokenStoreTestCase(unittest.TestCase):
8 | def setUp(self):
9 | self.cache_prefix = "test"
10 |
11 | def _generate_test_cache_key(self, key):
12 | return self.cache_prefix + "_" + key
13 |
14 | def test_fetch_by_code(self):
15 | code = "abc"
16 | saved_data = {"client_id": "myclient", "code": code,
17 | "expires_at": 100, "redirect_uri": "http://localhost",
18 | "scopes": ["foo_read", "foo_write"],
19 | "data": {"name": "test"}}
20 |
21 | mc_mock = Mock(spec=["get"])
22 | mc_mock.get.return_value = saved_data
23 |
24 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
25 |
26 | auth_code = store.fetch_by_code(code)
27 |
28 | mc_mock.get.assert_called_with(self._generate_test_cache_key(code))
29 | self.assertEqual(auth_code.client_id, saved_data["client_id"])
30 | self.assertEqual(auth_code.code, saved_data["code"])
31 | self.assertEqual(auth_code.expires_at, saved_data["expires_at"])
32 | self.assertEqual(auth_code.redirect_uri, saved_data["redirect_uri"])
33 | self.assertEqual(auth_code.scopes, saved_data["scopes"])
34 | self.assertEqual(auth_code.data, saved_data["data"])
35 |
36 | def test_fetch_by_code_no_data(self):
37 | mc_mock = Mock(spec=["get"])
38 | mc_mock.get.return_value = None
39 |
40 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
41 |
42 | with self.assertRaises(AuthCodeNotFound):
43 | store.fetch_by_code("abc")
44 |
45 | def test_save_code(self):
46 | data = {"client_id": "myclient", "code": "abc", "expires_at": 100,
47 | "redirect_uri": "http://localhost",
48 | "scopes": ["foo_read", "foo_write"],
49 | "data": {"name": "test"}, "user_id": 1}
50 |
51 | auth_code = AuthorizationCode(**data)
52 |
53 | cache_key = self._generate_test_cache_key(data["code"])
54 |
55 | mc_mock = Mock(spec=["set"])
56 |
57 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
58 |
59 | store.save_code(auth_code)
60 |
61 | mc_mock.set.assert_called_with(cache_key, data)
62 |
63 | def test_save_token(self):
64 | data = {"client_id": "myclient", "token": "xyz",
65 | "data": {"name": "test"}, "scopes": ["foo_read", "foo_write"],
66 | "expires_at": None, "refresh_token": "mno",
67 | "refresh_expires_at": None,
68 | "grant_type": "authorization_code",
69 | "user_id": 123}
70 |
71 | access_token = AccessToken(**data)
72 |
73 | cache_key = self._generate_test_cache_key(access_token.token)
74 | refresh_token_key = self._generate_test_cache_key(access_token.refresh_token)
75 | unique_token_key = self._generate_test_cache_key(
76 | "{0}_{1}_{2}".format(access_token.client_id,
77 | access_token.grant_type,
78 | access_token.user_id)
79 | )
80 |
81 | mc_mock = Mock(spec=["set"])
82 |
83 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
84 |
85 | store.save_token(access_token)
86 |
87 | mc_mock.set.assert_has_calls([call(cache_key, data),
88 | call(unique_token_key, data),
89 | call(refresh_token_key, data)])
90 |
91 | def test_fetch_existing_token_of_user(self):
92 | data = {"client_id": "myclient", "token": "xyz",
93 | "data": {"name": "test"}, "scopes": ["foo_read", "foo_write"],
94 | "expires_at": None, "refresh_token": "mno",
95 | "grant_type": "authorization_code",
96 | "user_id": 123}
97 |
98 | mc_mock = Mock(spec=["get"])
99 | mc_mock.get.return_value = data
100 |
101 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
102 |
103 | access_token = store.fetch_existing_token_of_user(
104 | client_id="myclient",
105 | grant_type="authorization_code",
106 | user_id=123)
107 |
108 | self.assertTrue(isinstance(access_token, AccessToken))
109 |
110 | def test_fetch_existing_token_of_user_no_data(self):
111 | mc_mock = Mock(spec=["get"])
112 | mc_mock.get.return_value = None
113 |
114 | store = TokenStore(mc=mc_mock, prefix=self.cache_prefix)
115 |
116 | with self.assertRaises(AccessTokenNotFound):
117 | store.fetch_existing_token_of_user(client_id="myclient",
118 | grant_type="authorization_code",
119 | user_id=123)
120 |
--------------------------------------------------------------------------------
/oauth2/test/store/test_memory.py:
--------------------------------------------------------------------------------
1 | from oauth2.datatype import AuthorizationCode, AccessToken
2 | from oauth2.error import ClientNotFoundError, AuthCodeNotFound
3 | from oauth2.store.memory import ClientStore, TokenStore
4 | from oauth2.test import unittest
5 |
6 | class MemoryClientStoreTestCase(unittest.TestCase):
7 | def test_add_client_and_fetch_by_client_id(self):
8 | expected_client_data = {"client_id": "abc", "client_secret": "xyz",
9 | "redirect_uris": ["http://localhost"]}
10 |
11 | store = ClientStore()
12 |
13 | success = store.add_client(expected_client_data["client_id"],
14 | expected_client_data["client_secret"],
15 | expected_client_data["redirect_uris"])
16 | self.assertTrue(success)
17 |
18 | client = store.fetch_by_client_id("abc")
19 |
20 | self.assertEqual(client.identifier, expected_client_data["client_id"])
21 | self.assertEqual(client.secret, expected_client_data["client_secret"])
22 | self.assertEqual(client.redirect_uris, expected_client_data["redirect_uris"])
23 |
24 | def test_fetch_by_client_id_no_client(self):
25 | store = ClientStore()
26 |
27 | with self.assertRaises(ClientNotFoundError):
28 | store.fetch_by_client_id("abc")
29 |
30 | class MemoryTokenStoreTestCase(unittest.TestCase):
31 | def setUp(self):
32 | self.access_token_data = {"client_id": "myclient",
33 | "token": "xyz",
34 | "scopes": ["foo_read", "foo_write"],
35 | "data": {"name": "test"},
36 | "grant_type": "authorization_code"}
37 | self.auth_code = AuthorizationCode("myclient", "abc", 100,
38 | "http://localhost",
39 | ["foo_read", "foo_write"],
40 | {"name": "test"})
41 |
42 | self.test_store = TokenStore()
43 |
44 | def test_fetch_by_code(self):
45 | with self.assertRaises(AuthCodeNotFound):
46 | self.test_store.fetch_by_code("unknown")
47 |
48 | def test_save_code_and_fetch_by_code(self):
49 | success = self.test_store.save_code(self.auth_code)
50 | self.assertTrue(success)
51 |
52 | result = self.test_store.fetch_by_code(self.auth_code.code)
53 |
54 | self.assertEqual(result, self.auth_code)
55 |
56 | def test_save_token_and_fetch_by_token(self):
57 | access_token = AccessToken(**self.access_token_data)
58 |
59 | success = self.test_store.save_token(access_token)
60 | self.assertTrue(success)
61 |
62 | result = self.test_store.fetch_by_token(access_token.token)
63 |
64 | self.assertEqual(result, access_token)
65 |
--------------------------------------------------------------------------------
/oauth2/test/store/test_mongodb.py:
--------------------------------------------------------------------------------
1 | from oauth2.test import unittest
2 | from oauth2.store.mongodb import AccessTokenStore, AuthCodeStore, \
3 | ClientStore
4 | from mock import Mock
5 | from oauth2.datatype import AccessToken, AuthorizationCode, Client
6 | from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, \
7 | ClientNotFoundError
8 |
9 |
10 | class MongodbAccessTokenStoreTestCase(unittest.TestCase):
11 | def setUp(self):
12 | self.access_token_data = {"client_id": "myclient",
13 | "grant_type": "authorization_code",
14 | "token": "xyz",
15 | "scopes": ["foo_read", "foo_write"],
16 | "data": {"name": "test"},
17 | "expires_at": 1000,
18 | "refresh_token": "abcd",
19 | "refresh_expires_at": 2000,
20 | "user_id": None}
21 |
22 | def test_fetch_by_refresh_token(self):
23 | refresh_token = "abcd"
24 |
25 | self.access_token_data["refresh_token"] = refresh_token
26 |
27 | collection_mock = Mock(spec=["find_one"])
28 | collection_mock.find_one.return_value = self.access_token_data
29 |
30 | store = AccessTokenStore(collection=collection_mock)
31 | token = store.fetch_by_refresh_token(refresh_token=refresh_token)
32 |
33 | collection_mock.find_one.assert_called_with(
34 | {"refresh_token": refresh_token})
35 | self.assertTrue(isinstance(token, AccessToken))
36 | self.assertDictEqual(token.__dict__, self.access_token_data)
37 |
38 | def test_fetch_by_refresh_token_no_data(self):
39 | collection_mock = Mock(spec=["find_one"])
40 | collection_mock.find_one.return_value = None
41 |
42 | store = AccessTokenStore(collection=collection_mock)
43 |
44 | with self.assertRaises(AccessTokenNotFound):
45 | store.fetch_by_refresh_token(refresh_token="abcd")
46 |
47 | def test_fetch_existing_token_of_user(self):
48 | test_data = {"client_id": "myclient",
49 | "grant_type": "authorization_code",
50 | "token": "xyz",
51 | "scopes": ["foo_read", "foo_write"],
52 | "data": {"name": "test"},
53 | "expires_at": 1000,
54 | "refresh_token": "abcd",
55 | "refresh_expires_at": 2000,
56 | "user_id": 123}
57 |
58 | collection_mock = Mock(spec=["find_one"])
59 | collection_mock.find_one.return_value = test_data
60 |
61 | store = AccessTokenStore(collection=collection_mock)
62 |
63 | token = store.fetch_existing_token_of_user(client_id="myclient",
64 | grant_type="authorization_code",
65 | user_id=123)
66 |
67 | self.assertTrue(isinstance(token, AccessToken))
68 | self.assertDictEqual(token.__dict__, test_data)
69 | collection_mock.find_one.assert_called_with({"client_id": "myclient",
70 | "grant_type": "authorization_code",
71 | "user_id": 123},
72 | sort=[("expires_at", -1)])
73 |
74 | def test_fetch_existing_token_of_user_no_data(self):
75 | collection_mock = Mock(spec=["find_one"])
76 | collection_mock.find_one.return_value = None
77 |
78 | store = AccessTokenStore(collection=collection_mock)
79 |
80 | with self.assertRaises(AccessTokenNotFound):
81 | store.fetch_existing_token_of_user(client_id="myclient",
82 | grant_type="authorization_code",
83 | user_id=123)
84 |
85 | def test_save_token(self):
86 | access_token = AccessToken(**self.access_token_data)
87 |
88 | collection_mock = Mock(spec=["insert"])
89 |
90 | store = AccessTokenStore(collection=collection_mock)
91 | store.save_token(access_token)
92 |
93 | collection_mock.insert.assert_called_with(self.access_token_data)
94 |
95 | class MongodbAuthCodeStoreTestCase(unittest.TestCase):
96 | def setUp(self):
97 | self.auth_code_data = {"client_id": "myclient", "expires_at": 1000,
98 | "redirect_uri": "https://redirect",
99 | "scopes": ["foo", "bar"], "data": {},
100 | "user_id": None}
101 |
102 | self.collection_mock = Mock(spec=["find_one", "insert", "remove"])
103 |
104 | def test_fetch_by_code(self):
105 | code = "abcd"
106 |
107 | self.collection_mock.find_one.return_value = self.auth_code_data
108 |
109 | self.auth_code_data["code"] = "abcd"
110 |
111 | store = AuthCodeStore(collection=self.collection_mock)
112 | auth_code = store.fetch_by_code(code=code)
113 |
114 | self.collection_mock.find_one.assert_called_with({"code": "abcd"})
115 | self.assertTrue(isinstance(auth_code, AuthorizationCode))
116 | self.assertDictEqual(auth_code.__dict__, self.auth_code_data)
117 |
118 | def test_fetch_by_code_no_data(self):
119 | self.collection_mock.find_one.return_value = None
120 |
121 | store = AuthCodeStore(collection=self.collection_mock)
122 |
123 | with self.assertRaises(AuthCodeNotFound):
124 | store.fetch_by_code(code="abcd")
125 |
126 | def test_save_code(self):
127 | self.auth_code_data["code"] = "abcd"
128 |
129 | auth_code = AuthorizationCode(**self.auth_code_data)
130 |
131 | store = AuthCodeStore(collection=self.collection_mock)
132 | store.save_code(auth_code)
133 |
134 | self.collection_mock.insert.assert_called_with(self.auth_code_data)
135 |
136 | class MongodbClientStoreTestCase(unittest.TestCase):
137 | def test_fetch_by_client_id(self):
138 | client_data = {"identifier": "testclient", "secret": "k#4g6",
139 | "redirect_uris": ["https://redirect"],
140 | "authorized_grants": []}
141 |
142 | collection_mock = Mock(spec=["find_one"])
143 | collection_mock.find_one.return_value = client_data
144 |
145 | store = ClientStore(collection=collection_mock)
146 | client = store.fetch_by_client_id(client_id=client_data["identifier"])
147 |
148 | collection_mock.find_one.assert_called_with({
149 | "identifier": client_data["identifier"]})
150 | self.assertTrue(isinstance(client, Client))
151 | self.assertEqual(client.identifier, client_data["identifier"])
152 |
153 | def test_fetch_by_client_id_no_data(self):
154 | collection_mock = Mock(spec=["find_one"])
155 | collection_mock.find_one.return_value = None
156 |
157 | store = ClientStore(collection=collection_mock)
158 |
159 | with self.assertRaises(ClientNotFoundError):
160 | store.fetch_by_client_id(client_id="testclient")
161 |
--------------------------------------------------------------------------------
/oauth2/test/store/test_redisdb.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import json
4 | from mock import Mock
5 |
6 | from oauth2.datatype import AccessToken
7 | from oauth2.store.redisdb import TokenStore
8 | from oauth2.test import unittest
9 |
10 |
11 | class TokenStoreTestCase(unittest.TestCase):
12 | def test_delete_refresh_token(self):
13 | refresh_token_id = "def"
14 | access_token = AccessToken(client_id="abc", grant_type="token",
15 | token="xyz")
16 |
17 | redisdb_mock = Mock(spec=["delete", "get"])
18 | redisdb_mock.get.return_value = bytes(json.dumps(access_token.__dict__).encode('utf-8'))
19 |
20 | store = TokenStore(rs=redisdb_mock)
21 | store.delete_refresh_token(refresh_token_id)
22 |
23 | self.assertEqual(1, redisdb_mock.delete.call_count)
24 |
--------------------------------------------------------------------------------
/oauth2/test/test_client_authenticator.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from base64 import b64encode
3 | from oauth2.test import unittest
4 | from mock import Mock
5 | from oauth2.client_authenticator import ClientAuthenticator, http_basic_auth, \
6 | request_body
7 | from oauth2.datatype import Client
8 | from oauth2.error import OAuthInvalidNoRedirectError, ClientNotFoundError,\
9 | OAuthInvalidError
10 | from oauth2.store import ClientStore
11 | from oauth2.web.wsgi import Request
12 |
13 |
14 | class ClientAuthenticatorTestCase(unittest.TestCase):
15 | def setUp(self):
16 | self.client = Client(identifier="abc", secret="xyz",
17 | authorized_grants=["authorization_code"],
18 | authorized_response_types=["code"],
19 | redirect_uris=["http://callback"])
20 | self.client_store_mock = Mock(spec=ClientStore)
21 |
22 | self.source_mock = Mock()
23 |
24 | self.authenticator = ClientAuthenticator(
25 | client_store=self.client_store_mock,
26 | source=self.source_mock)
27 |
28 | def test_by_identifier(self):
29 | redirect_uri = "http://callback"
30 |
31 | self.client_store_mock.fetch_by_client_id.return_value = self.client
32 |
33 | request_mock = Mock(spec=Request)
34 | request_mock.get_param.side_effect = [self.client.identifier,
35 | redirect_uri]
36 |
37 | client = self.authenticator.by_identifier(request=request_mock)
38 |
39 | self.client_store_mock.fetch_by_client_id.\
40 | assert_called_with(self.client.identifier)
41 | self.assertEqual(client.redirect_uri, redirect_uri)
42 |
43 | def test_by_identifier_client_id_not_set(self):
44 | request_mock = Mock(spec=Request)
45 | request_mock.get_param.return_value = None
46 |
47 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected:
48 | self.authenticator.by_identifier(request=request_mock)
49 |
50 | self.assertEqual(expected.exception.error, "missing_client_id")
51 |
52 | def test_by_identifier_unknown_client(self):
53 | request_mock = Mock(spec=Request)
54 | request_mock.get_param.return_value = "def"
55 |
56 | self.client_store_mock.fetch_by_client_id.\
57 | side_effect = ClientNotFoundError
58 |
59 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected:
60 | self.authenticator.by_identifier(request=request_mock)
61 |
62 | self.assertEqual(expected.exception.error, "unknown_client")
63 |
64 | def test_by_identifier_unknown_redirect_uri(self):
65 | response_type = "code"
66 | unknown_redirect_uri = "http://unknown.com"
67 |
68 | request_mock = Mock(spec=Request)
69 | request_mock.get_param.side_effect = [self.client.identifier,
70 | response_type,
71 | unknown_redirect_uri]
72 |
73 | self.client_store_mock.fetch_by_client_id.return_value = self.client
74 |
75 | with self.assertRaises(OAuthInvalidNoRedirectError) as expected:
76 | self.authenticator.by_identifier(request=request_mock)
77 |
78 | self.assertEqual(expected.exception.error, "invalid_redirect_uri")
79 |
80 | def test_by_identifier_secret(self):
81 | client_id = "abc"
82 | client_secret = "xyz"
83 | grant_type = "authorization_code"
84 |
85 | request_mock = Mock(spec=Request)
86 | request_mock.post_param.return_value = grant_type
87 |
88 | self.source_mock.return_value = (client_id, client_secret)
89 |
90 | self.client_store_mock.fetch_by_client_id.return_value = self.client
91 |
92 | self.authenticator.by_identifier_secret(request=request_mock)
93 | self.client_store_mock.fetch_by_client_id.\
94 | assert_called_with(client_id)
95 |
96 | def test_by_identifier_secret_unknown_client(self):
97 | client_id = "def"
98 | client_secret = "uvw"
99 |
100 | self.source_mock.return_value = (client_id, client_secret)
101 |
102 | request_mock = Mock(spec=Request)
103 |
104 | self.client_store_mock.fetch_by_client_id.\
105 | side_effect = ClientNotFoundError
106 |
107 | with self.assertRaises(OAuthInvalidError) as expected:
108 | self.authenticator.by_identifier_secret(request_mock)
109 |
110 | self.assertEqual(expected.exception.error, "invalid_client")
111 |
112 | def test_by_identifier_secret_client_not_authorized(self):
113 | client_id = "abc"
114 | client_secret = "xyz"
115 | grant_type = "client_credentials"
116 |
117 | self.source_mock.return_value = (client_id, client_secret)
118 |
119 | request_mock = Mock(spec=Request)
120 | request_mock.post_param.return_value = grant_type
121 |
122 | self.client_store_mock.fetch_by_client_id.return_value = self.client
123 |
124 | with self.assertRaises(OAuthInvalidError) as expected:
125 | self.authenticator.by_identifier_secret(request_mock)
126 |
127 | self.assertEqual(expected.exception.error, "unauthorized_client")
128 |
129 | def test_by_identifier_secret_wrong_secret(self):
130 | client_id = "abc"
131 | client_secret = "uvw"
132 | grant_type = "authorization_code"
133 |
134 | self.source_mock.return_value = (client_id, client_secret)
135 |
136 | request_mock = Mock(spec=Request)
137 | request_mock.post_param.return_value = grant_type
138 |
139 | self.client_store_mock.fetch_by_client_id.return_value = self.client
140 |
141 | with self.assertRaises(OAuthInvalidError) as expected:
142 | self.authenticator.by_identifier_secret(request_mock)
143 |
144 | self.assertEqual(expected.exception.error, "invalid_client")
145 |
146 |
147 | class RequestBodyTestCase(unittest.TestCase):
148 | def test_valid(self):
149 | client_id = "abc"
150 | client_secret = "secret"
151 |
152 | request_mock = Mock(spec=Request)
153 | request_mock.post_param.side_effect = [client_id, client_secret]
154 |
155 | result = request_body(request_mock)
156 |
157 | self.assertEqual(result[0], client_id)
158 | self.assertEqual(result[1], client_secret)
159 |
160 | def test_no_client_id(self):
161 | request_mock = Mock(spec=Request)
162 | request_mock.post_param.return_value = None
163 |
164 | with self.assertRaises(OAuthInvalidError) as expected:
165 | request_body(request_mock)
166 |
167 | self.assertEqual(expected.exception.error, "invalid_request")
168 |
169 | def test_no_client_secret(self):
170 | request_mock = Mock(spec=Request)
171 | request_mock.post_param.side_effect = ["abc", None]
172 |
173 | with self.assertRaises(OAuthInvalidError) as expected:
174 | request_body(request_mock)
175 |
176 | self.assertEqual(expected.exception.error, "invalid_request")
177 |
178 |
179 | class HttpBasicAuthTestCase(unittest.TestCase):
180 | def test_valid(self):
181 | client_id = "testclient"
182 | client_secret = "secret"
183 |
184 | credentials = "{0}:{1}".format(client_id, client_secret)
185 |
186 | encoded = b64encode(credentials.encode("latin1"))
187 |
188 | request_mock = Mock(spec=Request)
189 | request_mock.header.return_value = "Basic {0}".\
190 | format(encoded.decode("latin1"))
191 |
192 | result_client_id, result_client_secret = http_basic_auth(request=request_mock)
193 |
194 | request_mock.header.assert_called_with("authorization")
195 |
196 | self.assertEqual(result_client_id, client_id)
197 | self.assertEqual(result_client_secret, client_secret)
198 |
199 | def test_header_not_present(self):
200 | request_mock = Mock(spec=Request)
201 | request_mock.header.return_value = None
202 |
203 | with self.assertRaises(OAuthInvalidError) as expected:
204 | http_basic_auth(request=request_mock)
205 |
206 | self.assertEqual(expected.exception.error, "invalid_request")
207 |
208 | def test_invalid_authorization_header(self):
209 | request_mock = Mock(spec=Request)
210 | request_mock.header.return_value = "some-data"
211 |
212 | with self.assertRaises(OAuthInvalidError) as expected:
213 | http_basic_auth(request=request_mock)
214 |
215 | self.assertEqual(expected.exception.error, "invalid_request")
216 |
--------------------------------------------------------------------------------
/oauth2/test/test_datatype.py:
--------------------------------------------------------------------------------
1 | from oauth2.test import unittest
2 | from mock import patch
3 | from oauth2.datatype import AccessToken, Client
4 | from oauth2.error import RedirectUriUnknown
5 |
6 |
7 | def mock_time():
8 | return 1000
9 |
10 |
11 | class AccessTokenTestCase(unittest.TestCase):
12 | @patch("time.time", mock_time)
13 | def test_expires_in_expired(self):
14 | access_token = AccessToken(client_id="abc",
15 | grant_type="client_credentials",
16 | token="def", expires_at=999)
17 |
18 | self.assertEqual(access_token.expires_in, 0)
19 |
20 | @patch("time.time", mock_time)
21 | def test_expires_in_not_expired(self):
22 | access_token = AccessToken(client_id="abc",
23 | grant_type="client_credentials",
24 | token="def", expires_at=1100)
25 |
26 | self.assertEqual(access_token.expires_in, 100)
27 |
28 | def test_is_expired_expired_at_not_set(self):
29 | access_token = AccessToken(client_id="abc",
30 | grant_type="client_credentials",
31 | token="def")
32 |
33 | self.assertFalse(access_token.is_expired())
34 |
35 |
36 | class ClientTestCase(unittest.TestCase):
37 | def test_redirect_uri(self):
38 | client = Client(identifier="abc", secret="xyz",
39 | redirect_uris=["http://callback"])
40 |
41 | self.assertEqual(client.redirect_uri, "http://callback")
42 | client.redirect_uri = "http://callback"
43 | self.assertEqual(client.redirect_uri, "http://callback")
44 |
45 | with self.assertRaises(RedirectUriUnknown):
46 | client.redirect_uri = "http://another.callback"
47 |
48 | def test_response_type_supported(self):
49 | client = Client(identifier="abc", secret="xyz",
50 | authorized_grants=["test_grant"])
51 |
52 | self.assertTrue(client.grant_type_supported("test_grant"))
53 | self.assertFalse(client.grant_type_supported("unknown_grant"))
54 |
55 | def test_response_type_supported(self):
56 | client = Client(identifier="abc", secret="xyz",
57 | authorized_response_types=["test_response_type"])
58 |
59 | self.assertTrue(client.response_type_supported("test_response_type"))
60 | self.assertFalse(client.response_type_supported("unknown"))
61 |
--------------------------------------------------------------------------------
/oauth2/test/test_oauth2.py:
--------------------------------------------------------------------------------
1 | import json
2 | from mock import Mock
3 | from oauth2.error import OAuthInvalidNoRedirectError, OAuthInvalidError
4 | from oauth2.test import unittest
5 | from oauth2 import Provider
6 | from oauth2.store import ClientStore
7 | from oauth2.web import Response, AuthorizationCodeGrantSiteAdapter, \
8 | ResourceOwnerGrantSiteAdapter
9 | from oauth2.web.wsgi import Request
10 | from oauth2.grant import RefreshToken, AuthorizationCodeGrant, GrantHandler, \
11 | ResourceOwnerGrant
12 |
13 |
14 | class ProviderTestCase(unittest.TestCase):
15 | def setUp(self):
16 | self.client_store_mock = Mock(spec=ClientStore)
17 | self.token_generator_mock = Mock()
18 |
19 | self.response_mock = Mock(spec=Response)
20 | self.response_mock.body = ""
21 | response_class_mock = Mock(return_value=self.response_mock)
22 |
23 | self.token_generator_mock.expires_in = {}
24 | self.token_generator_mock.refresh_expires_in = 0
25 |
26 | self.auth_server = Provider(access_token_store=Mock(),
27 | auth_code_store=Mock(),
28 | client_store=self.client_store_mock,
29 | token_generator=self.token_generator_mock,
30 | response_class=response_class_mock)
31 |
32 | def test_add_grant_set_expire_time(self):
33 | """
34 | Provider.add_grant() should set the expiration time on the instance of TokenGenerator
35 | """
36 | self.auth_server.add_grant(
37 | AuthorizationCodeGrant(
38 | expires_in=400,
39 | site_adapter=Mock(spec=AuthorizationCodeGrantSiteAdapter)
40 | )
41 | )
42 | self.auth_server.add_grant(
43 | ResourceOwnerGrant(
44 | expires_in=500,
45 | site_adapter=Mock(spec=ResourceOwnerGrantSiteAdapter)
46 | )
47 | )
48 | self.auth_server.add_grant(RefreshToken(expires_in=1200))
49 |
50 | self.assertEqual(self.token_generator_mock.expires_in[AuthorizationCodeGrant.grant_type], 400)
51 | self.assertEqual(self.token_generator_mock.expires_in[ResourceOwnerGrant.grant_type], 500)
52 | self.assertEqual(self.token_generator_mock.refresh_expires_in, 1200)
53 |
54 | def test_dispatch(self):
55 | environ = {"session": "data"}
56 | process_result = "response"
57 |
58 | request_mock = Mock(spec=Request)
59 |
60 | grant_handler_mock = Mock(spec=["process", "read_validate_params"])
61 | grant_handler_mock.process.return_value = process_result
62 |
63 | grant_factory_mock = Mock(return_value=grant_handler_mock)
64 |
65 | self.auth_server.site_adapter = Mock(
66 | spec=AuthorizationCodeGrantSiteAdapter
67 | )
68 | self.auth_server.add_grant(grant_factory_mock)
69 | result = self.auth_server.dispatch(request_mock, environ)
70 |
71 | grant_factory_mock.assert_called_with(request_mock, self.auth_server)
72 | grant_handler_mock.read_validate_params.\
73 | assert_called_with(request_mock)
74 | grant_handler_mock.process.assert_called_with(request_mock,
75 | self.response_mock,
76 | environ)
77 | self.assertEqual(result, process_result)
78 |
79 | def test_dispatch_no_grant_type_found(self):
80 | error_body = {
81 | "error": "unsupported_response_type",
82 | "error_description": "Grant not supported"
83 | }
84 |
85 | request_mock = Mock(spec=Request)
86 |
87 | result = self.auth_server.dispatch(request_mock, {})
88 |
89 | self.response_mock.add_header.assert_called_with("Content-Type",
90 | "application/json")
91 | self.assertEqual(self.response_mock.status_code, 400)
92 | self.assertEqual(self.response_mock.body, json.dumps(error_body))
93 | self.assertEqual(result, self.response_mock)
94 |
95 | def test_dispatch_no_client_found(self):
96 | request_mock = Mock(spec=Request)
97 |
98 | grant_handler_mock = Mock(spec=GrantHandler)
99 | grant_handler_mock.process.side_effect = OAuthInvalidNoRedirectError(
100 | error="")
101 |
102 | grant_factory_mock = Mock(return_value=grant_handler_mock)
103 |
104 | self.auth_server.add_grant(grant_factory_mock)
105 | self.auth_server.dispatch(request_mock, {})
106 |
107 | self.response_mock.add_header.assert_called_with("Content-Type",
108 | "application/json")
109 | self.assertEqual(self.response_mock.status_code, 400)
110 | self.assertEqual(json.loads(self.response_mock.body)["error"], "invalid_redirect_uri")
111 |
112 | def test_dispatch_general_exception(self):
113 | request_mock = Mock(spec=Request)
114 |
115 | grant_handler_mock = Mock(spec=GrantHandler)
116 | grant_handler_mock.process.side_effect = KeyError
117 |
118 | grant_factory_mock = Mock(return_value=grant_handler_mock)
119 |
120 | self.auth_server.add_grant(grant_factory_mock)
121 | self.auth_server.dispatch(request_mock, {})
122 |
123 | self.assertTrue(grant_handler_mock.handle_error.called)
124 |
--------------------------------------------------------------------------------
/oauth2/test/test_tokengenerator.py:
--------------------------------------------------------------------------------
1 | import re
2 | from oauth2.test import unittest
3 | from oauth2.tokengenerator import URandomTokenGenerator, Uuid4
4 |
5 | class URandomTokenGeneratorTestCase(unittest.TestCase):
6 | def test_generate(self):
7 | length = 20
8 |
9 | generator = URandomTokenGenerator(length=length)
10 |
11 | result = generator.generate()
12 |
13 | self.assertTrue(isinstance(result, str))
14 | self.assertEqual(len(result), length)
15 |
16 | class Uuid4TestCase(unittest.TestCase):
17 | def setUp(self):
18 | self.uuid_regex = r"^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}-[a-z0-9]{12}$"
19 |
20 | def test_create_access_token_data_no_expiration(self):
21 | generator = Uuid4()
22 |
23 | result = generator.create_access_token_data('test_grant_type')
24 |
25 | self.assertRegexpMatches(result["access_token"], self.uuid_regex)
26 | self.assertEqual(result["token_type"], "Bearer")
27 |
28 | def test_create_access_token_data_with_expiration(self):
29 | generator = Uuid4()
30 | generator.expires_in = {'test_grant_type':600}
31 |
32 | result = generator.create_access_token_data('test_grant_type')
33 |
34 | self.assertRegexpMatches(result["access_token"], self.uuid_regex)
35 | self.assertEqual(result["token_type"], "Bearer")
36 | self.assertRegexpMatches(result["refresh_token"], self.uuid_regex)
37 | self.assertEqual(result["expires_in"], 600)
38 |
39 | def test_generate(self):
40 | generator = Uuid4()
41 |
42 | result = generator.generate()
43 |
44 | regex = re.compile(self.uuid_regex)
45 |
46 | match = regex.match(result)
47 |
48 | self.assertEqual(result, match.group())
49 |
50 | if __name__ == "__main__":
51 | unittest.main()
52 |
--------------------------------------------------------------------------------
/oauth2/test/test_web.py:
--------------------------------------------------------------------------------
1 | from oauth2.test import unittest
2 | from mock import Mock
3 | from oauth2.web import Response
4 | from oauth2.web.wsgi import Request, Application
5 | from oauth2 import Provider
6 |
7 |
8 | class RequestTestCase(unittest.TestCase):
9 | def test_initialization_no_post_data(self):
10 | request_method = "TEST"
11 | query_string = "foo=bar&baz=buz"
12 |
13 | environment = {"REQUEST_METHOD": request_method,
14 | "QUERY_STRING": query_string,
15 | "PATH_INFO": "/"}
16 |
17 | request = Request(environment)
18 |
19 | self.assertEqual(request.method, request_method)
20 | self.assertEqual(request.query_params, {"foo": "bar", "baz": "buz"})
21 | self.assertEqual(request.query_string, query_string)
22 | self.assertEqual(request.post_params, {})
23 |
24 | def test_initialization_with_post_data(self):
25 | content_length = "42"
26 | request_method = "POST"
27 | query_string = ""
28 | content = "foo=bar&baz=buz".encode('utf-8')
29 |
30 | wsgi_input_mock = Mock(spec=["read"])
31 | wsgi_input_mock.read.return_value = content
32 |
33 | environment = {"CONTENT_LENGTH": content_length,
34 | "CONTENT_TYPE": "application/x-www-form-urlencoded",
35 | "REQUEST_METHOD": request_method,
36 | "QUERY_STRING": query_string,
37 | "PATH_INFO": "/",
38 | "wsgi.input": wsgi_input_mock}
39 |
40 | request = Request(environment)
41 |
42 | wsgi_input_mock.read.assert_called_with(int(content_length))
43 | self.assertEqual(request.method, request_method)
44 | self.assertEqual(request.query_params, {})
45 | self.assertEqual(request.query_string, query_string)
46 | self.assertEqual(request.post_params, {"foo": "bar", "baz": "buz"})
47 |
48 | def test_get_param(self):
49 | request_method = "TEST"
50 | query_string = "foo=bar&baz=buz"
51 |
52 | environment = {"REQUEST_METHOD": request_method,
53 | "QUERY_STRING": query_string,
54 | "PATH_INFO": "/a-url"}
55 |
56 | request = Request(environment)
57 |
58 | result = request.get_param("foo")
59 |
60 | self.assertEqual(result, "bar")
61 |
62 | result_default = request.get_param("na")
63 |
64 | self.assertEqual(result_default, None)
65 |
66 | def test_post_param(self):
67 | content_length = "42"
68 | request_method = "POST"
69 | query_string = ""
70 | content = "foo=bar&baz=buz".encode('utf-8')
71 |
72 | wsgi_input_mock = Mock(spec=["read"])
73 | wsgi_input_mock.read.return_value = content
74 |
75 | environment = {"CONTENT_LENGTH": content_length,
76 | "CONTENT_TYPE": "application/x-www-form-urlencoded",
77 | "REQUEST_METHOD": request_method,
78 | "QUERY_STRING": query_string,
79 | "PATH_INFO": "/",
80 | "wsgi.input": wsgi_input_mock}
81 |
82 | request = Request(environment)
83 |
84 | result = request.post_param("foo")
85 |
86 | self.assertEqual(result, "bar")
87 |
88 | result_default = request.post_param("na")
89 |
90 | self.assertEqual(result_default, None)
91 |
92 | wsgi_input_mock.read.assert_called_with(int(content_length))
93 |
94 | def test_header(self):
95 | environment = {"REQUEST_METHOD": "GET",
96 | "QUERY_STRING": "",
97 | "PATH_INFO": "/",
98 | "HTTP_AUTHORIZATION": "Basic abcd"}
99 |
100 | request = Request(env=environment)
101 |
102 | self.assertEqual(request.header("authorization"), "Basic abcd")
103 | self.assertIsNone(request.header("unknown"))
104 | self.assertEqual(request.header("unknown", default=0), 0)
105 |
106 |
107 | class ServerTestCase(unittest.TestCase):
108 | def test_call(self):
109 | body = "body"
110 | headers = {"header": "value"}
111 | path = "/authorize"
112 | status_code = 200
113 | http_code = "200 OK"
114 |
115 | environment = {"PATH_INFO": path, "myvar": "value"}
116 |
117 | request_mock = Mock(spec=Request)
118 | request_class_mock = Mock(return_value=request_mock)
119 |
120 | response_mock = Mock(spec=Response)
121 | response_mock.body = body
122 | response_mock.headers = headers
123 | response_mock.status_code = status_code
124 |
125 | provider_mock = Mock(spec=Provider)
126 | provider_mock.dispatch.return_value = response_mock
127 |
128 | start_response_mock = Mock()
129 |
130 | wsgi = Application(provider=provider_mock, authorize_uri=path,
131 | request_class=request_class_mock,
132 | env_vars=["myvar"])
133 | result = wsgi(environment, start_response_mock)
134 |
135 | request_class_mock.assert_called_with(environment)
136 | provider_mock.dispatch.assert_called_with(request_mock,
137 | {"myvar": "value"})
138 | start_response_mock.assert_called_with(http_code,
139 | list(headers.items()))
140 | self.assertEqual(result, [body.encode('utf-8')])
141 |
--------------------------------------------------------------------------------
/oauth2/tokengenerator.py:
--------------------------------------------------------------------------------
1 | """
2 | Provides various implementations of algorithms to generate an Access Token or
3 | Refresh Token.
4 | """
5 |
6 | import hashlib
7 | import os
8 | import uuid
9 |
10 |
11 | class TokenGenerator(object):
12 | """
13 | Base class of every token generator.
14 | """
15 | def __init__(self):
16 | """
17 | Create a new instance of a token generator.
18 | """
19 | self.expires_in = {}
20 | self.refresh_expires_in = 0
21 |
22 | def create_access_token_data(self, grant_type):
23 | """
24 | Create data needed by an access token.
25 |
26 | :param grant_type:
27 | :type grant_type: str
28 |
29 | :return: A ``dict`` containing he ``access_token`` and the
30 | ``token_type``. If the value of ``TokenGenerator.expires_in``
31 | is larger than 0, a ``refresh_token`` will be generated too.
32 | :rtype: dict
33 | """
34 | result = {"access_token": self.generate(), "token_type": "Bearer"}
35 |
36 | if self.expires_in.get(grant_type, 0) > 0:
37 | result["refresh_token"] = self.generate()
38 |
39 | result["expires_in"] = self.expires_in[grant_type]
40 |
41 | return result
42 |
43 | def generate(self):
44 | """
45 | Implemented by generators extending this base class.
46 |
47 | :raises NotImplementedError:
48 | """
49 | raise NotImplementedError
50 |
51 |
52 | class URandomTokenGenerator(TokenGenerator):
53 | """
54 | Create a token using ``os.urandom()``.
55 | """
56 | def __init__(self, length=40):
57 | self.token_length = length
58 | TokenGenerator.__init__(self)
59 |
60 | def generate(self):
61 | """
62 | :return: A new token
63 | :rtype: str
64 | """
65 | random_data = os.urandom(100)
66 |
67 | hash_gen = hashlib.new("sha512")
68 | hash_gen.update(random_data)
69 |
70 | return hash_gen.hexdigest()[:self.token_length]
71 |
72 |
73 | class Uuid4(TokenGenerator):
74 | """
75 | Generate a token using uuid4.
76 | """
77 | def generate(self):
78 | """
79 | :return: A new token
80 | :rtype: str
81 | """
82 | return str(uuid.uuid4())
83 |
--------------------------------------------------------------------------------
/oauth2/web/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | class AuthenticatingSiteAdapter(object):
6 | """
7 | Extended by site adapters that need to authenticate the user.
8 | """
9 | def authenticate(self, request, environ, scopes, client):
10 | """
11 | Authenticates a user and checks if she has authorized access.
12 |
13 | :param request: Incoming request data.
14 | :type request: oauth2.web.Request
15 |
16 | :param environ: Environment variables of the request.
17 | :type environ: dict
18 |
19 | :param scopes: A list of strings with each string being one requested
20 | scope.
21 | :type scopes: list
22 |
23 | :param client: The client that initiated the authorization process
24 | :type client: oauth2.datatype.Client
25 |
26 | :return: A ``dict`` containing arbitrary data that will be passed to
27 | the current storage adapter and saved with auth code and
28 | access token. Return a tuple in the form
29 | `(additional_data, user_id)` if you want to use
30 | :doc:`unique_token`.
31 | :rtype: dict
32 |
33 | :raises oauth2.error.UserNotAuthenticated: If the user could not be
34 | authenticated.
35 | """
36 | raise NotImplementedError
37 |
38 |
39 | class UserFacingSiteAdapter(object):
40 | """
41 | Extended by site adapters that need to interact with the user.
42 |
43 | Display HTML or redirect the user agent to another page of your website
44 | where she can do something before being returned to the OAuth 2.0 server.
45 | """
46 | def render_auth_page(self, request, response, environ, scopes, client):
47 | """
48 | Defines how to display a confirmation page to the user.
49 |
50 | :param request: Incoming request data.
51 | :type request: oauth2.web.Request
52 |
53 | :param response: Response to return to a client.
54 | :type response: oauth2.web.Response
55 |
56 | :param environ: Environment variables of the request.
57 | :type environ: dict
58 |
59 | :param scopes: A list of strings with each string being one requested
60 | scope.
61 | :type scopes: list
62 |
63 | :param client: The client that initiated the authorization process
64 | :type client: oauth2.datatype.Client
65 |
66 | :return: The response passed in as a parameter.
67 | It can contain HTML or issue a redirect.
68 | :rtype: oauth2.web.Response
69 | """
70 | raise NotImplementedError
71 |
72 | def user_has_denied_access(self, request):
73 | """
74 | Checks if the user has denied access. This will lead to python-oauth2
75 | returning a "acess_denied" response to the requesting client app.
76 |
77 | :param request: Incoming request data.
78 | :type request: oauth2.web.Request
79 |
80 | :return: Return ``True`` if the user has denied access.
81 | :rtype: bool
82 | """
83 | raise NotImplementedError
84 |
85 |
86 | class AuthorizationCodeGrantSiteAdapter(UserFacingSiteAdapter,
87 | AuthenticatingSiteAdapter):
88 | """
89 | Definition of a site adapter as required by
90 | :class:`oauth2.grant.AuthorizationCodeGrant`.
91 | """
92 | pass
93 |
94 |
95 | class ImplicitGrantSiteAdapter(UserFacingSiteAdapter,
96 | AuthenticatingSiteAdapter):
97 | """
98 | Definition of a site adapter as required by
99 | :class:`oauth2.grant.ImplicitGrant`.
100 | """
101 | pass
102 |
103 |
104 | class ResourceOwnerGrantSiteAdapter(AuthenticatingSiteAdapter):
105 | """
106 | Definition of a site adapter as required by
107 | :class:`oauth2.grant.ResourceOwnerGrant`.
108 | """
109 | pass
110 |
111 |
112 | class Request(object):
113 | """
114 | Base class defining the interface of a request.
115 | """
116 | @property
117 | def method(self):
118 | """
119 | Returns the HTTP method of the request.
120 | """
121 | raise NotImplementedError
122 |
123 | @property
124 | def path(self):
125 | """
126 | Returns the current path portion of the current uri.
127 |
128 | Used by some grants to determine which action to take.
129 | """
130 | raise NotImplementedError
131 |
132 | def get_param(self, name, default=None):
133 | """
134 | Retrieve a parameter from the query string of the request.
135 | """
136 | raise NotImplementedError
137 |
138 | def header(self, name, default=None):
139 | """
140 | Retrieve a header of the request.
141 | """
142 | raise NotImplementedError
143 |
144 | def post_param(self, name, default=None):
145 | """
146 | Retrieve a parameter from the body of the request.
147 | """
148 | raise NotImplementedError
149 |
150 |
151 | class Response(object):
152 | """
153 | Contains data returned to the requesting user agent.
154 | """
155 | def __init__(self):
156 | self.status_code = 200
157 | self._headers = {"Content-Type": "text/html"}
158 | self.body = ""
159 |
160 | @property
161 | def headers(self):
162 | return self._headers
163 |
164 | def add_header(self, header, value):
165 | """
166 | Add a header to the response.
167 | """
168 | self._headers[header] = str(value)
169 |
--------------------------------------------------------------------------------
/oauth2/web/tornado.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | .. warning::
6 |
7 | Tornado support is currently experimental.
8 |
9 | Use Tornado to serve token requests:
10 |
11 | .. literalinclude:: examples/tornado_server.py
12 | """
13 |
14 | from __future__ import absolute_import
15 |
16 | from tornado.web import RequestHandler
17 |
18 |
19 | class Request(object):
20 | def __init__(self, handler):
21 | """
22 | :param handler: Handler of the current request
23 | :type handler: :class:`tornado.web.RequestHandler`
24 | """
25 | self.handler = handler
26 |
27 | @property
28 | def method(self):
29 | return self.handler.request.method
30 |
31 | @property
32 | def path(self):
33 | return self.handler.request.path
34 |
35 | @property
36 | def query_string(self):
37 | return self.handler.request.query
38 |
39 | def get_param(self, name, default=None):
40 | return self.handler.get_query_argument(name=name, default=default)
41 |
42 | def header(self, name, default=None):
43 | return self.handler.request.headers[name]
44 |
45 | def post_param(self, name, default=None):
46 | return self.handler.get_body_argument(name=name, default=default)
47 |
48 |
49 | class OAuth2Handler(RequestHandler):
50 | def initialize(self, provider):
51 | """
52 | :type provider: :class:`oauth2.Provider`
53 | """
54 | self.provider = provider
55 |
56 | def get(self):
57 | response = self._dispatch_request()
58 |
59 | self._map_response(response)
60 |
61 | def post(self):
62 | response = self._dispatch_request()
63 |
64 | self._map_response(response)
65 |
66 | def _dispatch_request(self):
67 | return self.provider.dispatch(request=Request(handler=self),
68 | environ=dict())
69 |
70 | def _map_response(self, response):
71 | for name, value in list(response.headers.items()):
72 | self.set_header(name, value)
73 |
74 | self.set_status(response.status_code)
75 | self.write(response.body)
76 |
--------------------------------------------------------------------------------
/oauth2/web/wsgi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | Classes for handling a HTTP request/response flow.
6 |
7 | .. versionchanged:: 1.0.0
8 | Moved from package ``oauth2.web`` to ``oauth2.web.wsgi``.
9 | """
10 |
11 | from oauth2.compatibility import parse_qs
12 |
13 |
14 | class Request(object):
15 | """
16 | Contains data of the current HTTP request.
17 | """
18 | def __init__(self, env):
19 | """
20 | :param env: Wsgi environment
21 | """
22 | self.method = env["REQUEST_METHOD"]
23 | self.query_params = {}
24 | self.query_string = env["QUERY_STRING"]
25 | self.path = env["PATH_INFO"]
26 | self.post_params = {}
27 | self.env_raw = env
28 |
29 | for param, value in parse_qs(env["QUERY_STRING"]).items():
30 | self.query_params[param] = value[0]
31 |
32 | if (self.method == "POST"
33 | and env["CONTENT_TYPE"].startswith("application/x-www-form-urlencoded")):
34 | self.post_params = {}
35 | content = env['wsgi.input'].read(int(env['CONTENT_LENGTH']))
36 | post_params = parse_qs(content)
37 |
38 | for param, value in post_params.items():
39 | decoded_param = param.decode('utf-8')
40 | decoded_value = value[0].decode('utf-8')
41 | self.post_params[decoded_param] = decoded_value
42 |
43 | def get_param(self, name, default=None):
44 | """
45 | Returns a param of a GET request identified by its name.
46 | """
47 | try:
48 | return self.query_params[name]
49 | except KeyError:
50 | return default
51 |
52 | def post_param(self, name, default=None):
53 | """
54 | Returns a param of a POST request identified by its name.
55 | """
56 | try:
57 | return self.post_params[name]
58 | except KeyError:
59 | return default
60 |
61 | def header(self, name, default=None):
62 | """
63 | Returns the value of the HTTP header identified by `name`.
64 | """
65 | wsgi_header = "HTTP_{0}".format(name.upper())
66 |
67 | try:
68 | return self.env_raw[wsgi_header]
69 | except KeyError:
70 | return default
71 |
72 |
73 | class Application(object):
74 | """
75 | Implements WSGI.
76 |
77 | .. versionchanged:: 1.0.0
78 | Renamed from ``Server`` to ``Application``.
79 | """
80 | HTTP_CODES = {200: "200 OK",
81 | 301: "301 Moved Permanently",
82 | 302: "302 Found",
83 | 400: "400 Bad Request",
84 | 401: "401 Unauthorized",
85 | 404: "404 Not Found"}
86 |
87 | def __init__(self, provider, authorize_uri="/authorize", env_vars=None,
88 | request_class=Request, token_uri="/token"):
89 | self.authorize_uri = authorize_uri
90 | self.env_vars = env_vars
91 | self.request_class = request_class
92 | self.provider = provider
93 | self.token_uri = token_uri
94 |
95 | self.provider.authorize_path = authorize_uri
96 | self.provider.token_path = token_uri
97 |
98 | def __call__(self, env, start_response):
99 | environ = {}
100 |
101 | if (env["PATH_INFO"] != self.authorize_uri
102 | and env["PATH_INFO"] != self.token_uri):
103 | start_response("404 Not Found",
104 | [('Content-type', 'text/html')])
105 | return [b"Not Found"]
106 |
107 | request = self.request_class(env)
108 |
109 | if isinstance(self.env_vars, list):
110 | for varname in self.env_vars:
111 | if varname in env:
112 | environ[varname] = env[varname]
113 |
114 | response = self.provider.dispatch(request, environ)
115 |
116 | start_response(self.HTTP_CODES[response.status_code],
117 | list(response.headers.items()))
118 |
119 | return [response.body.encode('utf-8')]
120 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mock
2 | nose
3 | pymongo
4 | python-memcached
5 | redis
6 | tornado
7 | http://dev.mysql.com/get/Downloads/Connector-Python/mysql-connector-python-1.1.7.tar.gz
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import setup
4 | from oauth2 import VERSION
5 |
6 | setup(name="python-oauth2",
7 | version=VERSION,
8 | description="OAuth 2.0 provider for python",
9 | long_description=open("README.rst").read(),
10 | author="Markus Meyer",
11 | author_email="hydrantanderwand@gmail.com",
12 | url="https://github.com/wndhydrnt/python-oauth2",
13 | packages=[d[0].replace("/", ".") for d in os.walk("oauth2") if not d[0].endswith("__pycache__")],
14 | extras_require={
15 | "memcache": ["python-memcached"],
16 | "mongodb": ["pymongo"],
17 | "redis": ["redis"]
18 | },
19 | classifiers=[
20 | "Development Status :: 4 - Beta",
21 | "License :: OSI Approved :: MIT License",
22 | "Programming Language :: Python :: 2",
23 | "Programming Language :: Python :: 2.7",
24 | "Programming Language :: Python :: 3",
25 | "Programming Language :: Python :: 3.4",
26 | "Programming Language :: Python :: 3.5",
27 | "Programming Language :: Python :: 3.6",
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/vagrant/Berksfile:
--------------------------------------------------------------------------------
1 | source "http://api.berkshelf.com"
2 |
3 | cookbook 'memcached'
4 | cookbook 'mongodb'
5 | cookbook 'mysql'
6 | cookbook 'redisio'
7 | cookbook 'ulimit'
8 |
--------------------------------------------------------------------------------
/vagrant/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'berkshelf'
4 |
5 |
--------------------------------------------------------------------------------
/vagrant/README.md:
--------------------------------------------------------------------------------
1 | Development VM
2 | ==============
3 |
4 | The development VM helps to quickly set up a dev environment.
5 | It uses VirtualBox and Vagrant to create the virtual machine.
6 | The VM uses the ``precise64`` image provided by Vagrant.
7 | All data is stored in a local mongodb which is installed automatically.
8 |
9 | Requirements
10 | ------------
11 |
12 | - [VirtualBox](https://www.virtualbox.org/wiki/Downloads) to create the VM.
13 | - [Vagrant](http://downloads.vagrantup.com/) to set up the VM.
14 | - [vagrant-omnibus](https://github.com/schisamo/vagrant-omnibus) to keep Chef up-to-date.
15 | - [Bundler](http://bundler.io/) to install gems
16 |
17 | Setup
18 | -----
19 |
20 | Go into the ``/vagrant`` sub-directory and use vagrant to boot up the VM:
21 |
22 | $ cd ./vagrant
23 | $ bundle install // install gems. this will install berkshelf.
24 | $ bundle exec berks vendor ./cookbooks // install chef cookbooks
25 | $ vagrant up
26 |
27 | Creating the VM can take several minutes.
28 |
29 | Starting the oauth2 server
30 | ------------------------
31 |
32 | After the VM has booted up, you can start the oauth2 server:
33 |
34 | $ vagrant ssh
35 | vagrant@precise64$ python /vagrant/start_provider.py
36 |
37 | The server listens on port ``8888``.
38 | You can now start to obtain tokens:
39 |
40 | $ curl "http://127.0.0.1:8888/authorize?response_type=code&client_id=tc&state=xyz" --verbose
41 |
--------------------------------------------------------------------------------
/vagrant/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure("2") do |config|
5 | # All Vagrant configuration is done here. The most common configuration
6 | # options are documented and commented below. For a complete reference,
7 | # please see the online documentation at vagrantup.com.
8 |
9 | # Every Vagrant virtual environment requires a box to build off of.
10 | config.vm.box = "precise64"
11 |
12 | # The url from where the 'config.vm.box' box will be fetched if it
13 | # doesn't already exist on the user's system.
14 | config.vm.box_url = "http://files.vagrantup.com/precise64.box"
15 |
16 | # Create a forwarded port mapping which allows access to a specific port
17 | # within the machine from a port on the host machine. In the example below,
18 | # accessing "localhost:8080" will access port 80 on the guest machine.
19 | config.vm.network :forwarded_port, guest: 8888, host: 8888
20 |
21 | # Create a private network, which allows host-only access to the machine
22 | # using a specific IP.
23 | # config.vm.network :private_network, ip: "192.168.33.10"
24 |
25 | # Create a public network, which generally matched to bridged network.
26 | # Bridged networks make the machine appear as another physical device on
27 | # your network.
28 | # config.vm.network :public_network
29 |
30 | # Share an additional folder to the guest VM. The first argument is
31 | # the path on the host to the actual folder. The second argument is
32 | # the path on the guest to mount the folder. And the optional third
33 | # argument is a set of non-required options.
34 | # config.vm.synced_folder "../data", "/vagrant_data"
35 | config.vm.synced_folder "../", "/opt/python-oauth2"
36 |
37 | # Provider-specific configuration so you can fine-tune various
38 | # backing providers for Vagrant. These expose provider-specific options.
39 | # Example for VirtualBox:
40 | #
41 | config.vm.provider :virtualbox do |vb|
42 | # Use VBoxManage to customize the VM. For example to change memory:
43 | vb.customize ["modifyvm", :id, "--memory", "512"]
44 | end
45 | #
46 | # View the documentation for the provider you're using for more
47 | # information on available options.
48 |
49 | # Enable provisioning with chef solo, specifying a cookbooks path, roles
50 | # path, and data_bags path (all relative to this Vagrantfile), and adding
51 | # some recipes and/or roles.
52 | #
53 | # config.vm.provision "shell", inline: "wget -O - https://www.opscode.com/chef/install.sh | sudo bash"
54 |
55 | config.omnibus.chef_version = :latest
56 |
57 | config.vm.provision :chef_solo do |chef|
58 | chef.cookbooks_path = "cookbooks"
59 |
60 | chef.add_recipe "apt"
61 | chef.add_recipe "memcached"
62 | chef.add_recipe "mongodb::10gen_repo"
63 | chef.add_recipe "mongodb"
64 | chef.add_recipe "mysql::server"
65 | chef.add_recipe "redisio::install"
66 | chef.add_recipe "redisio::enable"
67 |
68 | # You may also specify custom JSON attributes:
69 | chef.json = {
70 | 'mysql' => {
71 | 'server_root_password' => ''
72 | }
73 | }
74 | end
75 |
76 | config.vm.provision "shell", path: "setup.sh"
77 | end
78 |
--------------------------------------------------------------------------------
/vagrant/create_testclient.py:
--------------------------------------------------------------------------------
1 | import mysql.connector
2 | from pymongo import MongoClient
3 |
4 | client_id = "tc"
5 | client_secret = "abc"
6 | authorized_grants = ["authorization_code", "client_credentials", "password",
7 | "refresh_token"]
8 | authorized_response_types = ["code", "token"]
9 | redirect_uris = ["http://127.0.0.1/index.html"]
10 |
11 |
12 | def create_in_mongodb():
13 | client = MongoClient()
14 |
15 | db = client.testdb
16 |
17 | clients = db.clients
18 |
19 | client = clients.find_one({"identifier": client_id})
20 |
21 | if client is None:
22 | print("Creating test client in mongodb...")
23 | clients.insert({"identifier": client_id, "secret": client_secret,
24 | "authorized_grants": authorized_grants,
25 | "authorized_response_types": authorized_response_types,
26 | "redirect_uris": redirect_uris})
27 |
28 |
29 | def create_in_mysql():
30 | connection = mysql.connector.connect(host="127.0.0.1", user="root",
31 | passwd="", db="testdb")
32 |
33 | check_client = connection.cursor()
34 | check_client.execute("SELECT * FROM clients WHERE identifier = %s", (client_id,))
35 | client_data = check_client.fetchone()
36 | check_client.close()
37 |
38 | if client_data is None:
39 | print("Creating client in mysql...")
40 | create_client = connection.cursor()
41 |
42 | create_client.execute("""
43 | INSERT INTO clients (
44 | identifier, secret
45 | ) VALUES (
46 | %s, %s
47 | )""", (client_id, client_secret))
48 |
49 | client_id_in_mysql = create_client.lastrowid
50 |
51 | connection.commit()
52 |
53 | create_client.close()
54 |
55 | for authorized_grant in authorized_grants:
56 | create_grant = connection.cursor()
57 |
58 | create_grant.execute("""
59 | INSERT INTO client_grants (
60 | name, client_id
61 | ) VALUES (
62 | %s, %s
63 | )""", (authorized_grant, client_id_in_mysql))
64 |
65 | connection.commit()
66 |
67 | create_grant.close()
68 |
69 | for response_type in authorized_response_types:
70 | create_response_type = connection.cursor()
71 |
72 | create_response_type.execute("""
73 | INSERT INTO client_response_types (
74 | response_type, client_id
75 | ) VALUES (
76 | %s, %s
77 | )""", (response_type, client_id_in_mysql))
78 |
79 | connection.commit()
80 |
81 | create_response_type.close()
82 |
83 | for redirect_uri in redirect_uris:
84 | create_redirect_uri = connection.cursor()
85 |
86 | create_redirect_uri.execute("""
87 | INSERT INTO client_redirect_uris (
88 | redirect_uri, client_id
89 | ) VALUES (
90 | %s, %s
91 | )""", (redirect_uri, client_id_in_mysql))
92 |
93 | connection.commit()
94 |
95 | create_redirect_uri.close()
96 |
97 |
98 | create_in_mysql()
99 |
100 | create_in_mongodb()
101 |
--------------------------------------------------------------------------------
/vagrant/mysql-schema.sql:
--------------------------------------------------------------------------------
1 | -- MySQL Script generated by MySQL Workbench
2 | -- Sun May 11 20:26:06 2014
3 | -- Model: New Model Version: 1.0
4 | SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
5 | SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
6 | SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
7 |
8 | -- -----------------------------------------------------
9 | -- Schema testdb
10 | -- -----------------------------------------------------
11 | CREATE SCHEMA IF NOT EXISTS `testdb` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
12 | USE `testdb` ;
13 |
14 | -- -----------------------------------------------------
15 | -- Table `testdb`.`access_tokens`
16 | -- -----------------------------------------------------
17 | CREATE TABLE IF NOT EXISTS `testdb`.`access_tokens` (
18 | `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier',
19 | `client_id` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client. Assuming it is an arbitrary text which is a maximum of 32 characters long.',
20 | `grant_type` ENUM('authorization_code', 'implicit', 'password', 'client_credentials', 'refresh_token') NOT NULL COMMENT 'The type of a grant for which a token has been issued.',
21 | `token` CHAR(36) NOT NULL COMMENT 'The access token.',
22 | `expires_at` TIMESTAMP NULL COMMENT 'The timestamp at which the token expires.',
23 | `refresh_token` CHAR(36) NULL COMMENT 'The refresh token.',
24 | `refresh_expires_at` TIMESTAMP NULL COMMENT 'The timestamp at which the refresh token expires.',
25 | `user_id` INT NULL COMMENT 'The identifier of the user this token belongs to.',
26 | PRIMARY KEY (`id`),
27 | INDEX `fetch_by_refresh_token` (`refresh_token` ASC),
28 | INDEX `fetch_existing_token_of_user` (`client_id` ASC, `grant_type` ASC, `user_id` ASC))
29 | ENGINE = InnoDB;
30 |
31 |
32 | -- -----------------------------------------------------
33 | -- Table `testdb`.`access_token_scopes`
34 | -- -----------------------------------------------------
35 | CREATE TABLE IF NOT EXISTS `testdb`.`access_token_scopes` (
36 | `id` INT NOT NULL AUTO_INCREMENT,
37 | `name` VARCHAR(32) NOT NULL COMMENT 'The name of scope.',
38 | `access_token_id` INT NOT NULL COMMENT 'The unique identifier of the access token this scope belongs to.',
39 | PRIMARY KEY (`id`))
40 | ENGINE = InnoDB;
41 |
42 |
43 | -- -----------------------------------------------------
44 | -- Table `testdb`.`access_token_data`
45 | -- -----------------------------------------------------
46 | CREATE TABLE IF NOT EXISTS `testdb`.`access_token_data` (
47 | `id` INT NOT NULL AUTO_INCREMENT,
48 | `key` VARCHAR(32) NOT NULL COMMENT 'The key of an entry converted to the key in a Python dict.',
49 | `value` VARCHAR(32) NOT NULL COMMENT 'The value of an entry converted to the value in a Python dict.',
50 | `access_token_id` INT NOT NULL COMMENT 'The unique identifier of the access token a row belongs to.',
51 | PRIMARY KEY (`id`))
52 | ENGINE = InnoDB;
53 |
54 |
55 | -- -----------------------------------------------------
56 | -- Table `testdb`.`auth_codes`
57 | -- -----------------------------------------------------
58 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_codes` (
59 | `id` INT NOT NULL AUTO_INCREMENT,
60 | `client_id` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client. Assuming it is an arbitrary text which is a maximum of 32 characters long.',
61 | `code` CHAR(36) NOT NULL COMMENT 'The authorisation code.',
62 | `expires_at` TIMESTAMP NOT NULL COMMENT 'The timestamp at which the token expires.',
63 | `redirect_uri` VARCHAR(128) NULL COMMENT 'The redirect URI send by the client during the request of an authorisation code.',
64 | `user_id` INT NULL COMMENT 'The identifier of the user this authorisation code belongs to.',
65 | PRIMARY KEY (`id`),
66 | INDEX `fetch_code` (`code` ASC))
67 | ENGINE = InnoDB;
68 |
69 |
70 | -- -----------------------------------------------------
71 | -- Table `testdb`.`auth_code_data`
72 | -- -----------------------------------------------------
73 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_code_data` (
74 | `id` INT NOT NULL AUTO_INCREMENT,
75 | `key` VARCHAR(32) NOT NULL COMMENT 'The key of an entry converted to the key in a Python dict.',
76 | `value` VARCHAR(32) NOT NULL COMMENT 'The value of an entry converted to the value in a Python dict.',
77 | `auth_code_id` INT NOT NULL COMMENT 'The identifier of the authorisation code that this row belongs to.',
78 | PRIMARY KEY (`id`))
79 | ENGINE = InnoDB;
80 |
81 |
82 | -- -----------------------------------------------------
83 | -- Table `testdb`.`auth_code_scopes`
84 | -- -----------------------------------------------------
85 | CREATE TABLE IF NOT EXISTS `testdb`.`auth_code_scopes` (
86 | `id` INT NOT NULL AUTO_INCREMENT,
87 | `name` VARCHAR(32) NOT NULL,
88 | `auth_code_id` INT NOT NULL,
89 | PRIMARY KEY (`id`))
90 | ENGINE = InnoDB;
91 |
92 |
93 | -- -----------------------------------------------------
94 | -- Table `testdb`.`clients`
95 | -- -----------------------------------------------------
96 | CREATE TABLE IF NOT EXISTS `testdb`.`clients` (
97 | `id` INT NOT NULL AUTO_INCREMENT,
98 | `identifier` VARCHAR(32) NOT NULL COMMENT 'The identifier of a client.',
99 | `secret` VARCHAR(32) NOT NULL COMMENT 'The secret of a client.',
100 | PRIMARY KEY (`id`))
101 | ENGINE = InnoDB;
102 |
103 |
104 | -- -----------------------------------------------------
105 | -- Table `testdb`.`client_grants`
106 | -- -----------------------------------------------------
107 | CREATE TABLE IF NOT EXISTS `testdb`.`client_grants` (
108 | `id` INT NOT NULL AUTO_INCREMENT,
109 | `name` VARCHAR(32) NOT NULL,
110 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
111 | PRIMARY KEY (`id`))
112 | ENGINE = InnoDB;
113 |
114 |
115 | -- -----------------------------------------------------
116 | -- Table `testdb`.`client_redirect_uris`
117 | -- -----------------------------------------------------
118 | CREATE TABLE IF NOT EXISTS `testdb`.`client_redirect_uris` (
119 | `id` INT NOT NULL AUTO_INCREMENT,
120 | `redirect_uri` VARCHAR(128) NOT NULL COMMENT 'A URI of a client.',
121 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
122 | PRIMARY KEY (`id`))
123 | ENGINE = InnoDB;
124 |
125 |
126 | -- -----------------------------------------------------
127 | -- Table `testdb`.`client_response_types`
128 | -- -----------------------------------------------------
129 | CREATE TABLE IF NOT EXISTS `testdb`.`client_response_types` (
130 | `id` INT NOT NULL AUTO_INCREMENT,
131 | `response_type` VARCHAR(32) NOT NULL COMMENT 'The response type that a client can use.',
132 | `client_id` INT NOT NULL COMMENT 'The id of the client a row belongs to.',
133 | PRIMARY KEY (`id`))
134 | ENGINE = InnoDB;
135 |
136 |
137 | SET SQL_MODE=@OLD_SQL_MODE;
138 | SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
139 | SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
140 |
--------------------------------------------------------------------------------
/vagrant/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install pip and python development libs
4 | apt-get -y install python-pip python-dev libmysqlclient-dev make
5 | # Install pythonb libs
6 | pip install -r /opt/python-oauth2/requirements.txt
7 | # Make python-oauth2 available for python
8 | if ! grep -Fxq "export PYTHONPATH=/opt/python-oauth2" /home/vagrant/.bashrc
9 | then
10 | echo "export PYTHONPATH=/opt/python-oauth2" >> /home/vagrant/.bashrc
11 | fi
12 | # Create the testdb database in mysql
13 | mysql -uroot < /vagrant/mysql-schema.sql
14 | # Execute script to create a testclient entry in mongodb
15 | python /vagrant/create_testclient.py
16 |
--------------------------------------------------------------------------------
/vagrant/start_provider.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import mysql.connector
3 | from pymongo import MongoClient
4 |
5 | from wsgiref.simple_server import make_server
6 |
7 | from oauth2 import Provider
8 | from oauth2.store.dbapi.mysql import MysqlAccessTokenStore, MysqlAuthCodeStore, \
9 | MysqlClientStore
10 | from oauth2.store.mongodb import AccessTokenStore, AuthCodeStore, ClientStore
11 | from oauth2.tokengenerator import Uuid4
12 | from oauth2.web import SiteAdapter, Wsgi
13 | from oauth2.grant import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerGrant,\
14 | RefreshToken, ClientCredentialsGrant
15 |
16 |
17 | class TestSiteAdapter(SiteAdapter):
18 | def authenticate(self, request, environ, response):
19 | return {}, 123
20 |
21 | def user_has_denied_access(self, request):
22 | return False
23 |
24 |
25 | def main():
26 | parser = argparse.ArgumentParser(description="python-oauth2 test provider")
27 | parser.add_argument("--store", dest="store", type=str, default="mongodb",
28 | help="The store adapter to use. Can one of 'mongodb'"\
29 | "(default), 'mysql'")
30 | args = parser.parse_args()
31 |
32 | if args.store == "mongodb":
33 | print("Using mongodb stores...")
34 | client = MongoClient()
35 |
36 | db = client.testdb
37 |
38 | access_token_store = AccessTokenStore(collection=db["access_tokens"])
39 | auth_code_store = AuthCodeStore(collection=db["auth_codes"])
40 | client_store = ClientStore(collection=db["clients"])
41 | elif args.store == "mysql":
42 | print("Using mysql stores...")
43 | connection = mysql.connector.connect(host="127.0.0.1", user="root",
44 | passwd="", db="testdb")
45 |
46 | access_token_store = MysqlAccessTokenStore(connection=connection)
47 | auth_code_store = MysqlAuthCodeStore(connection=connection)
48 | client_store = MysqlClientStore(connection=connection)
49 | else:
50 | raise Exception("Unknown store")
51 |
52 | provider = Provider(access_token_store=access_token_store,
53 | auth_code_store=auth_code_store,
54 | client_store=client_store,
55 | site_adapter=TestSiteAdapter(),
56 | token_generator=Uuid4())
57 |
58 | provider.add_grant(AuthorizationCodeGrant(expires_in=120))
59 | provider.add_grant(ImplicitGrant())
60 | provider.add_grant(ResourceOwnerGrant())
61 | provider.add_grant(ClientCredentialsGrant())
62 | provider.add_grant(RefreshToken(expires_in=60))
63 |
64 | app = Wsgi(server=provider)
65 |
66 | try:
67 | httpd = make_server('', 8888, app)
68 | print("Starting test auth server on port 8888...")
69 | httpd.serve_forever()
70 | except KeyboardInterrupt:
71 | httpd.server_close()
72 |
73 | if __name__ == "__main__":
74 | main()
75 |
--------------------------------------------------------------------------------