OAuthClient

The oauthclient module provides OAuth web authorization support in Invenio.

OAuth client support is typically used to allow features such as social login (e.g. Sign in with Twitter) and access to resources owner by a specific user at a remote service. Both OAuth 1.0 and OAuth 2.0 are supported.

The module contains:

  • Views: OAuth login and authorized endpoints, linked account settings and sign-up handling.
  • Client: A client to interact with remote applications.
  • Contrib: Ready-to-use GitHub and ORCID remote applications.
  • Models: Persistence layer for OAuth access tokens including support for storing extra data together with a token.
  • Handlers: Customizable handlers for deciding what happens when a user authorizes a request.

Authorization Flow Overview

OAuth 2.0 defines several possible authorization flows depending on the type of client you are authorizing (e.g. web application, browser-based app or mobile apps). The web application client is the only authorization flow supported by this module.

A typical web application authorization flow involves the following roles:

  • Client (i.e. a third-party application in this case your Invenio instance).
  • Resource server (i.e. the remote service).
  • Resource owner (i.e. the user).

The web application authorization flow is used to e.g. allow sign in with service X. The end result of a completed authorization flow is an access token which allows the client to access a resource owner’s resources on the resource server.

Before the authorization flow is started, the client must be registered with the resource server. The resource server will provide a client key and client secret to the client. Following is an example of the authorization flow with ORCID:

  1. The resource owner (i.e. the user) clicks “Sign in with ORCID”:

    GET /oauth/login/orcid/ HTTP/1.1
    

    The client redirects the user to the resource server’s authorize URL.

    HTTP/1.1 302 FOUND
    Location: https://orcid.org/oauth/authorize?response_type=code&client_id=<CLIENT KEY>&redirect_uri=https://localhost/oauth/authorized/orcid/&scope=/authenticate&state=...
    

Note, following query parameters in the authorize URL:

  • response_type - Must be code for web application flow (named authorization code grant).
  • client_id - The client key provided by the resource server when the client was registered.
  • redirect_uri - The URL the resource server will redirect the resource owner back to after having authorized the request. Usually the redirect URL must be provided when registering the client application with the resource server.
  • scope - Defines the level of access (defined by the resource server)
  • state - A token to mitigate against cross-site request forgery (CRSF). In Invenio this state is a JSON Web Signature (JWS) that by default expires after 5 minutes.
  1. The resource server asks the user to sign-in (if not already signed in).

  2. The resource server asks the resource owner to authorize or reject the client’s request for access.

  3. If the resource owner authorizes the request, the resource server redirects the resource owner back to the client web application (using the redirect_uri provided in step 1):

    HTTP/1.1 302 FOUND
    Location: https://localhost/oauth/authorized/orcid/?code=<CODE>&state=...
    

    Included in the redirect is a one-time auth code which is typically only valid for short time (seconds), as well as the state token initially provided.

  4. The client now exchanges the one time auth code for an access token using the resource server’s access token URL:

    POST https://pub.orcid.org/oauth/token HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code&
    code=<CODE>&
    redirect_uri=<REDIRECT_URI>&
    client_id=<CLIENT KEY>&
    client_secret=<CLIENT SECRET>
    

    The resource server replies with an access token:

    {"access_token": "<ACCESS TOKEN>}
    

    The client stores the access token, and can use it to make authenticated requests to the resource server:

    GET https://api.example.org/ HTTP/1.1
    Authorization: Bearer <ACCESS TOKEN>
    

Further reading:

Usage

  1. Edit your configuration. Ensure invenio.modules.oauthclient is included in PACKAGES (by default it’s included).

  2. Define remote resource serves in the OAUTHCLIENT_REMOTE_APPS.

    PACKAGES = [
        # ...
        'invenio.modules.oauthclient',
        # ...
    ]
    
    OAUTHCLIENT_REMOTE_APPS = dict(
        # ...
    )
    

See Configuration for how to define remote applications in OAUTHCLIENT_REMOTE_APPS.

Configuration

Configuration variables for defining remote applications.

OAUTHCLIENT_REMOTE_APPS Dictionary of remote applications. See example below. Default: {}.
OAUTHCLIENT_SESSION_KEY_PREFIX Prefix for the session key used to store the an access token. Default: oauth_token.
OAUTHCLIENT_STATE_EXPIRES Number of seconds after which the state token expires. Defaults to 300 seconds.

Each remote application must be defined in the OAUTHCLIENT_REMOTE_APPS dictionary, where the keys are the application names and the values the configuration parameters for the application.

OAUTHCLIENT_REMOTE_APPS = dict(
    myapp=dict(
        # configuration values for myapp ...
    ),
)

The application name is used in the login, authorized, sign-up and disconnect endpoints:

  • Login endpoint: /oauth/login/<REMOTE APP>/.
  • Authorized endpoint: /oauth/authorized/<REMOTE APP>/.
  • Disconnect endpoint: /oauth/disconnect/<REMOTE APP>/.
  • Sign up endpoint: /oauth/login/<REMOTE APP>/.

Remote application

Configuration of a single remote application is a dictionary with the following keys:

  • title - Title of remote application. Displayed to end-users under Account > Linked accounts.
  • description - Short description of remote application. Displayed to end-users under Account > Linked accounts.
  • icon - CSS class for icon of service (e.g. fa fa-github for using the Font-Awesome GitHub icon). Displayed to end-users.
  • params - Flask-OAuthlib remote application parameters..
  • authorized_handler - Import path to authorized callback handler.
  • disconnect_handler - Import path to disconnect callback handler.
  • signup_handler - A dictionary of import path to sign up callback handler.
  • remember - Boolean indicating if the session should be permament.
OAUTHCLIENT_REMOTE_APPS = dict(
    myapp=dict(
        title='...',
        description='...',
        icon='...',
        authorized_handler="...",
        disconnect_handler="...",
        signup_handler=dict(
            info="...",
            setup="...",
            view="...",
        ),
        params=dict(...),
        remember=True
        )
    )
)

Flask-OAuthlib parameters

The Flask-OAuthlib parameters defines the remote application OAuth endpoints as well as the client id and secret. Full description of these parameters are given in the Flask-OAuthlib documentation.

Normally you will have to browse the remote application’s API documentation to find which URLs and scopes to use.

Below is an example for GitHub:

OAUTHCLIENT_REMOTE_APPS = dict(
    github=dict(
        # ...
        params=dict(
            request_token_params={'scope': 'user:email'},
            base_url='https://api.github.com/',
            request_token_url=None,
            access_token_url="https://github.com/login/oauth/access_token",
            access_token_method='POST',
            authorize_url="https://github.com/login/oauth/authorize",
            app_key="GITHUB_APP_CREDENTIALS",
        )
    )
)

GITHUB_APP_CREDENTIALS=dict(
    consumer_key="changeme"
    consumer_secret="changeme"
)

The app_key parameter allows you to put your sensitive client id and secret in your instance configuration (var/invenio.base-instance/invenio.cfg).

Handlers

Handlers allow customizing oauthclient endpoints for each remote application:

  • Authorized endpoint: /oauth/authorized/<REMOTE APP>/.
  • Disconnect endpoint: /oauth/disconnect/<REMOTE APP>/.
  • Sign up endpoint: /oauth/login/<REMOTE APP>/.

By default only authorized and disconnect handlers are required, and Invenio provide default implementation that stores the access token in the user session as well as to the database if the user is authenticated:

OAUTHCLIENT_REMOTE_APPS = dict(
    myapp=dict(
        # ...
        authorized_handler="invenio.modules.oauthclient.handlers"
                   ":authorized_default_handler",
        disconnect_handler="invenio.modules.oauthclient.handlers"
                   ":disconnect_handler",
        )
        # ...
    )
)

If you want to provide sign in/up functionality using oauthclient, Invenio comes with a default handler that will try to find a matching local user for a given authorize request.

OAUTHCLIENT_REMOTE_APPS = dict(
    orcid=dict(
        # ...
        authorized_handler="invenio.modules.oauthclient.handlers"
                   ":authorized_signup_handler",
        disconnect_handler="invenio.modules.oauthclient.handlers"
                   ":disconnect_handler",
        )
        signup_handler=dict(
            info="invenio.modules.oauthclient.contrib.orcid:account_info",
            setup="invenio.modules.oauthclient.contrib.orcid:account_setup",
            view="invenio.modules.oauthclient.handlers:signup_handler",
        ),
        # ...
    )
)

Contrib

GitHub

Pre-configured remote application for enabling sign in/up with GitHub.

Usage:

  1. Ensure you have github3.py package installed:

    cdvirtualenv src/invenio
    pip install -e .[github]
    
  2. Edit your configuration and add:

    from invenio.modules.oauthclient.contrib import github
    OAUTHCLIENT_REMOTE_APPS = dict(
        github=github.REMOTE_APP,
    )
    
    GITHUB_APP_CREDENTIALS = dict(
        consumer_key="changeme",
        consumer_secret="changeme",
    )
    
  3. Go to GitHub and register a new application: https://github.com/settings/applications/new. When registering the application ensure that the Authorization callback URL points to: CFG_SITE_SECURE_URL/oauth/authorized/github/ (e.g. http://localhost:4000/oauth/authorized/github/ for development).

  4. Grab the Client ID and Client Secret after registering the application and add them to your instance configuration (invenio.cfg):

    GITHUB_APP_CREDENTIALS = dict(
        consumer_key="<CLIENT ID>",
        consumer_secret="<CLIENT SECRET>",
    )
    
  5. Now go to CFG_SITE_SECURE_URL/oauth/login/github/ (e.g. http://localhost:4000/oauth/login/github/)

  6. Also, you should see GitHub listed under Linked accounts: http://localhost:4000//account/settings/linkedaccounts/

By default the GitHub module will try first look if a link already exists between a GitHub account and a user. If no link is found, the module tries to retrieve the user email address from GitHub to match it with a local user. If this fails, the user is asked to provide an email address to sign-up.

In templates you can add a sign in/up link:

<a href="{{url_for('oauthclient.login', remote_app='github')}}">Sign in with GitHub</a>

ORCID

Pre-configured remote application for enabling sign in/up with ORCID.

Usage:

  1. Edit your configuration and add:

    from invenio.modules.oauthclient.contrib import orcid
    OAUTHCLIENT_REMOTE_APPS = dict(
        orcid=orcid.REMOTE_APP,
    )
    
    ORCID_APP_CREDENTIALS = dict(
        consumer_key="changeme",
        consumer_secret="changeme",
    )
    
Note, if you want to use the ORCID sandbox, use orcid.REMOTE_SANDBOX_APP instead of orcid.REMOTE_APP.
  1. Register a new application with ORCID. When registering the application ensure that the Redirect URI points to: CFG_SITE_SECURE_URL/oauth/authorized/orcid/ (note, ORCID does not allow localhost to be used, thus testing on development machines is somewhat complicated by this).

  2. Grab the Client ID and Client Secret after registering the application and add them to your instance configuration (invenio.cfg):

    ORCID_APP_CREDENTIALS = dict(
        consumer_key="<CLIENT ID>",
        consumer_secret="<CLIENT SECRET>",
    )
    
  3. Now go to CFG_SITE_SECURE_URL/oauth/login/orcid/ (e.g. http://localhost:4000/oauth/login/orcid/)

  4. Also, you should see ORCID listed under Linked accounts: http://localhost:4000//account/settings/linkedaccounts/

By default the ORCID module will try first look if a link already exists between a ORCID account and a user. If no link is found, the user is asked to provide an email address to sign-up.

In templates you can add a sign in/up link:

<a href="{{url_for('oauthclient.login', remote_app='orcid')}}">
  Sign in with ORCID
</a>

API

Handlers

Handlers for customizing oauthclient endpoints.

invenio.modules.oauthclient.handlers.authorized_default_handler(*args, **kwargs)

Store access token in session.

Default authorized handler.

invenio.modules.oauthclient.handlers.authorized_signup_handler(*args, **kwargs)

Handle sign-in/up functionality.

invenio.modules.oauthclient.handlers.disconnect_handler(remote, *args, **kwargs)

Handle unlinking of remote account.

This default handler will just delete the remote account link. You may wish to extend this module to perform clean-up in the remote service before removing the link (e.g. removing install webhooks).

invenio.modules.oauthclient.handlers.get_session_next_url(remote_app)

Return redirect url stored in session.

invenio.modules.oauthclient.handlers.make_handler(f, remote, with_response=True)

Make a handler for authorized and disconnect callbacks.

Parameters:f – Callable or an import path to a callable
invenio.modules.oauthclient.handlers.make_token_getter(remote)

Make a token getter for a remote application.

invenio.modules.oauthclient.handlers.oauth1_token_setter(remote, resp, token_type='', extra_data=None)

Set an OAuth1 token.

invenio.modules.oauthclient.handlers.oauth2_handle_error(remote, resp, error_code, error_uri, error_description)

Handle errors during exchange of one-time code for an access tokens.

invenio.modules.oauthclient.handlers.oauth2_token_setter(remote, resp, token_type='', extra_data=None)

Set an OAuth2 token.

The refresh_token can be used to obtain a new access_token after the old one is expired. It is saved in the database for long term use. A refresh_token will be present only if access_type=offline is included in the authorization code request.

invenio.modules.oauthclient.handlers.oauth_error_handler(f)

Decorator to handle exceptions.

invenio.modules.oauthclient.handlers.oauth_logout_handler(sender_app, user=None)

Remove all access tokens from session on logout.

invenio.modules.oauthclient.handlers.response_token_setter(remote, resp)

Extract token from response and set it for the user.

invenio.modules.oauthclient.handlers.set_session_next_url(remote_app, url)

Store redirect url in session for security reasons.

invenio.modules.oauthclient.handlers.signup_handler(remote, *args, **kwargs)

Handle extra signup information.

invenio.modules.oauthclient.handlers.token_delete(remote, token='')

Remove OAuth access tokens from session.

invenio.modules.oauthclient.handlers.token_getter(remote, token='')

Retrieve OAuth access token.

Used by flask-oauthlib to get the access token when making requests.

Parameters:token – Type of token to get. Data passed from oauth.request() to identify which token to retrieve.
invenio.modules.oauthclient.handlers.token_session_key(remote_app)

Generate a session key used to store the token for a remote app.

invenio.modules.oauthclient.handlers.token_setter(remote, token, secret='', token_type='', extra_data=None)

Set token for user.

Models

Models for storing access tokens and links between users and remote apps.

class invenio.modules.oauthclient.models.RemoteAccount(**kwargs)

Storage for remote linked accounts.

client_id

Client ID of remote application (defined in OAUTHCLIENT_REMOTE_APPS).

classmethod create(user_id, client_id, extra_data)

Create new remote account for user.

Parameters:
  • user_id – User id.
  • client_id – Client id.
  • extra_data – JSON-serializable dictionary of any extra data that needs to be save together with this link.
delete()

Delete remote account together with all stored tokens.

extra_data

Extra data associated with this linked account.

classmethod get(user_id, client_id)

Get RemoteAccount object for user.

Parameters:
  • user_id – User id
  • client_id – Client id.
id

Primary key.

tokens

SQLAlchemy relationship to RemoteToken objects.

user

SQLAlchemy relationship to user.

user_id

Local user linked with a remote app via the access token.

class invenio.modules.oauthclient.models.RemoteToken(**kwargs)

Storage for the access tokens for linked accounts.

access_token

Access token to remote application.

classmethod create(user_id, client_id, token, secret, token_type='', extra_data=None)

Create a new access token.

Creates RemoteAccount as well if it does not exists.

classmethod get(user_id, client_id, token_type='', access_token=None)

Get RemoteToken for user.

classmethod get_by_token(client_id, access_token, token_type='')

Get RemoteAccount object for token.

id_remote_account

Foreign key to account.

secret

Used only by OAuth 1.

token()

Get token as expected by Flask-OAuthlib.

token_type

Type of token.

update_token(token, secret)

Update token with new values.

Views

Client blueprint used to handle OAuth callbacks.

invenio.modules.oauthclient.views.client.authorized(remote_app=None)

Authorized handler callback.

invenio.modules.oauthclient.views.client.disconnect(remote_app)

Disconnect user from remote application.

Removes application as well as associated information.

invenio.modules.oauthclient.views.client.login(remote_app)

Send user to remote application for authentication.

invenio.modules.oauthclient.views.client.setup_app()

Setup OAuth clients.

invenio.modules.oauthclient.views.client.signup(remote_app)

Extra signup step.

Account settings blueprint for oauthclient.

invenio.modules.oauthclient.views.settings.index(*args, **kwargs)

List linked accounts.

Forms

Forms for module.

class invenio.modules.oauthclient.forms.EmailSignUpForm(*args, **kwargs)

Form for requesting email address during sign up process.

validate_email(field)

Validate email address.

Ensures that the email address is not already registered.

Utils

Utility methods to help find, authenticate or register a remote user.

invenio.modules.oauthclient.utils.oauth_authenticate(client_id, userinfo, require_existing_link=False, remember=False)

Authenticate an oauth authorized callback.

invenio.modules.oauthclient.utils.oauth_get_user(client_id, account_info=None, access_token=None)

Retrieve user object for the given request.

Uses either the access token or extracted account information to retrieve the user object.

Link a user to an external id.

invenio.modules.oauthclient.utils.oauth_register(account_info, form_data=None)

Register user if possible.

Unlink a user from an external id.

Registries

Registries of handlers for oauthclient.

invenio.modules.oauthclient.client.account_handlers = <flask_registry.registries.core.DictRegistry object>

Registry of handlers for initializing an account.

invenio.modules.oauthclient.client.disconnect_handlers = <flask_registry.registries.core.DictRegistry object>

Registry of handlers for authorized handler callbacks.

invenio.modules.oauthclient.client.handlers = <flask_registry.registries.core.DictRegistry object>

Registry of handlers for authorized handler callbacks.

invenio.modules.oauthclient.client.oauth = <flask_oauthlib.client.OAuth object>

Flask-OAuthlib extension.

invenio.modules.oauthclient.client.signup_handlers = <flask_registry.registries.core.DictRegistry object>

Registry of handlers for signup handlers.

Errors

Module level errors.

exception invenio.modules.oauthclient.errors.OAuthClientError(message, remote, response)

Define OAuth client exception.

Client errors happens when the client (i.e. Invenio) creates an invalid request.

exception invenio.modules.oauthclient.errors.OAuthError(message, remote)

Base class for OAuth exceptions.

exception invenio.modules.oauthclient.errors.OAuthRejectedRequestError(message, remote, response)

Define exception of rejected response during OAuth process.

exception invenio.modules.oauthclient.errors.OAuthResponseError(message, remote, response)

Define response exception during OAuth process.