Getting started with oauthist

Installation

oauthist uses Redis as a storage.

$ sudo apt-get install redis-server

The client library is installed automatically as a dependency for current package

$ pip install oauthist

Configuration

Use configure() function to set up the framework.

Function accepts following parameters:

  • host, port, db, etc (optional): set of parameters to configure connection to Redis server
  • scopes (optional): list of strings, defining allowed access token scopes. If unset, any scopes accepted.
  • authorization_code_timeout (optional): expiration timeout of authorization code in seconds (by default, 3600).
  • access_token_timeout (optional): expiration timeout of access token (by default None which means that token never expires unless explicitly revoked)
  • prefix (recommended): the string prefix to use to store and search for keys in Redis database

Sample initialization:

>>> import oauthist
>>> oauthist.configure(redis.Redis(), prefix='oauthist')

Code examples in manual suppose, that you have already the framework, initialized as described.

Creating clients

According to the specification, before starting to use API calls, every client should be registered with the client registration. From the point of view of OAuth server, the only two arguments here are required:

  • client_type: the type of client, according to OAuth sepcification (can be either “web”, “user-agent” or “native”)
  • redirect_urls: list of redirection URL (strings).

Note

to prevent the “Open Redirectors” type of attacks, all possible redirection URLs must be explicitly set up on the configuration step.

A new random client-id will be generated, and for client of “web” type a shared secret will also be issued

>>> extra_kwargs = dict(user_id=1234, name='My Client', description='Hello world')
>>> client = oauthist.Client(client_type='web',
                             redirect_urls=['http://example.com/oauth2cb'],
                             **extra_kwargs)
>>> client.id
'ORG8hSAuTEb762AO'
>>> client.secret
'cx2kQAPjtCKlUGvwQPYA6yZ22OXDTrV5SbrGafqzHzGXQcDIgsI5uyCaLI8emjGn'
>>> client.name
'My Client'

All arguments, passed as extra kwargs, must be strings or objects unambiguously converted to strings, will be stored in the Redis, and can be accessed as object attributes.

The good part of it is that you may filter clients by any of the extra attributes, so that they can be considered also as tags:

>>> oauthist.Client(user_id=1234, ...).save()
>>> oauthist.Client(user_id=1234, ...).save()
>>> oauthist.Client.objects.find(user_id=1234)  # return two clients

Issuing authorization codes and access tokens

Issuing authorization code

Web clients having the secret shared with the authorization server should use two step authorization procedure.

  1. With the help of user, they get the authorization code. This code is available both to user and to client, but it is completely useless for anybody who doesn’t dispose the client secret.
  2. They exchange the secret code to access token

Issuing a secret code basically means “creating correct redirect URI” with the code in its GET argument.

To start working you should create the CodeRequest object first.

>>> req = oauthist.CodeRequest(client_id='...',
...                             redirect_uri='http://...',
...                             scope='...',
...                             state='...')

If you use Werkzeug or the framework based on it, you may call

>>> req = oauthist.CodeRequest.from_werkzeug(request)

The procedure of issuing of the secret code works passes following steps.

  1. Server ensures, that request hasn’t been forged in any way. If the request is just broken, server stops normal processing of the request, and returns the page, containing the error message:

    If the request fails due to a missing, invalid, or mismatching redirection URI, or if the client identifier is missing or invalid, the authorization server SHOULD inform the resource owner of the error and MUST NOT automatically redirect the user-agent to the invalid redirection URI.

    Here we refers to this state of request as “broken” and have corresponding method of code request: code_request.is_broken().

  2. Server performs some more formal checks. Currently the only check which framework can do without any help from the application programmer is to check for validity of received set of scopes. If scopes are invalid, it is safe at this point to return redirect. We call this state of request “invalid” and have a method code_request.is_invalid() for this sort of checking.

  3. If everything looks good from the point of view of the framework, the application programmer should create the code and ask user for permission to authorize it. It is important to not that although the code is saved, it doesn’t mean at all that the code can be exchanged for access token. Usually you store the code request along with user id attached:

    code = code_request.save_code(user_id=USER_ID)
    
  4. Then server displays the confirmation window to the user, and depending on user’s answer the state of the code can change itself to “accepted” or to “declined”. When code is accepted, a new property “accepted” is defined and set to True, when it is declined, the code is simply removed from the database.

    Usually handling of user input is provided in separate HTTP request. To mark request as accepted, you invoke code.accept(), otherwise you run code.decline()

That’s how the request handling can be performed in to views of Flask framework:

@app.route('/auth_endpt')
def auth_endpt():
    code_req = CodeRequest.from_werkzeug(request)
    if code_req.is_broken():
        return render_template('server/auth_endpt_broken.html',
                               error=code_req.error)
    if code_req.is_invalid():
        return redirect(code_req.get_redirect())

    # at this point we save the code request and wait for user confirmation
    code = code_req.save(user_id=USER_ID)
    return render_template('server/auth_endpt_confirmation.html', code=code,
                            client=code_req.client)


@app.route('/auth_endpt/confirmation')
def auth_endpt_confirmation():
    # get some information from the form
    code_id = ...  # it can be passed between handlers in a hidden for field
                       # or saved in session
    # search for code request
    code = Code.objects.get(code_id)
    if user_declined_access():
        return redirect(code.decline())
    else:
        return redirect(code.accept())

See 4.1.2.1 Error Response section of RFC for more variants of error argument of decline().

Exchanging authorization code for access token

Once Client receives the authorization code, it immediately exchanges it for access token.

Note

By default authorization code has limited lifetime, only one hour, whereas the access token by default is “eternal”, yet revocable.

Create the code exchange request

req = oauthist.CodeExchangeRequest(client_id='...',
                                   client_secret='...',
                                   redirect_uri='...',
                                   code='...')

Similarly, if you’re a lucky Werkzeug user, you can create the object by its Request.

req = oauthist.CodeExchangeRequest.from_werkzeug(request)

Verify, is request is valid.

if req.is_invalid():
    return req.get_error().to_werkzeug_response()

Then do exchange it for access token

access_token = req.exchange_for_token()

According to the section Issuing an Access Token of the RFC 6749, response must be encoded in JSON, and headers must contain correct Cache-Control and Pragma headers:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"example",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  "example_parameter":"example_value"
}

You can use corresponding methods of response object to convert it to HTTP response of Werkzeug, or to get dict of headers and the body contents to use them to build the response of your framework.

>>> resp.to_werkzeug_response()
...
>>> resp.get_code()
200
>>> resp.get_headers()
{'Content-Type': 'application/json;charset=UTF-8', ...}
>>> resp.get_body()
'{...}'

Refreshing access token

oauthist framework intentionally doesn’t issue refresh tokens, as we strive to keep things as simple as possible for us and for clients, and the security benefits introducing with access codes seem questionable.

Implicit grant

The implicit grant type is used to obtain access tokens with Client who can’t keep secrets (mostly client-side JavaScript applications). Instead of issuing the code which then can be exchanged to access token, the server returns the access token in the very first response.

The flow is similar to one used to obtain the code.

Create the AccessTokenRequest object

>>> req = oauthist.AccessTokenRequest(client_id='...',
...                             redirect_uri='http://...',
...                             scope='...',
...                             state='...')

If you use Werkzeug or the framework based on it, you may call

>>> req = oauthist.AccessTokenRequest.from_werkzeug(request)

Check if the request is valid

>>> if not req.is_valid():
...     return render('error.html', error=req.error)

Do additional checks, get positive (or negative) answer from the User, and return the HTTP response redirect with the access token.

>>> redirect_uri = req.get_redirect(user_id=user_id)
>>> str(redirect_uri)
'http://...#code=<...>&state=<...>'

If you need to return error

>>> redirect_uri = req.get_redirect(error='access_denied')
>>> str(redirect_uri)
'http://...#error=access_denied'

See 4.2.2.1 Error Response section of RFC for more variants of error argument.

Resource Owner Password Credentials Grant

The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application.

Technically, this kind of grant is nothing more but a “shortcut” of the server side flow, but here Client doesn’t ask User to provide the code, and just sends “bare” user’s credentials (username and password) to Server instead.

To check whether username and password, provided by the client, are valid, you should write a function.

  • The function accepts two arguments: username and password
  • Function must return None, if user not found, or user’s password doesn’t match the one provided by the client.
  • Function must return dict (the dict may be empty), to denote that user is valid. The contents of the dict will be written to the access token, returned by the client.
  • Function must be passed as an argument to PasswordExchangeRequest constructor

Trivial hypothetical example

def verify_requisites(username, password):
    user = User.objects.get(username=username, password=password)
    if user is None:
        return None
    else:
        return {'user_id': user.id}

Create the code exchange request

req = oauthist.PasswordExchangeRequest(username='...',
                                       password='...',
                                       client_id='...',
                                       client_secret='...',
                                       scope='...',
                                       verify_requisites=verify_requisites)

Parameters client_id and client_secret are required for private client; as client_secret doesn’t exist for public client, there is nothing to pass here; and finally, client_id for public client is optional.

Additionally, there are two optional boolean flags, altering the behavior of the request object: client_required – forbid “anonymous” clients, and client_secret_required – forbid public (in terms of OAuth specification) clients.

Verification of the request and issuing of the code is provided the same way as for CodeExchangeRequest. Below is a Flask-based example

req = PasswordExchangeRequest.from_werkzeug(request, verify_requisites)
if req.is_invalid():
    return req.get_error().to_werkzeug_response()
else:
    access_token = req.exchange_for_token()
    return access_token.to_werkzeug_response()

Verifying requests with access tokens

The way access tokens should be used is defined in the Access Tokens Types chapter of RFC 6749.

The framework supports Bearer Tokens (see RFC 6750). According to the recommendation, the token can be passed in Authorization: HTTP header, in POST or GET request as a parameter.

If you use Werkezeug-based framework, you may easily retrieve the token string from any of these source

request = oauthist.ProtectedResourceRequest(http_request)

The function returns object with field access_token, which will be set to None if token hasn’t been found.

Once you received the bearer token string, you must check if it is valid to your scope. If the token is valid, the AccessToken object will be returned, otherwise InvalidAccessToken exception will be raised.

>>> token = oauthist.ProtectedResourceRequest(access_token_string).verify_access_token('scopeA', 'scopeA-B')

Note

The successful response will be returned, if token is valid either for ‘scopeA’ or ‘scopeA-B’. If for some reasons you want to ensure that token is valid for both scopes, use cycle

try:
    for scope in scopes:
       token = req.verify_access_token(scope)
except oauthist.InvalidAccessToken as e:
    # handle exception here
    pass

How to identify the user then?

As you may remember, when you save a Code (Authorization code) instance with CodeRequest.save_code(...)(), you may pass there as much extra attributes, as you want, including user_id, for example.

Then, when you call CodeExchangeRequest.exchange_for_token(), all these attributes are copied to the AccessToken instance. Therefore, token you receive will contain all required extra fields, which you can use to show user’s data.

Revoking grant

Revoke all grants from a client:

>>> oauthist.revoke_grant(user_id, client_id)

Revoke particular access token

>>> oauthist.revoke_access_token(token_string)