Authentication

Authentication in EVE-SRP was designed from the start to allow for multiple different authentication systems and to make it easy to integrate it with an existing authentication system.

As an exercise in how to write your own authentication plugin, let’s write one that doesn’t rely on an external service. We’ll need to subclass two classes for this; AuthMethod and User

Let’s start with subclassing User. This class is mapped to an SQL table using SQLAlchemy’s declarative extension (more specifically, the Flask-SQLAlchemy plugin for Flask). The parent class automatically sets up the table name and inheritance mapper arguments for you, so all you need to do is provide the id attribute that links your class with the parent class and an attribute to store the password hash. In the example below, we’re using the simple-pbkdf2 package to provide the password hashing. We also have a checking method to make life easier for us later.

import os
from hashlib import sha512
from evesrp import db
from evesrp.auth.models import User
from pbkdf2 import pbkdf2_bin


class LocalUser(User):
    id = db.Column(db.Integer, db.ForeignKey(User.id), primary_key=True)
    password = db.Column(db.LargeBinary(24), nullable=False)
    salt = db.Column(db.LargeBinary(24), nullable=False)

    def __init__(self, name, password, authmethod, **kwargs):
        self.salt = os.urandom(24)
        self.password = pbkdf2_bin(password.encode('utf-8'), self.salt,
                iterations=10000)
        super(LocalUser, self).__init__(name, authmethod, **kwargs)

    def check_password(self, password):
        key = pbkdf2_bin(password.encode('utf-8'), self.salt,
                iterations=10000)
        matched = 0
        for a, b in zip(self.password, key):
            matched |= ord(a) ^ ord(b)
        return matched == 0

AuthMethod subclasses have four methods they can implement to customize thier behavior.

  • AuthMethod.form() returns a Form subclass that represents the necessary fields.
  • AuthMethod.login() performs the actual login process. As part of this, it is passed an instance of the class given by AuthMethod.form() with the submitted data via the form argument.
  • For those authentication methods that requires a secondary view/route, the AuthMethod.view() method can be implemented to handle requests made to login/safe_name where safe_name is the output of AuthMethod.safe_name.
  • Finally, the initializer should be overridden to provide a default name for your AuthMethod other than Base Authentication.
  • Finally, the initializer can be overridden to handle specialized configurations.

With these in mind, let’s implement our AuthMethod subclass:

from evesrp.auth import AuthMethod
from flask import redirect, url_for, render_template, request
from flask_wtf import Form
from sqlalchemy.orm.exc import NoResultFound
from wtforms.fields import StringField, PasswordField, SubmitField
from wtforms.validators import InputRequired, EqualTo


class LocalLoginForm(Form):
    username = StringField('Username', validators=[InputRequired()])
    password = PasswordField('Password', validators=[InputRequired()])
    submit = SubmitField('Log In')


class LocalCreateUserForm(Form):
    username = StringField('Username', validators=[InputRequired()])
    password = PasswordField('Password', validators=[InputRequired(),
            EqualTo('password_repeat', message='Passwords must match')])
    password_repeat = PasswordField(
            'Repeat Password', validators=[InputRequired()])
    submit = SubmitField('Log In')


class LocalAuth(AuthMethod):

    def form(self):
        return LocalLoginForm

    def login(self, form):
        # form has already been validated, we just need to process it.
        try:
            user = LocalUser.query.filter_by(name=form.username.data).one()
        except NoResultFound:
            flash("No user found with that username.", 'error')
            return redirect(url_for('login.login'))
        if user.check_password(form.password.data):
            self.login_user(user)
            return redirect(request.args.get('next') or url_for('index'))
        else:
            flash("Incorrect password.", 'error')
            return redirect(url_for('login.login'))

    def view(self):
        form = LocalCreateUserForm()
        if form.validate_on_submit():
            user = LocalUser(form.username.data, form.password.data)
            db.session.add(user)
            db.session.commit()
            self.login_user(user)
            return redirect(url_for('index'))
        # form.html is a template included in Eve-SRP that renders all
        # elements of a form.
        return render_template('form.html', form=form)

That’s all that’s necessary for a very simple AuthMethod. This example cuts some corners, and isn’t ready for production-level use, but it serves as a quick example of what’s necessary to write a custom authentication method. Feel free to look at the sources for the included AuthMethods below to gather ideas on how to use more complicated mechanisms.

Included Authentication Methods

Brave Core

class evesrp.auth.bravecore.BraveCore(client_key, server_key, identifier, url='https://core.braveineve.com', **kwargs)[source]

Bases: evesrp.auth.AuthMethod

__init__(client_key, server_key, identifier, url='https://core.braveineve.com', **kwargs)[source]

Authentication method using a Brave Core instance.

Uses the native Core API to authenticate users. Currently only supports a single character at a time due to limitations in Core’s API.

Parameters:
  • client_key (str) – The client’s private key in hex form.
  • server_key (str) – The server’s public key for this app in hex form.
  • identifier (str) – The identifier for this app in Core.
  • url (str) – The URL of the Core instance to authenticate against. Default: ‘https://core.braveineve.com
  • name (str) – The user-facing name for this authentication method. Default: ‘Brave Core’

TEST Legacy

class evesrp.auth.testauth.TestAuth(api_key=None, **kwargs)[source]

Bases: evesrp.auth.AuthMethod

__init__(api_key=None, **kwargs)[source]

Authentication method using TEST Auth‘s legacy (a.k.a v1) API.

Parameters:
  • api_key (str) – (optional) An Auth API key. Without this, only primary characters are able to be accessed/used.
  • name (str) – The user-facing name for this authentication method. Default: ‘Test Auth’

OAuth

A number of external authentication services have an OAuth provider for external applications to use with their API. To facilitate usage of thses services, an OAuthMethod class has been provided for easy integration. Subclasses will need to implement the get_user(), get_pilots() and get_groups() methods. Additionally, implementations for JFLP's provider and TEST's provider have been provided as a reference.

class evesrp.auth.oauth.OAuthMethod(**kwargs)[source]
__init__(**kwargs)[source]

Abstract AuthMethod for OAuth-based login methods.

Implementing classes need to implement get_user(), get_pilots(), and get_groups().

In addition to the keyword arguments from AuthMethod, this initializer accepts the following arguments that will be used in the creation of the OAuthMethod.oauth object (See the documentation for OAuthRemoteApp for more details):

  • client_id
  • client_secret
  • scope
  • access_token_url
  • refresh_token_url
  • authorize_url
  • access_token_params
  • method

As a convenience, the key and secret keyword arguments will be treated as consumer_key and consumer_secret respectively. The name argument is used for both AuthMethod and for OAuthRemoteApp.

Subclasses for providers that may be used by more than one entity are encouraged to provide their own defaults for the above arguments.

The redirect URL for derived classes is based off of the safe_name of the implementing AuthMethod, specifically the URL for view(). For example, the default redirect URL for TestOAuth is similar to https://example.com/login/test_oauth/ (Note the trailing slash, it is significant).

Parameters:default_token_expiry (int) – The default time (in seconds) access tokens are valid for. Defaults to 5 minutes.
get_groups()[source]

Returns a list of Groups for the given token.

Like get_user() and get_pilots(), this method is to be implemented by OAuthMethod subclasses to return a list of Groups associated with the account for the given access token.

Return type:list of Groups.
get_pilots()[source]

Return a list of Pilots for the given token.

Like get_user(), this method is to be implemented by OAuthMethod subclasses to return a list of Pilots associated with the account for the given access token.

Return type:list of Pilots.
get_user()[source]

Returns the OAuthUser instance for the current token.

This method is to be implemented by subclasses of OAuthMethod to use whatever APIs they have access to to get the user account given an access token.

Return type:OAuthUser
is_admin(user)[source]

Returns wether this user should be treated as a site-wide administrator.

The default implementation checks if the user’s name is contained within the list of administrators supplied as an argument to OAuthMethod.

Parameters:user (OAuthUser) – The user to check.
Return type:bool
refresh(user)[source]

Refreshes the current user’s information.

Attempts to refresh the pilots and groups for the given user. If the current access token has expired, the refresh token is used to get a new access token.

view()[source]

Handle creating and/or logging in the user and updating their Pilots and Groups.

EVE SSO

class evesrp.auth.evesso.EveSSO(singularity=False, **kwargs)[source]

Bases: evesrp.auth.oauth.OAuthMethod

get_groups()[source]

Set the user’s groups for their pilot.

At this time, Eve SSO only gives us character access, so they’re just set to the pilot’s corporation, and if they have on their alliance as well. In the future, this method may also add groups for mailing lists.

J4OAuth

class evesrp.auth.j4oauth.J4OAuth(base_url='https://j4lp.com/oauth/api/v1/', **kwargs)[source]

Bases: evesrp.auth.oauth.OAuthMethod

__init__(base_url='https://j4lp.com/oauth/api/v1/', **kwargs)[source]

AuthMethod for using J4OAuth as an authentication source.

Parameters:
  • authorize_url (str) – The URL to request OAuth authorization tokens. Default: 'https://j4lp.com/oauth/authorize'.
  • access_token_url (str) – The URL for OAuth token exchange. Default: 'https://j4lp.com/oauth/token'.
  • base_str (str) – The base URL for API requests. Default: 'https://j4lp.com/oauth/api/v1/'.
  • request_token_params (dict) – Additional parameters to include with the authorization token request. Default: {'scope': ['auth_info', 'auth_groups', 'characters']}.
  • access_token_method (str) – HTTP Method to use for exchanging authorization tokens for access tokens. Default: 'GET'.
  • name (str) – The name for this authentication method. Default: 'J4OAuth'.

TestOAuth

class evesrp.auth.testoauth.TestOAuth(devtest=False, **kwargs)[source]

Bases: evesrp.auth.oauth.OAuthMethod

__init__(devtest=False, **kwargs)[source]

AuthMethod using TEST Auth’s OAuth-based API for authentication and authorization.

Parameters:
  • admins (list) – Two types of values are accepted as values in this list, either a string specifying a user’s primary character’s name, or their Auth ID as an integer.
  • devtest (bool) – Testing parameter that changes the default domain for URLs from ‘https://auth.pleaseignore.com‘ to ‘https://auth.devtest.pleaseignore.com`. Default: False.
  • authorize_url (str) – The URL to request OAuth authorization tokens. Default: 'https://auth.pleaseignore.com/oauth2/authorize'.
  • access_token_url (str) – The URL for OAuth token exchange. Default: 'https://auth.pleaseignore.com/oauth2/access_token'.
  • base_str (str) – The base URL for API requests. Default: 'https://auth.pleaseignore.com/api/v3/'.
  • request_token_params (dict) – Additional parameters to include with the authorization token request. Default: {'scope': 'private-read'}.
  • access_token_method (str) – HTTP Method to use for exchanging authorization tokens for access tokens. Default: 'POST'.
  • name (str) – The name for this authentication method. Default: 'Test OAuth'.

Low-Level API

class evesrp.auth.PermissionType[source]

Enumerated type for the types of permissions available.

elevated[source]

Returns a frozenset of the permissions above submit.

all[source]

Returns a frozenset of all possible permission values.

admin = <admin>

Division-level administrator permission

audit = <audit>

A special permission for allowing read-only elevated access

pay = <pay>

Permission for payers in a Division.

review = <review>

Permission for reviewers of requests in a Division.

submit = <submit>

Permission allowing submission of Requests to a Division.

class evesrp.auth.AuthMethod(admins=None, name='Base Authentication', **kwargs)[source]

Represents an authentication mechanism for users.

__init__(admins=None, name='Base Authentication', **kwargs)[source]
Parameters:
  • admins (list) – A list of usernames to treat as site-wide administrators. Useful for initial setup.
  • name (str) – The user-facing name for this authentication method.
form()[source]

Return a flask_wtf.Form subclass to login with.

login(form)[source]

Process a validated login form.

You must return a valid response object.

static login_user(user)[source]

Signal to the authentication systems that a new user has logged in.

Handles calling flask_login.login_user() and any other related housekeeping functions for you.

Parameters:user (User) – The user that has been authenticated and is logging in.
refresh(user)[source]

Refresh a user’s information (if possible).

The AuthMethod should attmept to refresh the given user’s information as if they were logging in for the first time.

Parameters:user (User) – The user to refresh.
Returns:Wether or not the refresh attempt succeeded.
Return type:bool
safe_name[source]

Normalizes a string to be a valid Python identifier (along with a few other things).

Specifically, all letters are lower cased and non-ASCII and whitespace are replaced by underscores.

Returns:The normalized string.
Rtype str:
view()[source]

Optional method for providing secondary views.

evesrp.views.login.auth_method_login() is configured to allow both GET and POST requests, and will call this method as soon as it is known which auth method is meant to be called. The path for this view is /login/self.safe_name/, and can be generated with url_for('login.auth_method_login', auth_method=self.safe_name).

The default implementation redirects to the main login view.

class evesrp.auth.models.Entity(name, authmethod, **kwargs)[source]

Private class for shared functionality between User and Group.

This class defines a number of helper methods used indirectly by User and Group subclasses such as automatically defining the table name and mapper arguments.

This class should not be inherited from directly, instead either User or Group should be used.

authmethod

The name of the AuthMethod for this entity.

entity_permissions

Permissions associated specifically with this entity.

has_permission(permissions, division_or_request=None)[source]

Returns if this entity has been granted a permission in a division.

If division_or_request is None, this method checks if this group has the given permission in any division.

Parameters:
  • permissions (iterable) – The series of permissions to check
  • division_or_request – The division to check. May also be None or an SRP request.
Return type:

bool

name

The name of the entity. Usually a nickname.

class evesrp.auth.models.User(name, authmethod, **kwargs)[source]

Bases: evesrp.auth.models.Entity

User base class.

Represents users who can submit, review and/or pay out requests. It also supplies a number of convenience methods for subclasses.

actions

Actions this user has performed on requests.

admin

If the user is an administrator. This allows the user to create and administer divisions.

get_id()[source]

Part of the interface for Flask-Login.

groups

Groups this user is a member of

is_active[source]

Part of the interface for Flask-Login.

is_anonymous[source]

Part of the interface for Flask-Login.

is_authenticated[source]

Part of the interface for Flask-Login.

pilots

Pilots associated with this user.

requests

Requests this user has submitted.

submit_divisions()[source]

Get a list of the divisions this user is able to submit requests to.

Returns:A list of tuples. The tuples are in the form (division.id, division.name)
Return type:list
class evesrp.auth.models.Pilot(user, name, id_)[source]

Represents an in-game character.

__init__(user, name, id_)[source]

Create a new Pilot instance.

Parameters:
  • user (User) – The user this character belpongs to.
  • name (str) – The name of this character.
  • id (int) – The CCP-given characterID number.
name

The name of the character

requests

The Requests filed with lossmails from this character.

user

The User this character belongs to.

class evesrp.auth.models.APIKey(user)[source]

Represents an API key for use with the External API.

hex_key[source]

The key data in a modified base-64 format safe for use in URLs.

key

The raw key data.

user

The User this key belongs to.

class evesrp.auth.models.Note(user, noter, note)[source]

A note about a particular User.

content

The actual contents of this note.

noter

The author of this note.

user

The User this note refers to.

class evesrp.auth.models.Group(name, authmethod, **kwargs)[source]

Bases: evesrp.auth.models.Entity

Base class for a group of users.

Represents a group of users. Usable for granting permissions to submit, evaluate and pay.

permissions

Synonym for entity_permissions

users

User s that belong to this group.

class evesrp.auth.models.Permission(division, permission, entity)[source]
__init__(division, permission, entity)[source]

Create a Permission object granting an entity access to a division.

division

The division this permission is granting access to

entity

The Entity being granted access

permission

The permission being granted.

class evesrp.auth.models.Division(name)[source]

A reimbursement division.

A division has (possibly non-intersecting) groups of people that can submit requests, review requests, and pay out requests.

division_permissions

All Permissions associated with this division.

name

The name of this division.

permissions[source]

The permissions objects for this division, mapped via their permission names.

requests

Request s filed under this division.

transformers

A mapping of attribute names to Transformer instances.

class evesrp.auth.models.TransformerRef(**kwargs)[source]

Stores associations between Transformers and Divisions.

attribute_name

The attribute this transformer is applied to.

division

The division the transformer is associated with

transformer

The transformer instance.