OAuth2 Server¶
An OAuth2 server concerns how to grant the authorization and how to protect the resource. Register an OAuth provider:
from flask_oauth2_devices.provider import OAuth2DevicesProvider
app = Flask(__name__)
oauth = OAuth2DevicesProvider(app)
Like any other Flask extensions, we can pass the application later:
oauth = OAuth2DevicesProvider()
def create_app():
app = Flask(__name__)
oauth.init_app(app)
return app
To implement the authorization flow, we need to understand the data model.
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 properties:
- client_id: A random string
- client_secret: A random string
- client_type: A string represents if it is confidential
- redirect_uris: A list of redirect uris
- default_redirect_uri: One of the redirect uris
- default_scopes: Default scopes of the client
But it could be better, if you implemented:
- allowed_grant_types: A list of grant types
- allowed_response_types: A list of response types
- validate_scopes: A function to validate scopes
Note
The value of the scope parameter is expressed as a list of space- delimited, case-sensitive strings.
An example of the data model in SQLAlchemy (SQLAlchemy is not required):
class Client(db.Model):
client_id = db.Column(db.String(40), primary_key=True)
client_secret = db.Column(db.String(55), nullable=False)
user_id = db.Column(db.ForeignKey('user.id'))
user = db.relationship('User')
_redirect_uris = db.Column(db.Text)
_default_scopes = db.Column(db.Text)
@property
def client_type(self):
return 'public'
@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_scopes(self):
if self._default_scopes:
return self._default_scopes.split()
return []
Configuration¶
The oauth provider has some built-in defaults, you can change them with Flask config:
OAUTH2_PROVIDER_ERROR_URI | The error page when there is an error, default value is '/oauth/errors'. |
OAUTH2_PROVIDER_ERROR_ENDPOINT | You can also configure the error page uri with an endpoint name. |
OAUTH2_PROVIDER_CODE_EXPIRES_IN | Default OAuth code expires time, default is 3600. |
Implementation¶
The implementation of authorization flow needs two handlers, one is the code handler generate the initial user_code and device_code, the other is the authorization handler for the device to request an access token once the user has authorized the device.
Before the implementing of authorize and token handler, we need to set up some getters and setters 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_id):
return Client.query.filter_by(client_id=client_id).first()
Auth code getter and setter¶
Auth code getter and setter are required. They are used in the authorization flow, implemented with decorators:
@oauth.authcodegetter
def load_auth_code(code):
return Code.query.filter_by(code=code).first()
In our example our auth code setter also creates creates new auth codes:
@oauth.authcodesetter
def save_auth_code(code, client_id, user_id, *args, **kwargs):
expires_in = (AUTH_EXPIRATION_TIME if code is None else code.pop('expires_in'))
expires = datetime.utcnow() + timedelta(seconds=expires_in)
created = datetime.utcnow()
cod = Code(
client_id=client_id,
user_id=user_id,
code = (None if code is None else code['code']),
_scopes = ('public private' if code is None else code['scope']),
expires=expires,
created=created,
is_active=0
)
if cod.code is None:
cod.code = cod.generate_new_code(cod.client_id)[:8]
db.session.add(cod)
db.session.commit()
return cod
In the sample code, there is a get_current_user method, that will return the current user object, you should implement it yourself.
Token creation¶
You are free to generate access tokens in whatever way you want. We have provided an example for creating access tokens and refresh tokens on the token object:
def create_access_token(self, client_id, user_id, scope, token_type):
expires_in = AUTH_EXPIRATION_TIME
expires = datetime.utcnow() + timedelta(seconds=expires_in)
created = datetime.utcnow()
tok = Token(
client_id=client_id,
user_id=user_id,
access_token=None,
refresh_token=None,
token_type=token_type,
_scopes = ("public private" if scope is None else ' '.join(scope)),
expires=expires,
created=created,
)
if tok.access_token is None:
tok.access_token = tok._generate_token()
db.session.add(tok)
db.session.commit()
return tok
def refresh(self, token):
tok = Token(
client_id=self.client_id,
user_id=self.user_id,
access_token=self.access_token,
refresh_token=None,
token_type=token_type,
_scopes = ("public private" if scope is None else ' '.join(scope)),
expires=expires,
created=created,
)
if tok.refresh_token is None:
tok.refresh_token = tok._generate_refresh_token()
db.session.add(tok)
db.session.commit()
return tok
The crytographic functions you use to generate the actual tokens are totally up to you, however we have some example in the example code.
Code handler¶
Code handler is a decorator for generating Auth Codes. You don’t need to do much:
@app.route('/oauth/device', methods=['POST'])
@oauth.code_handler("https://api.example.com/oauth/device/authorize", "https://example.com/activate", 600, 600)
def code():
return None
It expects the following parameters
- Authroize URL
- Activate URL
- Expires Internal
- Recommended Polling Internal
Authorize handler¶
Authorize handler is a decorator for the device to request an access token once the user has authorized the device. You don’t need to do much:
@app.route('/oauth/device/authorize', methods=['POST'])
@oauth.authorize_handler()
def authorize():
return None
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)
Example for OAuth 2 for devices¶
Here is an example of OAuth 2 server: https://github.com/greedo/flask-oauth2-devices/example