Thursday, December 4, 2014

Simple OAuth implementation

An OAuth server implements one or more of the following workflows:

1) Client Credential grant
2) Implicit grant
3) Authorization code grant
4) Refresh token grant

1) is for guest access without any user context
2) is less secure but supports Password patterns
3) is the typical OAuth workflow supporting anti-password pattern
4) is used to refresh the code/token after they expire

Simplest case is issue bearer token with indefinite expiration time by method 2) for user resource access and least secure.

For more details or for accuracy, please refer the RFC

# implicit workflow
# GET /oauth/authorize/v1?client_id=CarouselTest1&response_type=token&username=foo&password=bar HTTP/1.1
# HTTP/1.1 200 OK
# <meta http-equiv="refresh"content="0;url=http://127.0.0.1#expires_in=86399945&token_type=bearer&access_token=ACCESS_TOKEN">

# authorization workflow
# auth_code request
# GET /oauth/authorize/?client_id=BCTest1&redirect_uri=http%3A%2F%2F127.0.0.1:8888/callback&scope=openid HTTP/1.1
# Content-Type : application/x-www-form-urlencoded
# HTTP/1.1 302 Moved Temporarily
# Location: http://127.0.0.1/oauth/?code=AUTH_CODE

# access_token request
# POST /oauth/token/v1 HTTP/1.1
# Content-Type: application/x-www-form-urlencoded
# grant_type=authorization_code&client_id=THE_CLIENT_ID&client_secret=THE_CLIENT_SECRET&code=AUTH_CODE
# HTTP/1.1 200 OK
{"token_type":"bearer","expires_in":86399945,"refresh_token":"REFRESH_TOKEN","access_token":"ACCESS_TOKEN"}


# refresh token request
# POST /oauth/token/v1 HTTP/1.1
# Content-Type: application/x-www-form-urlencoded
# grant_type=refresh_token&client_id=THE_CLIENT_ID&client_secret=THE_CLIENT_SECRET&refresh_token=REFRESH_TOKEN
# HTTP/1.1 200 OK
{"token_type":"bearer","expires_in":86399968,"refresh_token":"REFRESH_TOKEN","access_token":"ACCESS_TOKEN"}


# client_credential request
# POST /oauth/token/v1 HTTP/1.1
# Content-Type: application/x-www-form-urlencoded
# grant_type=access_token&client_id=THE_CLIENT_ID&client_secret=THE_CLIENT_SECRET
# HTTP/1.1 200 OK
#{"token_type":"bearer","expires_in":86399945,"refresh_token":"REFRESH_TOKEN","access_token":"ACCESS_TOKEN"}


This is implemented as:

# coding: utf-8

from datetime import datetime, timedelta
from flask import Flask
from flask import session, request
from flask import make_response
from flask import render_template, redirect, jsonify
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import gen_salt
from flask_oauthlib.provider import OAuth2Provider
import random
import string

app = Flask(__name__, template_folder='templates')
app.debug = True
app.secret_key = 'notasecret'
app.config.update({
    'SQLALCHEMY_DATABASE_URI': 'mysql://root:P@ssword@127.0.0.1/oauth',
})
db = SQLAlchemy(app)
#oauth = OAuth2Provider(app)

@app.route('/oauth/token/', methods=['POST'])
#@oauth.token_handler
def access_token():
    client_id = request.args.get('client_id', '')
    client_secret = request.args.get('client_secret', '')
    if client_id.isspace() or client_secret.isspace():  # ideally we check registered clients
       return make_response(jsonify({'error':'invalid client'}), 400)
    grant_type = request.args.get('grant_type','')
    if grant_type == 'authorization_code':
       code = request.args.get('code', '') # ideally we check code issued
       if code.isspace():
          return jsonify({'error':'invalid code'}), 400
       refresh_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
       access_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
       return jsonify({"token_type":"bearer","expires_in":3600,"refresh_token":refresh_token,"access_token":access_token}), 200
    if grant_type == 'access_token':
       # guest access token
       refresh_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
       access_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued

       return jsonify({"token_type":"bearer","expires_in":3600,"refresh_token":refresh_token,"access_token":access_token}), 200
    if grant_type == 'refresh_token':
       refresh_token = request.args.get('refresh_token', '')
       if refresh_token.isspace():
          return make_response(jsonify({'error':'invalid refresh_token'}, 400))
       refresh_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
       access_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
       return jsonify({"token_type":"bearer","expires_in":3600,"refresh_token":refresh_token,"access_token":access_token}), 200
    return make_response(jsonify({'error':'invalid grant_type'}), 400)

@app.route('/oauth/authorize/', methods=['GET'])
#@oauth.authorize_handler
def authorize(*args, **kwargs):
         response_type = request.args.get('response_type', '')
         if response_type == 'token':
            # implicit
            username = request.args.get('username', '')
            password = request.args.get('password', '')
            access_token = ''
            if not username.isspace() and not password.isspace(): # ideally we check a membership provider
               access_token = ''.join([random.choice('0123456789ABCDEF') for x in range(0, 16)]) # ideally we save tokens issued
               return make_response(jsonify({'url':'http://127.0.0.1/', 'token_type': 'bearer', 'access_token': access_token, 'expires_in': 3600}), 200)
            return make_response(jsonify({'error':'invalid credentials'}), 400)
         else:
            # auth code request
            client_id = request.args.get('client_id', '') # ideally we check registered clients
            if client_id.isspace():
                return make_response(jsonify({'error':'invalid client'}), 400)
            redirect_uri = request.args.get('redirect_uri', '') # ideally we check registered callback uri
            if redirect_uri.isspace():
                return make_response(jsonify({'error': 'invalid redirect_uri'}), 400)
            auth_code = ''.join([random.choice(string.digits + string.ascii_uppercase) for x in range(0, 12)])
            redirect_uri += "?code=" + auth_code # ideally we check for query strings
            return redirect(redirect_uri)

if __name__ == '__main__':
    app.run(port=8888)

Usually we don't send the client secret over the wire
Instead we could use something like this
# Here we simply propose the use of an OTP token to sign and make a request.
# implemented in totp.py discussed earlier as the OneTimePasswordAlgorithm ()
# for more detail, please visit : http://1drv.ms/13YBENz and code at https://github.com/ravibeta/apisecurity-1.0
otp = OneTimePasswordAlgorithm()
import hashlib
otp.generateTOTP(client_secret, epoch_time, '6', hashlib.sha256)

'857652'
StringToSign = ?timestamp=23453231&token=857652
Put the HMAC_SHA1 of the above in the authorization header

No comments:

Post a Comment