flask-oidc-1.4.0/0000775000175000017500000000000013263723662016160 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/README.rst0000664000175000017500000000171113137632531017641 0ustar puiterwijkpuiterwijk00000000000000flask-oidc ========== `OpenID Connect `_ support for `Flask `_. .. image:: https://img.shields.io/pypi/v/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://img.shields.io/pypi/dm/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://readthedocs.org/projects/flask-oidc/badge/?version=latest :target: http://flask-oidc.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/travis/puiterwijk/flask-oidc.svg?style=flat :target: https://travis-ci.org/puiterwijk/flask-oidc This library should work with any standards compliant OpenID Connect provider. It has been tested with: * `Google+ Login `_ * `Ipsilon `_ Project status ============== This project is in active development. flask-oidc-1.4.0/flask_oidc/0000775000175000017500000000000013263723662020256 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/flask_oidc/registration.py0000664000175000017500000001267313137632531023345 0ustar puiterwijkpuiterwijk00000000000000# Copyright (c) 2016, Patrick Uiterwijk # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json import httplib2 from flask_oidc import _json_loads def check_redirect_uris(uris, client_type=None): """ This function checks all return uris provided and tries to deduce as what type of client we should register. :param uris: The redirect URIs to check. :type uris: list :param client_type: An indicator of which client type you are expecting to be used. If this does not match the deduced type, an error will be raised. :type client_type: str :returns: The deduced client type. :rtype: str :raises ValueError: An error occured while checking the redirect uris. .. versionadded:: 1.0 """ if client_type not in [None, 'native', 'web']: raise ValueError('Invalid client type indicator used') if not isinstance(uris, list): raise ValueError('uris needs to be a list of strings') if len(uris) < 1: raise ValueError('At least one return URI needs to be provided') for uri in uris: if uri.startswith('https://'): if client_type == 'native': raise ValueError('https url with native client') client_type = 'web' elif uri.startswith('http://localhost'): if client_type == 'web': raise ValueError('http://localhost url with web client') client_type = 'native' else: if (uri.startswith('http://') and not uri.startswith('http://localhost')): raise ValueError('http:// url with non-localhost is illegal') else: raise ValueError('Invalid uri provided: %s' % uri) return client_type class RegistrationError(Exception): """ This class is used to pass errors reported by the OpenID Provider during dynamic registration. .. versionadded:: 1.0 """ errorcode = None errordescription = None def __init__(self, response): self.errorcode = response['error'] self.errordescription = response.get('error_description') # OpenID Connect Dynamic Client Registration 1.0 def register_client(provider_info, redirect_uris): """ This function registers a new client with the specified OpenID Provider, and then returns the regitered client ID and other information. :param provider_info: The contents of the discovery endpoint as specified by the OpenID Connect Discovery 1.0 specifications. :type provider_info: dict :param redirect_uris: The redirect URIs the application wants to register. :type redirect_uris: list :returns: An object containing the information needed to configure the actual client code to communicate with the OpenID Provider. :rtype: dict :raises ValueError: The same error as used by check_redirect_uris. :raises RegistrationError: Indicates an error was returned by the OpenID Provider during registration. .. versionadded:: 1.0 """ client_type = check_redirect_uris(redirect_uris) submit_info = {'redirect_uris': redirect_uris, 'application_type': client_type, 'token_endpoint_auth_method': 'client_secret_post'} headers = {'Content-type': 'application/json'} resp, content = httplib2.Http().request( provider_info['registration_endpoint'], 'POST', json.dumps(submit_info), headers=headers) if int(resp['status']) >= 400: raise Exception('Error: the server returned HTTP ' + resp['status']) client_info = _json_loads(content) if 'error' in client_info: raise Exception('Error occured during registration: %s (%s)' % (client_info['error'], client_info.get('error_description'))) json_file = {'web': { 'client_id': client_info['client_id'], 'client_secret': client_info['client_secret'], 'auth_uri': provider_info['authorization_endpoint'], 'token_uri': provider_info['token_endpoint'], 'userinfo_uri': provider_info['userinfo_endpoint'], 'redirect_uris': redirect_uris, 'issuer': provider_info['issuer'], }} return json_file flask-oidc-1.4.0/flask_oidc/discovery.py0000664000175000017500000000356113137632531022636 0ustar puiterwijkpuiterwijk00000000000000# Copyright (c) 2016, Patrick Uiterwijk # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import httplib2 from flask_oidc import _json_loads # OpenID Connect Discovery 1.0 def discover_OP_information(OP_uri): """ Discovers information about the provided OpenID Provider. :param OP_uri: The base URI of the Provider information is requested for. :type OP_uri: str :returns: The contents of the Provider metadata document. :rtype: dict .. versionadded:: 1.0 """ _, content = httplib2.Http().request( '%s/.well-known/openid-configuration' % OP_uri) return _json_loads(content) flask-oidc-1.4.0/flask_oidc/__init__.py0000664000175000017500000010421113262435441022360 0ustar puiterwijkpuiterwijk00000000000000# Copyright (c) 2014-2015, Jeremy Ehrhardt # Copyright (c) 2016, Patrick Uiterwijk # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from functools import wraps import os import json from base64 import b64encode, urlsafe_b64encode, urlsafe_b64decode import time from copy import copy import logging from warnings import warn import calendar from six.moves.urllib.parse import urlencode from flask import request, session, redirect, url_for, g, current_app from oauth2client.client import flow_from_clientsecrets, OAuth2WebServerFlow,\ AccessTokenRefreshError, OAuth2Credentials import httplib2 from itsdangerous import JSONWebSignatureSerializer, BadSignature, \ TimedJSONWebSignatureSerializer, SignatureExpired __all__ = ['OpenIDConnect', 'MemoryCredentials'] logger = logging.getLogger(__name__) def _json_loads(content): if not isinstance(content, str): content = content.decode('utf-8') return json.loads(content) class MemoryCredentials(dict): """ Non-persistent local credentials store. Use this if you only have one app server, and don't mind making everyone log in again after a restart. """ pass class DummySecretsCache(object): """ oauth2client secrets cache """ def __init__(self, client_secrets): self.client_secrets = client_secrets def get(self, filename, namespace): return self.client_secrets class ErrStr(str): """ This is a class to work around the time I made a terrible API decision. Basically, the validate_token() function returns a boolean True if all went right, but a string with an error message if something went wrong. The problem here is that this means that "if validate_token(...)" will always be True, even with an invalid token, and users had to do "if validate_token(...) is True:". This is counter-intuitive, so let's "fix" this by returning instances of this ErrStr class, which are basic strings except for their bool() results: they return False. """ def __nonzero__(self): """The py2 method for bool().""" return False def __bool__(self): """The py3 method for bool().""" return False GOOGLE_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'] class OpenIDConnect(object): """ The core OpenID Connect client object. """ def __init__(self, app=None, credentials_store=None, http=None, time=None, urandom=None): self.credentials_store = credentials_store\ if credentials_store is not None\ else MemoryCredentials() if http is not None: warn('HTTP argument is deprecated and unused', DeprecationWarning) if time is not None: warn('time argument is deprecated and unused', DeprecationWarning) if urandom is not None: warn('urandom argument is deprecated and unused', DeprecationWarning) # By default, we do not have a custom callback self._custom_callback = None # get stuff from the app's config, which may override stuff set above if app is not None: self.init_app(app) def init_app(self, app): """ Do setup that requires a Flask app. :param app: The application to initialize. :type app: Flask """ secrets = self.load_secrets(app) self.client_secrets = list(secrets.values())[0] secrets_cache = DummySecretsCache(secrets) # Set some default configuration options app.config.setdefault('OIDC_SCOPES', ['openid', 'email']) app.config.setdefault('OIDC_GOOGLE_APPS_DOMAIN', None) app.config.setdefault('OIDC_ID_TOKEN_COOKIE_NAME', 'oidc_id_token') app.config.setdefault('OIDC_ID_TOKEN_COOKIE_PATH', '/') app.config.setdefault('OIDC_ID_TOKEN_COOKIE_TTL', 7 * 86400) # 7 days # should ONLY be turned off for local debugging app.config.setdefault('OIDC_COOKIE_SECURE', True) app.config.setdefault('OIDC_VALID_ISSUERS', (self.client_secrets.get('issuer') or GOOGLE_ISSUERS)) app.config.setdefault('OIDC_CLOCK_SKEW', 60) # 1 minute app.config.setdefault('OIDC_REQUIRE_VERIFIED_EMAIL', False) app.config.setdefault('OIDC_OPENID_REALM', None) app.config.setdefault('OIDC_USER_INFO_ENABLED', True) app.config.setdefault('OIDC_CALLBACK_ROUTE', '/oidc_callback') app.config.setdefault('OVERWRITE_REDIRECT_URI', False) # Configuration for resource servers app.config.setdefault('OIDC_RESOURCE_SERVER_ONLY', False) app.config.setdefault('OIDC_RESOURCE_CHECK_AUD', False) # We use client_secret_post, because that's what the Google # oauth2client library defaults to app.config.setdefault('OIDC_INTROSPECTION_AUTH_METHOD', 'client_secret_post') app.config.setdefault('OIDC_TOKEN_TYPE_HINT', 'access_token') if not 'openid' in app.config['OIDC_SCOPES']: raise ValueError('The value "openid" must be in the OIDC_SCOPES') # register callback route and cookie-setting decorator if not app.config['OIDC_RESOURCE_SERVER_ONLY']: app.route(app.config['OIDC_CALLBACK_ROUTE'])(self._oidc_callback) app.before_request(self._before_request) app.after_request(self._after_request) # Initialize oauth2client self.flow = flow_from_clientsecrets( app.config['OIDC_CLIENT_SECRETS'], scope=app.config['OIDC_SCOPES'], cache=secrets_cache) assert isinstance(self.flow, OAuth2WebServerFlow) # create signers using the Flask secret key self.extra_data_serializer = JSONWebSignatureSerializer( app.config['SECRET_KEY']) self.cookie_serializer = TimedJSONWebSignatureSerializer( app.config['SECRET_KEY']) try: self.credentials_store = app.config['OIDC_CREDENTIALS_STORE'] except KeyError: pass def load_secrets(self, app): # Load client_secrets.json to pre-initialize some configuration return _json_loads(open(app.config['OIDC_CLIENT_SECRETS'], 'r').read()) @property def user_loggedin(self): """ Represents whether the user is currently logged in. Returns: bool: Whether the user is logged in with Flask-OIDC. .. versionadded:: 1.0 """ return g.oidc_id_token is not None def user_getfield(self, field, access_token=None): """ Request a single field of information about the user. :param field: The name of the field requested. :type field: str :returns: The value of the field. Depending on the type, this may be a string, list, dict, or something else. :rtype: object .. versionadded:: 1.0 """ info = self.user_getinfo([field], access_token) return info.get(field) def user_getinfo(self, fields, access_token=None): """ Request multiple fields of information about the user. :param fields: The names of the fields requested. :type fields: list :returns: The values of the current user for the fields requested. The keys are the field names, values are the values of the fields as indicated by the OpenID Provider. Note that fields that were not provided by the Provider are absent. :rtype: dict :raises Exception: If the user was not authenticated. Check this with user_loggedin. .. versionadded:: 1.0 """ if g.oidc_id_token is None and access_token is None: raise Exception('User was not authenticated') info = {} all_info = None for field in fields: if access_token is None and field in g.oidc_id_token: info[field] = g.oidc_id_token[field] elif current_app.config['OIDC_USER_INFO_ENABLED']: # This was not in the id_token. Let's get user information if all_info is None: all_info = self._retrieve_userinfo(access_token) if all_info is None: # To make sure we don't retry for every field all_info = {} if field in all_info: info[field] = all_info[field] else: # We didn't get this information pass return info def get_access_token(self): """Method to return the current requests' access_token. :returns: Access token or None :rtype: str .. versionadded:: 1.2 """ try: credentials = OAuth2Credentials.from_json( self.credentials_store[g.oidc_id_token['sub']]) return credentials.access_token except KeyError: logger.debug("Expired ID token, credentials missing", exc_info=True) return None def get_refresh_token(self): """Method to return the current requests' refresh_token. :returns: Access token or None :rtype: str .. versionadded:: 1.2 """ try: credentials = OAuth2Credentials.from_json( self.credentials_store[g.oidc_id_token['sub']]) return credentials.refresh_token except KeyError: logger.debug("Expired ID token, credentials missing", exc_info=True) return None def _retrieve_userinfo(self, access_token=None): """ Requests extra user information from the Provider's UserInfo and returns the result. :returns: The contents of the UserInfo endpoint. :rtype: dict """ if 'userinfo_uri' not in self.client_secrets: logger.debug('Userinfo uri not specified') raise AssertionError('UserInfo URI not specified') # Cache the info from this request if '_oidc_userinfo' in g: return g._oidc_userinfo http = httplib2.Http() if access_token is None: try: credentials = OAuth2Credentials.from_json( self.credentials_store[g.oidc_id_token['sub']]) except KeyError: logger.debug("Expired ID token, credentials missing", exc_info=True) return None credentials.authorize(http) resp, content = http.request(self.client_secrets['userinfo_uri']) else: # We have been manually overriden with an access token resp, content = http.request( self.client_secrets['userinfo_uri'], "POST", body=urlencode({"access_token": access_token}), headers={'Content-Type': 'application/x-www-form-urlencoded'}) logger.debug('Retrieved user info: %s' % content) info = _json_loads(content) g._oidc_userinfo = info return info def get_cookie_id_token(self): """ .. deprecated:: 1.0 Use :func:`user_getinfo` instead. """ warn('You are using a deprecated function (get_cookie_id_token). ' 'Please reconsider using this', DeprecationWarning) return self._get_cookie_id_token() def _get_cookie_id_token(self): try: id_token_cookie = request.cookies.get(current_app.config[ 'OIDC_ID_TOKEN_COOKIE_NAME']) if not id_token_cookie: # Do not error if we were unable to get the cookie. # The user can debug this themselves. return None return self.cookie_serializer.loads(id_token_cookie) except SignatureExpired: logger.debug("Invalid ID token cookie", exc_info=True) return None def set_cookie_id_token(self, id_token): """ .. deprecated:: 1.0 """ warn('You are using a deprecated function (set_cookie_id_token). ' 'Please reconsider using this', DeprecationWarning) return self._set_cookie_id_token(id_token) def _set_cookie_id_token(self, id_token): """ Cooperates with @after_request to set a new ID token cookie. """ g.oidc_id_token = id_token g.oidc_id_token_dirty = True def _after_request(self, response): """ Set a new ID token cookie if the ID token has changed. """ # This means that if either the new or the old are False, we set # insecure cookies. # We don't define OIDC_ID_TOKEN_COOKIE_SECURE in init_app, because we # don't want people to find it easily. cookie_secure = (current_app.config['OIDC_COOKIE_SECURE'] and current_app.config.get('OIDC_ID_TOKEN_COOKIE_SECURE', True)) if getattr(g, 'oidc_id_token_dirty', False): if g.oidc_id_token: signed_id_token = self.cookie_serializer.dumps(g.oidc_id_token) response.set_cookie( current_app.config['OIDC_ID_TOKEN_COOKIE_NAME'], signed_id_token, secure=cookie_secure, httponly=True, max_age=current_app.config['OIDC_ID_TOKEN_COOKIE_TTL']) else: # This was a log out response.set_cookie( current_app.config['OIDC_ID_TOKEN_COOKIE_NAME'], '', path=current_app.config['OIDC_ID_TOKEN_COOKIE_PATH'], secure=cookie_secure, httponly=True, expires=0) return response def _before_request(self): g.oidc_id_token = None self.authenticate_or_redirect() def authenticate_or_redirect(self): """ Helper function suitable for @app.before_request and @check. Sets g.oidc_id_token to the ID token if the user has successfully authenticated, else returns a redirect object so they can go try to authenticate. :returns: A redirect object, or None if the user is logged in. :rtype: Redirect .. deprecated:: 1.0 Use :func:`require_login` instead. """ # the auth callback and error pages don't need user to be authenticated if request.endpoint in frozenset(['_oidc_callback', '_oidc_error']): return None # retrieve signed ID token cookie id_token = self._get_cookie_id_token() if id_token is None: return self.redirect_to_auth_server(request.url) # ID token expired # when Google is the IdP, this happens after one hour if time.time() >= id_token['exp']: # get credentials from store try: credentials = OAuth2Credentials.from_json( self.credentials_store[id_token['sub']]) except KeyError: logger.debug("Expired ID token, credentials missing", exc_info=True) return self.redirect_to_auth_server(request.url) # refresh and store credentials try: credentials.refresh(httplib2.Http()) if credentials.id_token: id_token = credentials.id_token else: # It is not guaranteed that we will get a new ID Token on # refresh, so if we do not, let's just update the id token # expiry field and reuse the existing ID Token. if credentials.token_expiry is None: logger.debug('Expired ID token, no new expiry. Falling' ' back to assuming 1 hour') id_token['exp'] = time.time() + 3600 else: id_token['exp'] = calendar.timegm( credentials.token_expiry.timetuple()) self.credentials_store[id_token['sub']] = credentials.to_json() self._set_cookie_id_token(id_token) except AccessTokenRefreshError: # Can't refresh. Wipe credentials and redirect user to IdP # for re-authentication. logger.debug("Expired ID token, can't refresh credentials", exc_info=True) del self.credentials_store[id_token['sub']] return self.redirect_to_auth_server(request.url) # make ID token available to views g.oidc_id_token = id_token return None def require_login(self, view_func): """ Use this to decorate view functions that require a user to be logged in. If the user is not already logged in, they will be sent to the Provider to log in, after which they will be returned. .. versionadded:: 1.0 This was :func:`check` before. """ @wraps(view_func) def decorated(*args, **kwargs): if g.oidc_id_token is None: return self.redirect_to_auth_server(request.url) return view_func(*args, **kwargs) return decorated # Backwards compatibility check = require_login """ .. deprecated:: 1.0 Use :func:`require_login` instead. """ def flow_for_request(self): """ .. deprecated:: 1.0 Use :func:`require_login` instead. """ warn('You are using a deprecated function (flow_for_request). ' 'Please reconsider using this', DeprecationWarning) return self._flow_for_request() def _flow_for_request(self): """ Build a flow with the correct absolute callback URL for this request. :return: """ flow = copy(self.flow) redirect_uri = current_app.config['OVERWRITE_REDIRECT_URI'] if not redirect_uri: flow.redirect_uri = url_for('_oidc_callback', _external=True) else: flow.redirect_uri = redirect_uri return flow def redirect_to_auth_server(self, destination=None, customstate=None): """ Set a CSRF token in the session, and redirect to the IdP. :param destination: The page that the user was going to, before we noticed they weren't logged in. :type destination: Url to return the client to if a custom handler is not used. Not available with custom callback. :param customstate: The custom data passed via the ODIC state. Note that this only works with a custom_callback, and this will ignore destination. :type customstate: Anything that can be serialized :returns: A redirect response to start the login process. :rtype: Flask Response .. deprecated:: 1.0 Use :func:`require_login` instead. """ if not self._custom_callback and customstate: raise ValueError('Custom State is only avilable with a custom ' 'handler') if 'oidc_csrf_token' not in session: csrf_token = urlsafe_b64encode(os.urandom(24)).decode('utf-8') session['oidc_csrf_token'] = csrf_token state = { 'csrf_token': session['oidc_csrf_token'], } statefield = 'destination' statevalue = destination if customstate is not None: statefield = 'custom' statevalue = customstate state[statefield] = self.extra_data_serializer.dumps( statevalue).decode('utf-8') extra_params = { 'state': urlsafe_b64encode(json.dumps(state).encode('utf-8')), } if current_app.config['OIDC_GOOGLE_APPS_DOMAIN']: extra_params['hd'] = current_app.config['OIDC_GOOGLE_APPS_DOMAIN'] if current_app.config['OIDC_OPENID_REALM']: extra_params['openid.realm'] = current_app.config[ 'OIDC_OPENID_REALM'] flow = self._flow_for_request() auth_url = '{url}&{extra_params}'.format( url=flow.step1_get_authorize_url(), extra_params=urlencode(extra_params)) # if the user has an ID token, it's invalid, or we wouldn't be here self._set_cookie_id_token(None) return redirect(auth_url) def _is_id_token_valid(self, id_token): """ Check if `id_token` is a current ID token for this application, was issued by the Apps domain we expected, and that the email address has been verified. @see: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation """ if not id_token: return False # step 2: check issuer if id_token['iss'] not in current_app.config['OIDC_VALID_ISSUERS']: logger.error('id_token issued by non-trusted issuer: %s' % id_token['iss']) return False if isinstance(id_token['aud'], list): # step 3 for audience list if self.flow.client_id not in id_token['aud']: logger.error('We are not a valid audience') return False # step 4 if 'azp' not in id_token: logger.error('Multiple audiences and not authorized party') return False else: # step 3 for single audience if id_token['aud'] != self.flow.client_id: logger.error('We are not the audience') return False # step 5 if 'azp' in id_token and id_token['azp'] != self.flow.client_id: logger.error('Authorized Party is not us') return False # step 6-8: TLS checked # step 9: check exp if int(time.time()) >= int(id_token['exp']): logger.error('Token has expired') return False # step 10: check iat if id_token['iat'] < (time.time() - current_app.config['OIDC_CLOCK_SKEW']): logger.error('Token issued in the past') return False # (not required if using HTTPS?) step 11: check nonce # step 12-13: not requested acr or auth_time, so not needed to test # additional steps specific to our usage if current_app.config['OIDC_GOOGLE_APPS_DOMAIN'] and \ id_token.get('hd') != current_app.config[ 'OIDC_GOOGLE_APPS_DOMAIN']: logger.error('Invalid google apps domain') return False if not id_token.get('email_verified', False) and \ current_app.config['OIDC_REQUIRE_VERIFIED_EMAIL']: logger.error('Email not verified') return False return True WRONG_GOOGLE_APPS_DOMAIN = 'WRONG_GOOGLE_APPS_DOMAIN' def custom_callback(self, view_func): """ Wrapper function to use a custom callback. The custom OIDC callback will get the custom state field passed in with redirect_to_auth_server. """ @wraps(view_func) def decorated(*args, **kwargs): plainreturn, data = self._process_callback('custom') if plainreturn: return data else: return view_func(data, *args, **kwargs) self._custom_callback = decorated return decorated def _oidc_callback(self): plainreturn, data = self._process_callback('destination') if plainreturn: return data else: return redirect(data) def _process_callback(self, statefield): """ Exchange the auth code for actual credentials, then redirect to the originally requested page. """ # retrieve session and callback variables try: session_csrf_token = session.get('oidc_csrf_token') state = _json_loads(urlsafe_b64decode(request.args['state'].encode('utf-8'))) csrf_token = state['csrf_token'] code = request.args['code'] except (KeyError, ValueError): logger.debug("Can't retrieve CSRF token, state, or code", exc_info=True) return True, self._oidc_error() # check callback CSRF token passed to IdP # against session CSRF token held by user if csrf_token != session_csrf_token: logger.debug("CSRF token mismatch") return True, self._oidc_error() # make a request to IdP to exchange the auth code for OAuth credentials flow = self._flow_for_request() credentials = flow.step2_exchange(code) id_token = credentials.id_token if not self._is_id_token_valid(id_token): logger.debug("Invalid ID token") if id_token.get('hd') != current_app.config[ 'OIDC_GOOGLE_APPS_DOMAIN']: return True, self._oidc_error( "You must log in with an account from the {0} domain." .format(current_app.config['OIDC_GOOGLE_APPS_DOMAIN']), self.WRONG_GOOGLE_APPS_DOMAIN) return True, self._oidc_error() # store credentials by subject # when Google is the IdP, the subject is their G+ account number self.credentials_store[id_token['sub']] = credentials.to_json() # Retrieve the extra statefield data try: response = self.extra_data_serializer.loads(state[statefield]) except BadSignature: logger.error('State field was invalid') return True, self._oidc_error() # set a persistent signed cookie containing the ID token # and redirect to the final destination self._set_cookie_id_token(id_token) return False, response def _oidc_error(self, message='Not Authorized', code=None): return (message, 401, { 'Content-Type': 'text/plain', }) def logout(self): """ Request the browser to please forget the cookie we set, to clear the current session. Note that as described in [1], this will not log out in the case of a browser that doesn't clear cookies when requested to, and the user could be automatically logged in when they hit any authenticated endpoint. [1]: https://github.com/puiterwijk/flask-oidc/issues/5#issuecomment-86187023 .. versionadded:: 1.0 """ # TODO: Add single logout self._set_cookie_id_token(None) # Below here is for resource servers to validate tokens def validate_token(self, token, scopes_required=None): """ This function can be used to validate tokens. Note that this only works if a token introspection url is configured, as that URL will be queried for the validity and scopes of a token. :param scopes_required: List of scopes that are required to be granted by the token before returning True. :type scopes_required: list :returns: True if the token was valid and contained the required scopes. An ErrStr (subclass of string for which bool() is False) if an error occured. :rtype: Boolean or String .. versionadded:: 1.1 """ valid = self._validate_token(token, scopes_required) if valid is True: return True else: return ErrStr(valid) def _validate_token(self, token, scopes_required=None): """The actual implementation of validate_token.""" if scopes_required is None: scopes_required = [] scopes_required = set(scopes_required) token_info = None valid_token = False has_required_scopes = False if token: try: token_info = self._get_token_info(token) except Exception as ex: token_info = {'active': False} logger.error('ERROR: Unable to get token info') logger.error(str(ex)) valid_token = token_info.get('active', False) if 'aud' in token_info and \ current_app.config['OIDC_RESOURCE_CHECK_AUD']: valid_audience = False aud = token_info['aud'] clid = self.client_secrets['client_id'] if isinstance(aud, list): valid_audience = clid in aud else: valid_audience = clid == aud if not valid_audience: logger.error('Refused token because of invalid ' 'audience') valid_token = False if valid_token: token_scopes = token_info.get('scope', '').split(' ') else: token_scopes = [] has_required_scopes = scopes_required.issubset( set(token_scopes)) if not has_required_scopes: logger.debug('Token missed required scopes') if (valid_token and has_required_scopes): g.oidc_token_info = token_info return True if not valid_token: return 'Token required but invalid' elif not has_required_scopes: return 'Token does not have required scopes' else: return 'Something went wrong checking your token' def accept_token(self, require_token=False, scopes_required=None, render_errors=True): """ Use this to decorate view functions that should accept OAuth2 tokens, this will most likely apply to API functions. Tokens are accepted as part of the query URL (access_token value) or a POST form value (access_token). Note that this only works if a token introspection url is configured, as that URL will be queried for the validity and scopes of a token. :param require_token: Whether a token is required for the current function. If this is True, we will abort the request if there was no token provided. :type require_token: bool :param scopes_required: List of scopes that are required to be granted by the token before being allowed to call the protected function. :type scopes_required: list :param render_errors: Whether or not to eagerly render error objects as JSON API responses. Set to False to pass the error object back unmodified for later rendering. :type render_errors: callback(obj) or None .. versionadded:: 1.0 """ def wrapper(view_func): @wraps(view_func) def decorated(*args, **kwargs): token = None if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '): token = request.headers['Authorization'].split(None,1)[1].strip() if 'access_token' in request.form: token = request.form['access_token'] elif 'access_token' in request.args: token = request.args['access_token'] validity = self.validate_token(token, scopes_required) if (validity is True) or (not require_token): return view_func(*args, **kwargs) else: response_body = {'error': 'invalid_token', 'error_description': validity} if render_errors: response_body = json.dumps(response_body) return response_body, 401, {'WWW-Authenticate': 'Bearer'} return decorated return wrapper def _get_token_info(self, token): # We hardcode to use client_secret_post, because that's what the Google # oauth2client library defaults to request = {'token': token} headers = {'Content-type': 'application/x-www-form-urlencoded'} hint = current_app.config['OIDC_TOKEN_TYPE_HINT'] if hint != 'none': request['token_type_hint'] = hint auth_method = current_app.config['OIDC_INTROSPECTION_AUTH_METHOD'] if (auth_method == 'client_secret_basic'): basic_auth_string = '%s:%s' % (self.client_secrets['client_id'], self.client_secrets['client_secret']) basic_auth_bytes = bytearray(basic_auth_string, 'utf-8') headers['Authorization'] = 'Basic %s' % b64encode(basic_auth_bytes) elif (auth_method == 'bearer'): headers['Authorization'] = 'Bearer %s' % token elif (auth_method == 'client_secret_post'): request['client_id'] = self.client_secrets['client_id'] request['client_secret'] = self.client_secrets['client_secret'] resp, content = httplib2.Http().request( self.client_secrets['token_introspection_uri'], 'POST', urlencode(request), headers=headers) # TODO: Cache this reply return _json_loads(content) flask-oidc-1.4.0/flask_oidc/registration_util.py0000664000175000017500000000711313137632531024373 0ustar puiterwijkpuiterwijk00000000000000# Copyright (c) 2016, Patrick Uiterwijk # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse import json import logging import os.path import sys from flask_oidc import discovery from flask_oidc import registration logging.basicConfig() LOG = logging.getLogger("oidc-register") def _parse_args(): parser = argparse.ArgumentParser(description='Help register an OpenID ' 'Client') parser.add_argument('provider_url', help='Base URL to the provider to register at') parser.add_argument('application_url', help='Base URL to the application') parser.add_argument('--token-introspection-uri', help='Token introspection URI') parser.add_argument('--output-file', default='client_secrets.json', help='File to write client info to') parser.add_argument('--debug', action='store_true') return parser.parse_args() def main(): args = _parse_args() if os.path.exists(args.output_file): print('Output file exists. Please provide other filename') return 1 if args.debug: LOG.setLevel(logging.DEBUG) redirect_uris = ['%s/oidc_callback' % args.application_url] registration.check_redirect_uris(redirect_uris) try: OP = discovery.discover_OP_information(args.provider_url) except Exception as ex: print('Error discovering OP information') if args.debug: print(ex) LOG.exception("Error caught when discovering OP information:") return 1 if args.debug: print('Provider info: %s' % OP) try: reg_info = registration.register_client(OP, redirect_uris) except Exception as ex: print('Error registering client') if args.debug: print(ex) LOG.exception("Error caught when registering the client:") return 1 if args.debug: print('Registration info: %s' % reg_info) if args.token_introspection_uri: reg_info['web']['token_introspection_uri'] = \ args.token_introspection_uri with open(args.output_file, 'w') as outfile: outfile.write(json.dumps(reg_info)) print('Client information file written') if __name__ == '__main__': retval = main() if retval: sys.exit(retval) flask-oidc-1.4.0/flask_oidc.egg-info/0000775000175000017500000000000013263723662021750 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/flask_oidc.egg-info/dependency_links.txt0000644000175000017500000000000113263723661026013 0ustar puiterwijkpuiterwijk00000000000000 flask-oidc-1.4.0/flask_oidc.egg-info/entry_points.txt0000664000175000017500000000010513263723661025241 0ustar puiterwijkpuiterwijk00000000000000[console_scripts] oidc-register = flask_oidc.registration_util:main flask-oidc-1.4.0/flask_oidc.egg-info/requires.txt0000644000175000017500000000004413263723661024343 0ustar puiterwijkpuiterwijk00000000000000Flask itsdangerous oauth2client six flask-oidc-1.4.0/flask_oidc.egg-info/top_level.txt0000644000175000017500000000001313263723661024471 0ustar puiterwijkpuiterwijk00000000000000flask_oidc flask-oidc-1.4.0/flask_oidc.egg-info/not-zip-safe0000644000175000017500000000000113137632531024166 0ustar puiterwijkpuiterwijk00000000000000 flask-oidc-1.4.0/flask_oidc.egg-info/PKG-INFO0000644000175000017500000000427213263723661023047 0ustar puiterwijkpuiterwijk00000000000000Metadata-Version: 1.1 Name: flask-oidc Version: 1.4.0 Summary: OpenID Connect extension for Flask Home-page: https://github.com/puiterwijk/flask-oidc Author: Jeremy Ehrhardt, Patrick Uiterwijk Author-email: jeremy@bat-country.us, patrick@puiterwijk.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: flask-oidc ========== `OpenID Connect `_ support for `Flask `_. .. image:: https://img.shields.io/pypi/v/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://img.shields.io/pypi/dm/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://readthedocs.org/projects/flask-oidc/badge/?version=latest :target: http://flask-oidc.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/travis/puiterwijk/flask-oidc.svg?style=flat :target: https://travis-ci.org/puiterwijk/flask-oidc This library should work with any standards compliant OpenID Connect provider. It has been tested with: * `Google+ Login `_ * `Ipsilon `_ Project status ============== This project is in active development. Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules flask-oidc-1.4.0/flask_oidc.egg-info/SOURCES.txt0000644000175000017500000000147413263723661023637 0ustar puiterwijkpuiterwijk00000000000000LICENSE.txt MANIFEST.in README.rst requirements.txt setup.cfg setup.py docs/Makefile docs/conf.py docs/index.rst docs/_themes/.git docs/_themes/.gitignore docs/_themes/LICENSE docs/_themes/README docs/_themes/flask_theme_support.py docs/_themes/flask/layout.html docs/_themes/flask/relations.html docs/_themes/flask/theme.conf docs/_themes/flask/static/flasky.css_t docs/_themes/flask_small/layout.html docs/_themes/flask_small/theme.conf docs/_themes/flask_small/static/flasky.css_t flask_oidc/__init__.py flask_oidc/discovery.py flask_oidc/registration.py flask_oidc/registration_util.py flask_oidc.egg-info/PKG-INFO flask_oidc.egg-info/SOURCES.txt flask_oidc.egg-info/dependency_links.txt flask_oidc.egg-info/entry_points.txt flask_oidc.egg-info/not-zip-safe flask_oidc.egg-info/requires.txt flask_oidc.egg-info/top_level.txtflask-oidc-1.4.0/MANIFEST.in0000664000175000017500000000040413137632531017706 0ustar puiterwijkpuiterwijk00000000000000include README.rst LICENSE.rst CHANGES.rst recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs *.pyo recursive-include example * recursive-exclude example *.pyc recursive-exclude example *.pyo prune docs/_build prune docs/_themes/.git flask-oidc-1.4.0/requirements.txt0000664000175000017500000000004413137632531021434 0ustar puiterwijkpuiterwijk00000000000000Flask itsdangerous oauth2client six flask-oidc-1.4.0/LICENSE.txt0000664000175000017500000000255613137632531020005 0ustar puiterwijkpuiterwijk00000000000000Copyright (c) 2014-2015, Jeremy Ehrhardt Copyright (c) 2016, Patrick Uiterwijk All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-oidc-1.4.0/docs/0000775000175000017500000000000013263723662017110 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/Makefile0000664000175000017500000001762013137632531020550 0ustar puiterwijkpuiterwijk00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-OIDC.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-OIDC.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-OIDC" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-OIDC" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." flask-oidc-1.4.0/docs/index.rst0000664000175000017500000002207613262435441020752 0ustar puiterwijkpuiterwijk00000000000000Flask-OIDC ========== Flask-OIDC is an extension to `Flask`_ that allows you to add `OpenID Connect`_ based authentication to your website in a matter of minutes. It depends on Flask and `oauth2client`_. You can install the requirements from PyPI with `easy_install` or `pip` or download them by hand. Features -------- - Support for OpenID Connect 1.0 - Support for OpenID Connect Discovery 1.0 - Support for OpenID Connect Dynamic Registration 1.0 - Friendly API - Perfect integration into Flask - Helper functions to allow resource servers to accept OAuth2 tokens Installation ------------ Install the extension with `pip`:: $ pip install Flask-OIDC How to use ---------- To integrate Flask-OpenID into your application you need to create an instance of the :class:`OpenID` object first:: from flask.ext.oidc import OpenIDConnect oidc = OpenIDConnect(app) Alternatively the object can be instantiated without the application in which case it can later be registered for an application with the :meth:`~flask_oidc.OpenIDConnect.init_app` method. Note that you should probably provide the library with a place to store the credentials it has retrieved for the user. These need to be stored in a place where the user themselves or an attacker can not get to them. To provide this, give an object that has ``__setitem__`` and ``__getitem__`` dict APIs implemented as second argument to the :meth:`~flask_oidc.OpenIDConnect.__init__` call. Without this, the library will only work on a single thread, and only retain sessions until the server is restarted. Using this library is very simple: you can use :data:`~flask_oidc.OpenIDConnect.user_loggedin` to determine whether a user is currently logged in using OpenID Connect. If the user is logged in, you an use :meth:`~flask_oidc.OpenIDConnect.user_getfield` and :meth:`~flask_oidc.OpenIDConnect.user_getinfo` to get information about the currently logged in user. If the user is not logged in, you can send them to any function that is decorated with :meth:`~flask_oidc.OpenIDConnect.require_login` to get them automatically redirected to login. Example ------- A very basic example client:: @app.route('/') def index(): if oidc.user_loggedin: return 'Welcome %s' % oidc.user_getfield('email') else return 'Not logged in' @app.route('/login') @oidc.require_login def login(): return 'Welcome %s' % oidc.user_getfield('email') Custom callback --------------- It is possible to override the default OIDC callback to keep track of a custom state dict through the OIDC authentication steps, which makes it possible to write stateless apps. To do this, add the decorator `oidc.custom_callback` to your callback function. This will get the (json-serializable) custom state that you passed in as `customstate` to `oidc.redirect_to_auth_server`. Note that to use this, you will need to set `OVERWRITE_REDIRECT_URI`. Example:: @app.route('/') def index(): return oidc.redirect_to_auth_server(None, flask.request.values) @app.route('/custom_callback') @oidc.custom_callback def callback(data): return 'Hello. You submitted %s' % data Resource server --------------- Also, if you have implemented an API that can should be able to accept tokens issued by the OpenID Connect provider, just decorate those API functions with :meth:`~flask_oidc.OpenIDConnect.accept_token`:: @app.route('/api') @oidc.accept_token() def my_api(): return json.dumps('Welcome %s' % g.oidc_token_info['sub']) If you are only using this part of flask-oidc, it is suggested to set the configuration option `OIDC_RESOURCE_SERVER_ONLY` (new in 1.0.5). Registration ------------ To be able to use an OpenID Provider, you will need to register your client with them. If the Provider you want to use supports Dynamic Registration, you can execute ``oidc-register https://myprovider.example.com/ https://myapplication.example.com/`` and the full client_secrets.json will be generated for you, and you are ready to start. If it does not, please see the documentation of the Provider you want to use for information on how to obtain client secrets. For example, for Google, you will need to visit `Google API credentials management `_. Manual client registration -------------------------- If your identity provider does not offer Dynamic Registration (and you can't push them to do so, as it would make it a lot simpler!), you might need to know the following details: Grant type authorization_code (Authorization Code flow) Response type Code Token endpoint auth metod client_secret_post Redirect URI /oidc_callback You will also need to manually craft your client_secrets.json. This is just a json document, with everything under a top-level "web" key. Underneath that top-level key, you have the following keys: client_id Client ID issued by your IdP client_secret Client secret belonging to the registered ID auth_uri The Identity Provider's authorization endpoint url token_uri The Identity Provider's token endpoint url (Optional, used for resource server) userinfo_uri The Identity Provider's userinfo url issuer The "issuer" value for the Identity Provider redirect_uris A list of the registered redirect uris Settings reference ------------------- This is a list of all settings supported in the current release. OIDC_SCOPES A python list with the scopes that should be requested. This impacts the information available in the UserInfo field and what the token can be used for. Please check your identity provider's documentation for valid values. Defaults to ['openid', 'email']. OIDC_GOOGLE_APPS_DOMAIN The Google Apps domain that must be used to login to this application. Defaults to None, which means this check is skipped and the user can login with any Google account. OIDC_ID_TOKEN_COOKIE_NAME Name of the cookie used to store the users' login state. Defaults to "oidc_id_token". OIDC_ID_TOKEN_COOKIE_PATH Path under which the login state cookie is stored. Defaults to "/". OIDC_ID_TOKEN_COOKIE_TTL Integer telling how long the login state of the user remains valid. Defaults to 7 days. OIDC_COOKIE_SECURE Boolean indicating whether the cookie should be sent with the secure flag enabled. This means that a client (browser) will only send the cookie over an https connection. *Do NOT disable this in production please.* Defaults to True, indicating the cookie is marked as secure. OIDC_VALID_ISSUERS The token issuer that is accepted. Please check your Identity Providers documentation for the correct value. Defaults to the value of "issuer" in client_secrets, or the Google issuer if not found. OIDC_CLOCK_SKEW Number of seconds of clock skew allowed when checking the "don't use before" and "don't use after" values for tokens. Defaults to sixty seconds (one minute). OIDC_REQUIRE_VERIFIED_EMAIL Boolean indicating whether the Identity Provider needs to mark the email as "verified". Defaults to False. OIDC_OPENID_REALM String passed to the OpenID Connect provider to ask for the old OpenID identity for users. This helps when migrating from OpenID 2.0 to OpenID Connect because the Identity Provider will also add the OpenID 2.0 identity so you can tie them together. Defaults to None. OIDC_USER_INFO_ENABLED Boolean whether to get user information from the UserInfo endpoint provided by the Identity Provider in addition to the token information. Defaults to True. OIDC_CALLBACK_ROUTE URL relative to the web root to indicate where the oidc_callback url is mounted on. Defaults to /oidc_callback. OVERWRITE_REDIRECT_URI URL to use as return url when passing to the Identity Provider. To be used when Flask could not detect the correct hostname, scheme or path to your application. Alternatively used when custom handler is to be used. Defaults to False (disabled). OIDC_RESOURCE_SERVER_ONLY Boolean whether to disable the OpenID Client parts. You can enable this in applications where you only use the resource server parts (accept_token) and will skip checking for any cookies. OIDC_RESOURCE_CHECK_AUD Boolean to indicate whether the current application needs to be the "audience" of tokens passed. Defaults to False. OIDC_INTROSPECTION_AUTH_METHOD String that sets the authentication method used when communicating with the token_introspection_uri. Valid values are 'client_secret_post', 'client_secret_basic', or 'bearer'. Defaults to 'client_secret_post'. API References -------------- The full API reference: .. automodule:: flask_oidc :members: Discovery --------- .. automodule:: flask_oidc.discovery :members: Registration ------------ .. automodule:: flask_oidc.registration :members: .. _Flask: http://flask.pocoo.org/ .. _OpenID Connect: https://openid.net/connect/ .. _oauth2client: https://github.com/google/oauth2client flask-oidc-1.4.0/docs/conf.py0000664000175000017500000002262613137632531020411 0ustar puiterwijkpuiterwijk00000000000000# -*- coding: utf-8 -*- # # Flask-OIDC documentation build configuration file, created by # sphinx-quickstart on Tue May 17 10:16:49 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.abspath('_themes')) sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Flask-OIDC' copyright = u'2016, Patrick Uiterwijk' author = u'Patrick Uiterwijk' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'1.1' # The full version, including alpha/beta/rc tags. release = u'1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'flask_small' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { 'index_logo': 'flask-openid.png', 'github_fork': 'puiterwijk/flask-oidc' } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # The name for this set of Sphinx documents. # " v documentation" by default. #html_title = u'Flask-OIDC v0.1' # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. #html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Flask-OIDCdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Flask-OIDC.tex', u'Flask-OIDC Documentation', u'Patrick Uiterwijk', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'flask-oidc', u'Flask-OIDC Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Flask-OIDC', u'Flask-OIDC Documentation', author, 'Flask-OIDC', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False flask-oidc-1.4.0/docs/_themes/0000775000175000017500000000000013263723662020534 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/_themes/README0000664000175000017500000000210513137632531021404 0ustar puiterwijkpuiterwijk00000000000000Flask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge flask-oidc-1.4.0/docs/_themes/LICENSE0000664000175000017500000000337513137632531021543 0ustar puiterwijkpuiterwijk00000000000000Copyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-oidc-1.4.0/docs/_themes/.git0000664000175000017500000000005013137632531021305 0ustar puiterwijkpuiterwijk00000000000000gitdir: ../../.git/modules/docs/_themes flask-oidc-1.4.0/docs/_themes/flask_theme_support.py0000664000175000017500000001141313137632531025156 0ustar puiterwijkpuiterwijk00000000000000# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } flask-oidc-1.4.0/docs/_themes/.gitignore0000664000175000017500000000002613137632531022514 0ustar puiterwijkpuiterwijk00000000000000*.pyc *.pyo .DS_Store flask-oidc-1.4.0/docs/_themes/flask/0000775000175000017500000000000013263723662021634 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/_themes/flask/static/0000775000175000017500000000000013263723662023123 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/_themes/flask/static/flasky.css_t0000664000175000017500000002154613137632531025453 0ustar puiterwijkpuiterwijk00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0 0 20px 0; margin: 0; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt { background: #EEE; } @media screen and (max-width: 870px) { div.sphinxsidebar { display: none; } div.document { width: 100%; } div.documentwrapper { margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; } div.bodywrapper { margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; } ul { margin-left: 0; } .document { width: auto; } .footer { width: auto; } .bodywrapper { margin: 0; } .footer { width: auto; } .github { display: none; } } @media screen and (max-width: 875px) { body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } .rtd_doc_footer { display: none; } .document { width: auto; } .footer { width: auto; } .footer { width: auto; } .github { display: none; } } /* scrollbars */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment { display: block; height: 10px; } ::-webkit-scrollbar-button:vertical:increment { background-color: #fff; } ::-webkit-scrollbar-track-piece { background-color: #eee; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:vertical { height: 50px; background-color: #ccc; -webkit-border-radius: 3px; } ::-webkit-scrollbar-thumb:horizontal { width: 50px; background-color: #ccc; -webkit-border-radius: 3px; } /* misc. */ .revsys-inline { display: none!important; }flask-oidc-1.4.0/docs/_themes/flask/layout.html0000664000175000017500000000126513137632531024035 0ustar puiterwijkpuiterwijk00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {%- block footer %} {% if pagename == 'index' %}
{% endif %} {%- endblock %} flask-oidc-1.4.0/docs/_themes/flask/relations.html0000664000175000017500000000111613137632531024513 0ustar puiterwijkpuiterwijk00000000000000

Related Topics

flask-oidc-1.4.0/docs/_themes/flask/theme.conf0000664000175000017500000000024413137632531023577 0ustar puiterwijkpuiterwijk00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px touch_icon = flask-oidc-1.4.0/docs/_themes/flask_small/0000775000175000017500000000000013263723662023024 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/_themes/flask_small/static/0000775000175000017500000000000013263723662024313 5ustar puiterwijkpuiterwijk00000000000000flask-oidc-1.4.0/docs/_themes/flask_small/static/flasky.css_t0000664000175000017500000001100113137632531026624 0ustar puiterwijkpuiterwijk00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.85em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } flask-oidc-1.4.0/docs/_themes/flask_small/layout.html0000664000175000017500000000125313137632531025222 0ustar puiterwijkpuiterwijk00000000000000{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} flask-oidc-1.4.0/docs/_themes/flask_small/theme.conf0000664000175000017500000000027013137632531024766 0ustar puiterwijkpuiterwijk00000000000000[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' flask-oidc-1.4.0/PKG-INFO0000664000175000017500000000427213263723662017262 0ustar puiterwijkpuiterwijk00000000000000Metadata-Version: 1.1 Name: flask-oidc Version: 1.4.0 Summary: OpenID Connect extension for Flask Home-page: https://github.com/puiterwijk/flask-oidc Author: Jeremy Ehrhardt, Patrick Uiterwijk Author-email: jeremy@bat-country.us, patrick@puiterwijk.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: flask-oidc ========== `OpenID Connect `_ support for `Flask `_. .. image:: https://img.shields.io/pypi/v/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://img.shields.io/pypi/dm/flask-oidc.svg?style=flat :target: https://pypi.python.org/pypi/flask-oidc .. image:: https://readthedocs.org/projects/flask-oidc/badge/?version=latest :target: http://flask-oidc.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/travis/puiterwijk/flask-oidc.svg?style=flat :target: https://travis-ci.org/puiterwijk/flask-oidc This library should work with any standards compliant OpenID Connect provider. It has been tested with: * `Google+ Login `_ * `Ipsilon `_ Project status ============== This project is in active development. Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules flask-oidc-1.4.0/setup.py0000664000175000017500000000410113263162156017661 0ustar puiterwijkpuiterwijk00000000000000import os.path import io import sys from setuptools import setup # This check is to make sure we checkout docs/_themes before running sdist if not os.path.exists("./docs/_themes/README"): print('Please make sure you have docs/_themes checked out while running setup.py!') if os.path.exists('.git'): print('You seem to be using a git checkout, please execute the following commands to get the docs/_themes directory:') print(' - git submodule init') print(' - git submodule update') else: print('You seem to be using a release. Please use the release tarball from PyPI instead of the archive from GitHub') sys.exit(1) here = os.path.abspath(os.path.dirname(__file__)) with io.open(os.path.join(here, 'README.rst')) as f: readme = f.read() setup( name='flask-oidc', description='OpenID Connect extension for Flask', long_description=readme, url='https://github.com/puiterwijk/flask-oidc', author='Jeremy Ehrhardt, Patrick Uiterwijk', author_email='jeremy@bat-country.us, patrick@puiterwijk.org', version='1.4.0', packages=[ 'flask_oidc', ], install_requires=[ 'Flask', 'itsdangerous', 'oauth2client', 'six', ], tests_require=['nose', 'mock'], entry_points={ 'console_scripts': ['oidc-register=flask_oidc.registration_util:main'], }, zip_safe=False, classifiers=[ 'Environment :: Web Environment', 'Framework :: Flask', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], ) flask-oidc-1.4.0/setup.cfg0000664000175000017500000000013713263723662020002 0ustar puiterwijkpuiterwijk00000000000000[aliases] test = nosetests [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0