flask-oidc-1.4.0/ 0000775 0001750 0001750 00000000000 13263723662 016160 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/README.rst 0000664 0001750 0001750 00000001711 13137632531 017641 0 ustar puiterwijk puiterwijk 0000000 0000000 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.
flask-oidc-1.4.0/flask_oidc/ 0000775 0001750 0001750 00000000000 13263723662 020256 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/flask_oidc/registration.py 0000664 0001750 0001750 00000012673 13137632531 023345 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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.py 0000664 0001750 0001750 00000003561 13137632531 022636 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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__.py 0000664 0001750 0001750 00000104211 13262435441 022360 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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.py 0000664 0001750 0001750 00000007113 13137632531 024373 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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/ 0000775 0001750 0001750 00000000000 13263723662 021750 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/flask_oidc.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13263723661 026013 0 ustar puiterwijk puiterwijk 0000000 0000000
flask-oidc-1.4.0/flask_oidc.egg-info/entry_points.txt 0000664 0001750 0001750 00000000105 13263723661 025241 0 ustar puiterwijk puiterwijk 0000000 0000000 [console_scripts]
oidc-register = flask_oidc.registration_util:main
flask-oidc-1.4.0/flask_oidc.egg-info/requires.txt 0000644 0001750 0001750 00000000044 13263723661 024343 0 ustar puiterwijk puiterwijk 0000000 0000000 Flask
itsdangerous
oauth2client
six
flask-oidc-1.4.0/flask_oidc.egg-info/top_level.txt 0000644 0001750 0001750 00000000013 13263723661 024471 0 ustar puiterwijk puiterwijk 0000000 0000000 flask_oidc
flask-oidc-1.4.0/flask_oidc.egg-info/not-zip-safe 0000644 0001750 0001750 00000000001 13137632531 024166 0 ustar puiterwijk puiterwijk 0000000 0000000
flask-oidc-1.4.0/flask_oidc.egg-info/PKG-INFO 0000644 0001750 0001750 00000004272 13263723661 023047 0 ustar puiterwijk puiterwijk 0000000 0000000 Metadata-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.txt 0000644 0001750 0001750 00000001474 13263723661 023637 0 ustar puiterwijk puiterwijk 0000000 0000000 LICENSE.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.txt flask-oidc-1.4.0/MANIFEST.in 0000664 0001750 0001750 00000000404 13137632531 017706 0 ustar puiterwijk puiterwijk 0000000 0000000 include 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.txt 0000664 0001750 0001750 00000000044 13137632531 021434 0 ustar puiterwijk puiterwijk 0000000 0000000 Flask
itsdangerous
oauth2client
six
flask-oidc-1.4.0/LICENSE.txt 0000664 0001750 0001750 00000002556 13137632531 020005 0 ustar puiterwijk puiterwijk 0000000 0000000 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.
flask-oidc-1.4.0/docs/ 0000775 0001750 0001750 00000000000 13263723662 017110 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/docs/Makefile 0000664 0001750 0001750 00000017620 13137632531 020550 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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.rst 0000664 0001750 0001750 00000022076 13262435441 020752 0 ustar puiterwijk puiterwijk 0000000 0000000 Flask-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.py 0000664 0001750 0001750 00000022626 13137632531 020411 0 ustar puiterwijk puiterwijk 0000000 0000000 # -*- 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/ 0000775 0001750 0001750 00000000000 13263723662 020534 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/docs/_themes/README 0000664 0001750 0001750 00000002105 13137632531 021404 0 ustar puiterwijk puiterwijk 0000000 0000000 Flask 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/LICENSE 0000664 0001750 0001750 00000003375 13137632531 021543 0 ustar puiterwijk puiterwijk 0000000 0000000 Copyright (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/.git 0000664 0001750 0001750 00000000050 13137632531 021305 0 ustar puiterwijk puiterwijk 0000000 0000000 gitdir: ../../.git/modules/docs/_themes
flask-oidc-1.4.0/docs/_themes/flask_theme_support.py 0000664 0001750 0001750 00000011413 13137632531 025156 0 ustar puiterwijk puiterwijk 0000000 0000000 # 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/.gitignore 0000664 0001750 0001750 00000000026 13137632531 022514 0 ustar puiterwijk puiterwijk 0000000 0000000 *.pyc
*.pyo
.DS_Store
flask-oidc-1.4.0/docs/_themes/flask/ 0000775 0001750 0001750 00000000000 13263723662 021634 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/docs/_themes/flask/static/ 0000775 0001750 0001750 00000000000 13263723662 023123 5 ustar puiterwijk puiterwijk 0000000 0000000 flask-oidc-1.4.0/docs/_themes/flask/static/flasky.css_t 0000664 0001750 0001750 00000021546 13137632531 025453 0 ustar puiterwijk puiterwijk 0000000 0000000 /*
* 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.html 0000664 0001750 0001750 00000001265 13137632531 024035 0 ustar puiterwijk puiterwijk 0000000 0000000 {%- extends "basic/layout.html" %}
{%- block extrahead %}
{{ super() }}
{% if theme_touch_icon %}
{% endif %}
{% endblock %}
{%- block relbar2 %}{% endblock %}
{% block header %}
{{ super() }}
{% if pagename == 'index' %}