diff --git a/docs/examplesapp.rst b/docs/examplesapp.rst deleted file mode 100644 index f5d16f7c..00000000 --- a/docs/examplesapp.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. - This file is part of Invenio. - Copyright (C) 2015-2018 CERN. - - Invenio is free software; you can redistribute it and/or modify it - under the terms of the MIT License; see LICENSE file for more details. - -Example applications -==================== - -Example application -------------------- - -.. include:: ../examples/app.py - :start-after: SPHINX-START - :end-before: SPHINX-END - -Example OAuth2 Consumer ------------------------ - -.. include:: ../examples/consumer.py - :start-after: SPHINX-START - :end-before: SPHINX-END diff --git a/docs/index.rst b/docs/index.rst index 7b7ba953..aeb8a20d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,6 @@ Invenio-OAuth2Server. installation configuration usage - examplesapp API Reference diff --git a/examples/app-fixtures.sh b/examples/app-fixtures.sh deleted file mode 100755 index e4d81d49..00000000 --- a/examples/app-fixtures.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -# quit on errors: -set -o errexit - -# quit on unbound symbols: -set -o nounset - -DIR=`dirname "$0"` - -cd $DIR -export FLASK_APP=app.py - -## Load fixtures -flask users create reader@inveniosoftware.org -a --password 123456 -flask users create clientapp@inveniosoftware.org -a --password 123456 -flask users create admin@inveniosoftware.org -a --password 123456 - -# create admin role and add the role to a user -flask roles create admin -flask roles add reader@inveniosoftware.org admin -flask roles add admin@inveniosoftware.org admin - -# assign some allowed actions to this users -flask access allow admin-access role admin -flask access allow superuser-access role admin diff --git a/examples/app-setup.sh b/examples/app-setup.sh deleted file mode 100755 index cbf55e2b..00000000 --- a/examples/app-setup.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/sh - -# quit on errors: -set -o errexit - -# quit on unbound symbols: -set -o nounset - -DIR=`dirname "$0"` - -cd $DIR -export FLASK_APP=app.py - -# Install specific dependencies -pip install -r requirements.txt - -mkdir $DIR/instance - -# Preapare all static files: -npm install -g node-sass clean-css@3.4.19 requirejs uglify-js -flask npm -cd static ; npm install ; cd .. -flask collect -v -flask assets build - -# Create the database -flask db init -flask db create - -# Install requirements -npm install -g node-sass clean-css clean-css-cli requirejs uglify-js - -# Collect npm, requirements from registered bundles -flask npm - -# Now install npm requirements (requires that npm is already installed) -cd static ; npm install ; cd .. - -# Next, we copy the static files from the Python packages into the Flask -# application's static folder -flask collect -v - -# Finally, we build the webassets bundles -flask assets build diff --git a/examples/app-teardown.sh b/examples/app-teardown.sh deleted file mode 100755 index c6e88b02..00000000 --- a/examples/app-teardown.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -DIR=`dirname "$0"` - -cd $DIR -export FLASK_APP=app.py - -# Drop the database tables -flask db drop --yes-i-know - -# clean environment -[ -e "$DIR/instance" ] && rm -Rf $DIR/instance -[ -e "$DIR/static" ] && rm -Rf $DIR/static diff --git a/examples/app.py b/examples/app.py deleted file mode 100644 index e4e958b1..00000000 --- a/examples/app.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# Copyright (C) 2015-2018 CERN. -# -# Invenio is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -r"""Minimal Flask application example for development. - -SPHINX-START - -Run example development server: - -.. code-block:: console - - $ pip install -e .[all] - $ cd examples - $ ./app-setup.sh - $ ./app-fixtures.sh - $ FLASK_APP=app.py flask run -p 5000 - -Open settings page to generate a token: - -.. code-block:: console - - $ open http://127.0.0.1:5000/account/settings/applications - -Login with: - - | username: admin\@inveniosoftware.org - | password: 123456 - -Click on "New token" and compile the form: -insert the name "foobar", check scope "test:scope" and click "create". -The server will show you the generated Access Token. - -Make a request to test the token: - -.. code-block:: console - - export TOKEN= - curl -i -X GET -H "Content-Type:application/json" http://127.0.0.1:5000/ \ - -H "Authorization:Bearer $TOKEN" - -To end and remove any traces of example application, stop the example -application and run: -.. code-block:: console - - $ ./app-teardown.sh - - -SPHINX-END -""" - -from __future__ import absolute_import, print_function - -import os - -from flask import Flask, render_template -from flask_breadcrumbs import Breadcrumbs -from flask_oauthlib.provider import OAuth2Provider -from invenio_access import InvenioAccess -from invenio_accounts import InvenioAccountsREST, InvenioAccountsUI -from invenio_accounts.views import blueprint as blueprint_account -from invenio_admin import InvenioAdmin -from invenio_admin.views import blueprint as blueprint_admin_ui -from invenio_assets import InvenioAssets -from invenio_db import InvenioDB -from invenio_i18n import InvenioI18N -from invenio_theme import InvenioTheme - -from invenio_oauth2server import ( - InvenioOAuth2Server, - InvenioOAuth2ServerREST, - current_oauth2server, - require_api_auth, - require_oauth_scopes, -) -from invenio_oauth2server.models import Scope -from invenio_oauth2server.views import server_blueprint, settings_blueprint - -# Create Flask application -app = Flask(__name__) -app.config.update( - APP_ENABLE_SECURE_HEADERS=False, - CELERY_ALWAYS_EAGER=True, - CELERY_CACHE_BACKEND="memory", - CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, - CELERY_RESULT_BACKEND="cache", - OAUTH2_CACHE_TYPE="simple", - OAUTHLIB_INSECURE_TRANSPORT=True, - TESTING=True, - DEBUG=True, # needed to register localhost addresses as callback_urls - SECRET_KEY="test_key", - SECURITY_PASSWORD_HASH="plaintext", - SECURITY_PASSWORD_SCHEMES=["plaintext"], - SECURITY_DEPRECATED_PASSWORD_SCHEMES=[], - LOGIN_DISABLED=False, - TEMPLATE_AUTO_RELOAD=True, - SQLALCHEMY_TRACK_MODIFICATIONS=True, - SQLALCHEMY_DATABASE_URI=os.getenv( - "SQLALCHEMY_DATABASE_URI", "sqlite:///example.db" - ), - I18N_LANGUAGES=[ - ("de", "German"), - ("es", "Spanish"), - ("fr", "French"), - ("it", "Italian"), - ], -) -InvenioAssets(app) -InvenioTheme(app) -InvenioI18N(app) -Breadcrumbs(app) -InvenioDB(app) -InvenioAdmin(app) -InvenioAccess(app) -OAuth2Provider(app) -InvenioOAuth2ServerREST(app) - -accounts = InvenioAccountsUI(app) -InvenioAccountsREST(app) -app.register_blueprint(blueprint_account) - -InvenioOAuth2Server(app) - -# Register blueprints -app.register_blueprint(settings_blueprint) -app.register_blueprint(server_blueprint) -app.register_blueprint(blueprint_admin_ui) - -with app.app_context(): - # Register a test scope - current_oauth2server.register_scope( - Scope("test:scope", help_text="Access to the homepage", group="test") - ) - - -# @app.route('/jwt', methods=['GET']) -# def jwt(): -# """JWT.""" -# return render_template('jwt.html') - - -@app.route("/", methods=["GET", "POST"]) -@require_api_auth() -@require_oauth_scopes("test:scope") -def index(): - """Protected endpoint.""" - return "hello world" diff --git a/examples/consumer.py b/examples/consumer.py deleted file mode 100644 index 6f455022..00000000 --- a/examples/consumer.py +++ /dev/null @@ -1,411 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# Copyright (C) 2015-2018 CERN. -# -# Invenio is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""Minimal OAuth2 consumer implementation to demonstrate OAuth2 protocol - -SPHINX-START - -This example OAuth2 consumer application is used to fetch an OAuth2 access -token from example application. - -| For more information about OAuth2 protocol see -| https://invenio-oauthclient.readthedocs.io/en/latest/overview.html - -.. note:: Before continuing make sure example application is running. - -| - -Open settings page of example app to register a new OAuth2 application: - -.. code-block:: console - - $ open http://127.0.0.1:5000/account/settings/applications - -Login using: - - | **username:** admin\@inveniosoftware.org - | **password:** 123456 - -Click on "New application" and compile registration form with following data: - | **Name:** foobar-app - | **Description:** An example OAuth2 consumer application - | **Website URL:** \http://127.0.0.1:5100/ - | **Redirect URIs:** \http://127.0.0.1:5100/authorized - | **Client Type:** Confidential - - -Click register and example application will generate and show you -a Client ID and Client Secret. - -Open another terminal and move to examples-folder. - -Export these values using following environment variables before starting -the example consumer or change values of corresponding keys -in *examples/consumer.py* to match. - -.. code-block:: console - - $ export CONSUMER_CLIENT_ID= - $ export CONSUMER_CLIENT_SECRET= - -| - -**LOGOUT admin\@inveniosoftware.org from example application:** - -.. code-block:: console - - $ open http://127.0.0.1:5000/logout - -| - -Run the example consumer - -.. code-block:: console - - $ FLASK_APP=consumer.py flask run -p 5100 - -Start OAuth authorization flow and you will be redirected to example -application for authentication and to authorize example consumer to -access your account details on example application. - -Login to example application with: - - | **username:** reader\@inveniosoftware.org - | **password:** 123456 - -Review the authorization request presented to you and authorize -the example consumer. - -You will be redirected back to example consumer where you can see details -of the authorization token that example application generated to -example consumer. - -.. note:: - - In case the authorization flow ends in an error, you can usually see - the error in query-part of the URL. - -| - -Using example consumer's UI you can request a new access token from example -application either by using a refresh token or by completing -the authorization flow again. - -To manage settings of OAuth2 consumer at invenio-oauth2server -settings page, login with the account that registered the consumer, -admin\@inveniosoftware.org. - -To review and possibly revoke permissions of OAuth2 consumer that has -been authorized to access resources login with the account that authorized -the consumer, reader\@inveniosoftware.org. - - -| - - -This example consumer is inspired by example presented in -requests-oauthlib documentation -(http://requests-oauthlib.rtfd.io/en/latest/examples/real_world_example_with_refresh.html) -and is based on example application(s) of flask-oauthlib: -(https://github.com/lepture/flask-oauthlib/tree/master/example) -(https://github.com/lepture/flask-oauthlib/tree/master/example/contrib/experiment-client/douban.py) - -Note that to support automatic refreshing of access tokens this consumer uses -flask-oauthlib.contrib.client which is considered experimental. - -SPHINX-END -""" - -from __future__ import print_function - -import os -from functools import wraps -from pprint import pformat -from time import time - -from flask import Flask, redirect, request, session, url_for -from flask_oauthlib.contrib.client import OAuth -from requests_oauthlib import OAuth2Session - -# OAuth2 consumer configuration of this example consumer application -# You get the _CONSUMER_CLIENT_ID and _CONSUMER_CLIENT_SECRET when -# registering the consumer application to provider. -_CONSUMER_CLIENT_ID = "insert_generated_client_id_here" -_CONSUMER_CLIENT_SECRET = "insert_generated_client_secret_here" - -CONSUMER_CREDENTIALS = dict( - INVENIO_EXAMPLE_CONSUMER_CLIENT_ID=os.environ.get( - "CONSUMER_CLIENT_ID", _CONSUMER_CLIENT_ID - ), - INVENIO_EXAMPLE_CONSUMER_CLIENT_SECRET=os.environ.get( - "CONSUMER_CLIENT_SECRET", _CONSUMER_CLIENT_SECRET - ), - INVENIO_EXAMPLE_CONSUMER_SCOPE=[ - "test:scope", - # 'email', - ], -) - - -def create_app(): - # OAUTHLIB_INSECURE_TRANSPORT needed in order to use HTTP-endpoints - os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true" - - app = Flask(__name__) - app.config.update(CONSUMER_CREDENTIALS) - app.secret_key = "development" - - oauth = OAuth(app) - - # OAuth2 Provider configuration of invenio-oauth2server example app. - remote = oauth.remote_app( - name="invenio_example_consumer", - # client_id='', - # client_secret='', - # scope=['test:scope', 'email'], - version="2", - endpoint_url="http://127.0.0.1:5000/", - access_token_url="http://127.0.0.1:5000/oauth/token", - refresh_token_url="http://127.0.0.1:5000/oauth/token", - authorization_url="http://127.0.0.1:5000/oauth/authorize", - ) - - def oauth_token_required(f): - """Decorator that checks if consumer already has access_token - - Only checks the availability, not validity of the token. - """ - - @wraps(f) - def decorated_function(*args, **kwargs): - if get_oauth_token() is None: - # return redirect(url_for('.index')) - return ( - "
" - "

T_T You don't have a token yet

" - ' Get one from here'.format(url_for("index")) - ) - return f(*args, **kwargs) - - return decorated_function - - @app.route("/", methods=["GET"]) - def index(): - """Mainpage. Display info about token and an action menu.""" - if get_oauth_token(): - return """ -

^_^ Congratulations, you (already?) have an OAuth 2 token!

-

What would you like to do next?

- -
Current token information:
{}
- """.format( - pformat(get_oauth_token(), indent=4) - ) # noqa - - return """ -

OAuth2 consumer application

-

Please click the link to start OAuth authorization flow

- - """ - - @app.route("/login", methods=["GET"]) - def login(): - """Prepare an authorization request and redirect client to Provider - - Prepare an authorization request and redirect client / browser to - Provider for authentication and to answer the authorization request. - - Example of key-values sent to Provider in query part URI - - response_type: 'code' for authz flow, 'token' for implicit flow - - client_id: 'identifier of consumer generated by provider' - - redirect_uri: 'where client should be redirected after authorization' - - scope: [list of scopes consumer requesting authorization for] - - state: used to protect against CSRF - - """ - return remote.authorize(callback_uri=url_for("authorized", _external=True)) - - @app.route("/authorized", methods=["GET", "POST"]) - def authorized(): - """Endpoint where client is redirected after an authorization attempt. - - OAuth2 Provider will redirect client / browser to this endpoint - after an authorization attempt has been made. - - If authorization was granted redirect request contains - 'code'-parameter in querystring. - Consumer automatically requests an access token using this - 'code'-parameter form Provider's access_token_url - - Example of key values sent in access token request: - - code: 'value of code parameter obtained previously' - - client_id: 'identifier of consumer generated by provider' - - client_secret: 'secret of consumer generated by provider' - - grant_type: 'authorization code' as code - - redirect_url: used only to verify client is - - Example of response received from Provider - - access_token: 2YotnFZFEjr1zCsicMWpAA - - token_type: Bearer - - expires_in: 3600 - - refresh_token: tGzv3JOkF0XG5Qx2TlKWIA - - scopes: [list of scopes consumer is authorized to] - - Possible errors in authorization process are returned in querystring. - - """ - response = remote.authorized_response() - if response is None: - return ( - "
" - "

T_T Access denied

" - "
" - "
error={error}
" - '
Try again'.format( - link=url_for("index"), error=request.args["error"] - ) - ) - if isinstance(response, dict) and "access_token" in response: - store_oauth_token(response) - return redirect(url_for(".index")) - return str(response) - - @app.route("/auto_refresh", methods=["GET"]) - @oauth_token_required - def auto_refresh(): - """Obtaining a new access token automatically using a refresh token. - - By making a call to this endpoint the consumer set token to expired - state and upon making a new request for protected resource, consumer - first request a new access token using refresh token. - - Note that all requests and updates related to tokens happen - behind the scenes. - """ - - # We force expiration of the token by setting expired_at value - # of the token to a past moment. - # This will trigger an automatic refresh next time we interact with - # Invenio API. - token = get_oauth_token() - token["expires_at"] = time() - 10 - - resp = remote.get("oauth/info") # prefixed with remote.endpoint_url - return redirect(url_for(".index")) - - @app.route("/manual_refresh", methods=["GET"]) - @oauth_token_required - def manual_refresh(): - """Obtaining a new access token manually using a refresh token. - - Request for new access token can be sent even if the old - access token has not yet expired. - - Unfortunately flask-oauthlib OAuthApplication doesn't provide - manual refreshing of tokens out-of-the-box, so internal - OAuth2Session needs to be used. - - When using OAuth2Session to refresh, client_id and client_secret - must be provided as extra arguments. - When doing implicit / automatic refresh with flask-oauthlib - these values are automatically included in the request. - - Note that obtained new access token must be stored manually. - """ - - extra = { - "client_id": remote.client_id, - "client_secret": remote.client_secret, - } - - oauthsession = OAuth2Session(remote.client_id, token=get_oauth_token()) - - resp = oauthsession.refresh_token(remote.refresh_token_url, **extra) - - store_oauth_token(resp) - - # Alternative way - # # Build OAuth2Session - # oauthsession = remote.make_oauth_session(token=get_oauth_token()) - # - # # Use the built OAuth2Session to execute refresh token request - # # even though authorization token is still valid. - # store_oauth_token(oauthsession.refresh_token( - # remote.refresh_token_url)) - - return redirect(url_for(".index")) - - @app.route("/validate", methods=["GET"]) - @oauth_token_required - def validate(): - """Endpoint used to check validity of access token. - - Basically consumer tries to access a resource in Provider and - if the request succeeds the token is considered to be valid. - """ - resp = remote.get("oauth/info") # prefixed with remote.endpoint_url - return """Endpoint '/oauth/info' returned following information: -
{}
-
- Return - """.format( - pformat(resp.json(), indent=4) - ) - - @app.route("/logout", methods=["GET"]) - def logout(): - """Endpoint to logout from the consumer. - - Consumer will delete access and refresh tokens it has stored, losing - access to resources at Provider. - """ - - session.pop("oauth_token", None) - return redirect(url_for("index")) - - @remote.tokengetter - def get_oauth_token(): - """Returns the saved OAuth token object. - - Returned object contains all the information Provider sent - regarding to access token. - - Example of OAuth token object: - { "access_token": "6J3JyHLoP0K9HzL21V8y3Pj73zQDVf", - "token_type": "Bearer", - "expires_in": 3600, - "refresh_token": "RxFZ1CSkjJF9PeFqDCaKTWYB6V5N4w", - "scope": "test:scope" } - """ - return session.get("oauth_token") - - @remote.tokensaver - def store_oauth_token(resp): - """Saves the OAuth token and it's metadata obtained from Provider""" - # request-oauthlib expects a dictionary containing at least - # access_token and token_type values. Successful access token response - # must always contain those values, so we can store the whole response. - session["oauth_token"] = resp - - return app - - -app = create_app() - -if __name__ == "__main__": - app = create_app() - app.run(host="127.0.0.1", port=5100) diff --git a/examples/requirements.txt b/examples/requirements.txt deleted file mode 100644 index f38bea03..00000000 --- a/examples/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -invenio-access>=1.0.0a12 diff --git a/examples/templates/jwt.html b/examples/templates/jwt.html deleted file mode 100644 index 0d0ba293..00000000 --- a/examples/templates/jwt.html +++ /dev/null @@ -1,64 +0,0 @@ -{# -*- coding: utf-8 -*- - - This file is part of Invenio. - Copyright (C) 2017-2018 CERN. - - Invenio is free software; you can redistribute it and/or modify it - under the terms of the MIT License; see LICENSE file for more details. -#} -{%- extends 'invenio_theme/page.html' %} - -{%- block javascript %} - {{ super() }} - -{%- endblock %} -{%- block page_body %} -
-
- -
-
-
-
- {% if current_user.is_authenticated %} - {{ jwt() | safe }} - {% endif %} -
-
-
-{%- endblock %} diff --git a/setup.cfg b/setup.cfg index 93bd08cb..ea80f4de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ # # This file is part of Invenio. # Copyright (C) 2015-2018 CERN. -# Copyright (C) 2022-2023 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -28,14 +28,13 @@ python_requires = >=3.7 zip_safe = False install_requires = cachelib>=0.1 - Flask-Breadcrumbs>=0.4.0 Flask-OAuthlib>=0.9.5 Flask-WTF>=0.14.3 future>=0.16.0 - invenio-accounts>=1.3.1 - invenio-base>=1.2.4 + invenio-accounts>=5.0.0 + invenio-base>=1.3.0 invenio-i18n>=2.0.0 - invenio-theme>=1.3.4 + invenio-theme>=3.0.0 pyjwt>=1.5.0 requests-oauthlib>=1.1.0,<1.2.0 WTForms-Alchemy>=0.15.0 diff --git a/tests/conftest.py b/tests/conftest.py index 9d3ebad3..190e0c5c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ # # This file is part of Invenio. # Copyright (C) 2015-2018 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -20,7 +21,6 @@ from flask import Flask, url_for from flask.cli import ScriptInfo from flask.views import MethodView -from flask_breadcrumbs import Breadcrumbs from flask_mail import Mail from flask_menu import Menu from helpers import create_oauth_client, patch_request @@ -89,7 +89,6 @@ def init_app(app): InvenioI18N(app) Mail(app) Menu(app) - Breadcrumbs(app) InvenioDB(app) InvenioOAuth2Server(app) InvenioI18N(app)