OAuth1 Server¶
Note
You SHOULD read Flask OAuth 1.0 Provider documentation.
This part of documentation covers the tutorial of setting up an OAuth1 provider. An OAuth1 server concerns how to grant the authorization and how to protect the resource. Register an OAuth provider:
from flask_oauthlib.provider import OAuth1Provider
app = Flask(__name__)
oauth = OAuth1Provider(app)
Like any other Flask extensions, we can pass the application later:
oauth = OAuth1Provider()
def create_app():
app = Flask(__name__)
oauth.init_app(app)
return app
To implemente the oauthorization flow, we need to understand the data model.
User (Resource Owner)¶
A user, or resource owner, is usally the registered user on your site. You design your own user model, there is not much to say.
Client (Application)¶
A client is the app which want to use the resource of a user. It is suggested that the client is registered by a user on your site, but it is not required.
The client should contain at least these information:
- client_key: A random string
- client_secret: A random string
- redirect_uris: A list of redirect uris
- default_redirect_uri: One of the redirect uris
- default_realms: Default realms/scopes of the client
But it could be better, if you implemented:
- validate_realms: A function to validate realms
An example of the data model in SQLAlchemy (SQLAlchemy is not required):
class Client(db.Model):
# human readable name, not required
name = db.Column(db.String(40))
# human readable description, not required
description = db.Column(db.String(400))
# creator of the client, not required
user_id = db.Column(db.ForeignKey('user.id'))
# required if you need to support client credential
user = db.relationship('User')
client_key = db.Column(db.String(40), primary_key=True)
client_secret = db.Column(db.String(55), unique=True, index=True,
nullable=False)
_realms = db.Column(db.Text)
_redirect_uris = db.Column(db.Text)
@property
def redirect_uris(self):
if self._redirect_uris:
return self._redirect_uris.split()
return []
@property
def default_redirect_uri(self):
return self.redirect_uris[0]
@property
def default_realms(self):
if self._realms:
return self._realms.split()
return []
Request Token and Verifier¶
Request token is designed for exchanging access token. Verifier token is designed to verify the current user. It is always suggested that you combine request token and verifier together.
The request token should contain:
- client: Client associated with this token
- token: Access token
- secret: Access token secret
- realms: Realms with this access token
- redirect_uri: A URI for redirecting
The verifier should contain:
- verifier: A random string for verifier
- user: The current user
And the all in one token example:
class RequestToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')
)
user = db.relationship('User')
client_key = db.Column(
db.String(40), db.ForeignKey('client.client_key'),
nullable=False,
)
client = db.relationship('Client')
token = db.Column(db.String(255), index=True, unique=True)
secret = db.Column(db.String(255), nullable=False)
verifier = db.Column(db.String(255))
redirect_uri = db.Column(db.Text)
_realms = db.Column(db.Text)
@property
def realms(self):
if self._realms:
return self._realms.split()
return []
Since the request token and verifier is a one-time token, it would be better to put them in a cache.
Timestamp and Nonce¶
Timestamp and nonce is a token for preventing repeating requests, it can store these information:
- client_key: The client/consure key
- timestamp: The
oauth_timestamp
parameter - nonce: The
oauth_nonce
parameter - request_token: Request token string, if any
- access_token: Access token string, if any
The timelife of a timestamp and nonce is 60 senconds, put it in a cache please. Here is an example in SQLAlchemy:
class Nonce(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.Integer)
nonce = db.Column(db.String(40))
client_key = db.Column(
db.String(40), db.ForeignKey('client.client_key'),
nullable=False,
)
client = db.relationship('Client')
request_token = db.Column(db.String(50))
access_token = db.Column(db.String(50))
Access Token¶
An access token is the final token that could be use by the client. Client will send access token everytime when it need to access resource.
A access token requires at least these information:
- client: Client associated with this token
- user: User associated with this token
- token: Access token
- secret: Access token secret
- realms: Realms with this access token
The implementation in SQLAlchemy:
class AccessToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
client_key = db.Column(
db.String(40), db.ForeignKey('client.client_key'),
nullable=False,
)
client = db.relationship('Client')
user_id = db.Column(
db.Integer, db.ForeignKey('user.id'),
)
user = db.relationship('User')
token = db.Column(db.String(255))
secret = db.Column(db.String(255))
_realms = db.Column(db.Text)
@property
def realms(self):
if self._realms:
return self._realms.split()
return []
Configuration¶
The oauth provider has some built-in defaults, you can change them with Flask config:
OAUTH1_PROVIDER_ERROR_URI | The error page when there is an error,
default value is '/oauth/errors' . |
OAUTH1_PROVIDER_ERROR_ENDPOINT | You can also configure the error page uri with an endpoint name. |
OAUTH1_PROVIDER_REALMS | A list of allowed realms, default is []. |
OAUTH1_PROVIDER_KEY_LENGTH | A range allowed for key length, default value is (20, 30). |
OAUTH1_PROVIDER_ENFORCE_SSL | If the server should be enforced through SSL. Default value is True. |
OAUTH1_PROVIDER_SIGNATURE_METHODS | Allowed signature methods, default value is (SIGNATURE_HMAC, SIGNATURE_RSA). |
Warning
RSA signature is not ready at this moment, you should use HMAC.
Implements¶
The implementings of authorization flow needs three handlers, one is request token handler, one is authorize handler for user to confirm the grant, the other is token handler for client to exchange access token.
Before the implementing of authorize and request/access token handler, we need to set up some getters and setter to communicate with the database.
Client getter¶
A client getter is required. It tells which client is sending the requests, creating the getter with decorator:
@oauth.clientgetter
def load_client(client_key):
return Client.query.filter_by(client_key=client_key).first()
Request token & verifier getters and setters¶
Request token & verifier getters and setters are required. They are used in the authorization flow, implemented with decorators:
@oauth.grantgetter
def load_request_token(token):
grant = RequestToken.query.filter_by(token=token).first()
return grant
@oauth.grantsetter
def save_request_token(token, request):
if oauth.realms:
realms = ' '.join(request.realms)
else:
realms = None
grant = RequestToken(
token=token['oauth_token'],
secret=token['oauth_token_secret'],
client=request.client,
redirect_uri=request.redirect_uri,
_realms=realms,
)
db.session.add(grant)
db.session.commit()
return grant
@oauth.verifiergetter
def load_verifier(verifier, token):
return RequestToken.query.filter_by(verifier=verifier, token=token).first()
@oauth.verifiersetter
def save_verifier(token, verifier, *args, **kwargs):
tok = RequestToken.query.filter_by(token=token).first()
tok.verifier = verifier['oauth_verifier']
tok.user = get_current_user()
db.session.add(tok)
db.session.commit()
return tok
In the sample code, there is a get_current_user
method, that will return
the current user object, you should implement it yourself.
The token
for grantsetter
is a dict, that contains:
{
u'oauth_token': u'arandomstringoftoken',
u'oauth_token_secret': u'arandomstringofsecret',
u'oauth_authorized_realms': u'email address'
}
And the verifier
for verifiersetter
is a dict too, it contains:
{
u'oauth_verifier': u'Gqm3id67MdkrASOCQIAlb3XODaPlun',
u'oauth_token': u'eTYP46AJbhp8u4LE5QMjXeItRGGoAI',
u'resource_owner_key': u'eTYP46AJbhp8u4LE5QMjXeItRGGoAI'
}
Token getter and setter¶
Token getter and setters are required. They are used in the authorization flow and accessing resource flow. Implemented with decorators:
@oauth.tokengetter
def load_access_token(client_key, token, *args, **kwargs):
t = AccessToken.query.filter_by(
client_key=client_key, token=token).first()
return t
@oauth.tokensetter
def save_access_token(token, request):
tok = AccessToken(
client=request.client,
user=request.user,
token=token['oauth_token'],
secret=token['oauth_token_secret'],
_realms=token['oauth_authorized_realms'],
)
db.session.add(tok)
db.session.commit()
The setter receives token
and request
parameters. The token
is a
dict, which contains:
{
u'oauth_token_secret': u'H1xGH4X1ZkRAulHHdLfdFm7NR350tr',
u'oauth_token': u'aXNlKcjkVImnTfTKj8CgFpc1XRZr6P',
u'oauth_authorized_realms': u'email'
}
The request
is an object, it contains at least a user and client
objects for current flow.
Timestamp and Nonce getter and setter¶
Timestamp and Nonce getter and setter is required. They are used everywhere:
@oauth.noncegetter
def load_nonce(client_key, timestamp, nonce, request_token, access_token):
return Nonce.query.filter_by(
client_key=client_key, timestamp=timestamp, nonce=nonce,
request_token=request_token, access_token=access_token,
).first()
@oauth.noncesetter
def save_nonce(client_key, timestamp, nonce, request_token, access_token):
nonce = Nonce(
client_key=client_key,
timestamp=timestamp,
nonce=nonce,
request_token=request_token,
access_token=access_token,
)
db.session.add(nonce)
db.session.commit()
return nonce
Request token handler¶
Request token handler is a decorator for generating request token. You don’t need to do much:
@app.route('/oauth/request_token')
@oauth.request_token_handler
def request_token():
return {}
You can add more data on token response:
@app.route('/oauth/request_token')
@oauth.request_token_handler
def request_token():
return {'version': '0.1.0'}
Authorize handler¶
Authorize handler is a decorator for authorize endpoint. It is suggested that you implemented it this way:
@app.route('/oauth/authorize', methods=['GET', 'POST'])
@require_login
@oauth.authorize_handler
def authorize(*args, **kwargs):
if request.method == 'GET':
client_key = kwargs.get('resource_owner_key')
client = Client.query.filter_by(client_key=client_key).first()
kwargs['client'] = client
return render_template('authorize.html', **kwargs)
confirm = request.form.get('confirm', 'no')
return confirm == 'yes'
The GET request will render a page for user to confirm the grant, parameters in kwargs are:
- resource_owner_key: same as client_key
- realms: realms that this client requests
The POST request needs to return a bool value that tells whether user grantted the access or not.
Access token handler¶
Access token handler is a decorator for exchange access token. Client will request an access token with a request token. You don’t need to do much:
@app.route('/oauth/access_token')
@oauth.access_token_handler
def access_token():
return {}
Just like request token handler, you can add more data in access token.
Protect Resource¶
Protect the resource of a user with require_oauth
decorator now:
@app.route('/api/me')
@oauth.require_oauth('email')
def me():
user = request.oauth.user
return jsonify(email=user.email, username=user.username)
@app.route('/api/user/<username>')
@oauth.require_oauth('email')
def user(username):
user = User.query.filter_by(username=username).first()
return jsonify(email=user.email, username=user.username)
The decorator accepts a list of realms, only the clients with the given realms can access the defined resources.
Changed in version 0.5.0.
The request
has an additional property oauth
, it contains at least:
- client: client model object
- realms: a list of scopes
- user: user model object
- headers: headers of the request
- body: body content of the request
Example for OAuth 1¶
Here is an example of OAuth 1 server: https://github.com/lepture/example-oauth1-server
Also read this article http://lepture.com/en/2013/create-oauth-server.