Flask Integration#
Protects users resources, so that only the authorized clients with the authorized access token can access the given scope resources.
A resource server can be a different server other than the authorization server. Authlib offers a decorator to protect your API endpoints:
from flask import jsonify
from authlib.integrations.flask_oauth2 import ResourceProtector, current_token
from authlib.oauth2.rfc6750 import BearerTokenValidator
class MyBearerTokenValidator(BearerTokenValidator):
def authenticate_token(self, token_string):
return Token.query.filter_by(access_token=token_string).first()
require_oauth = ResourceProtector()
# only bearer token is supported currently
require_oauth.register_token_validator(MyBearerTokenValidator())
The access token is read from the Authorization header. To also accept it
in a form-encoded request body, see Token in the Request Body.
When the resource server has no access to the Token model (database), and
there is an introspection token endpoint in authorization server, you can
Use Introspection in Resource Server.
Here is the way to protect your users’ resources:
@app.route('/user')
@require_oauth('profile')
def user_profile():
# if Token model has `.user` foreign key
user = current_token.user
return jsonify(id=user.id, username=user.username)
If the resource is not protected by a scope, omit the argument:
@app.route('/user')
@require_oauth
def user_profile():
user = current_token.user
return jsonify(id=user.id, username=user.username)
The current_token is a proxy to the Token model you have defined above.
Since there is a user relationship on the Token model, we can access this
user with current_token.user.
If the decorator is not your favorite, there is a with statement for you:
@app.route('/user')
def user_profile():
with require_oauth.acquire('profile') as token:
user = token.user
return jsonify(id=user.id, username=user.username)
JWT access tokens#
When the authorization server issues JWT access tokens (RFC 9068), the
resource server can validate them locally, without a database or an
introspection call. Subclass JWTBearerTokenValidator
and tell it where to find the issuer’s public keys:
from authlib.oauth2.rfc9068 import JWTBearerTokenValidator
class MyJWTValidator(JWTBearerTokenValidator):
def get_jwks(self):
return app.config['AUTHORIZATION_SERVER_JWKS']
require_oauth = ResourceProtector()
require_oauth.register_token_validator(
MyJWTValidator(
issuer='https://auth.example.org',
resource_server='https://api.example.org',
)
)
The validator checks the signature, the iss claim against issuer and
the aud claim against resource_server, so tokens issued for another API
are rejected.
Accepting tokens from several issuers#
A validator trusts one issuer. To accept tokens from an approved list of
authorization servers, keep one validator per issuer and pick the right one
from the iss claim. The iss value is read before the signature is
checked and is only used to select a validator: the token is still verified
with that issuer’s keys, so a token that claims a trusted issuer but is signed
with another key is rejected:
import json
from joserfc import jws
from joserfc.errors import JoseError
from authlib.oauth2.rfc6750 import InvalidTokenError
from authlib.oauth2.rfc9068 import JWTBearerTokenValidator
class IssuerValidator(JWTBearerTokenValidator):
def __init__(self, issuer, jwks, resource_server):
super().__init__(issuer, resource_server)
self.jwks = jwks
def get_jwks(self):
return self.jwks
class MultiIssuerValidator(JWTBearerTokenValidator):
def __init__(self, resource_server, trusted_issuers):
# trusted_issuers maps an issuer identifier to its JWKS
super().__init__(None, resource_server)
self.validators = {
issuer: IssuerValidator(issuer, jwks, resource_server)
for issuer, jwks in trusted_issuers.items()
}
def authenticate_token(self, token_string):
try:
payload = jws.extract_compact(token_string.encode()).payload
validator = self.validators[json.loads(payload)['iss']]
except (JoseError, ValueError, KeyError, TypeError):
raise InvalidTokenError(
realm=self.realm, extra_attributes=self.extra_attributes
)
return validator.authenticate_token(token_string)
require_oauth.register_token_validator(
MultiIssuerValidator(
'https://api.example.org',
{
'https://auth-a.example.org': jwks_a,
'https://auth-b.example.org': jwks_b,
},
)
)
Because each issuer has its own validator instance, nothing is shared between
requests. Fetch and cache each issuer’s JWKS as described in
JWTBearerTokenValidator.
Multiple Scopes#
Changed in version v1.0.
You can apply multiple scopes to one endpoint in AND, OR and mix modes. Here are some examples:
@app.route('/profile')
@require_oauth(['profile email'])
def user_profile():
user = current_token.user
return jsonify(id=user.id, username=user.username)
It requires the token containing both profile and email scope.
@app.route('/profile')
@require_oauth(['profile', 'email'])
def user_profile():
user = current_token.user
return jsonify(id=user.id, username=user.username)
It requires the token containing either profile or email scope.
It is also possible to mix AND and OR logic. e.g.:
@app.route('/profile')
@require_oauth(['profile email', 'user'])
def user_profile():
user = current_token.user
return jsonify(id=user.id, username=user.username)
This means if the token will be valid if:
token contains both
profileandemailscopeor token contains
userscope
Optional require_oauth#
There is one more parameter for require_oauth which is used to serve
public endpoints:
@app.route('/timeline')
@require_oauth(optional=True)
def timeline_api():
if current_token:
return get_user_timeline(current_token.user)
return get_public_timeline()
MethodView & Flask-Restful#
You can also use the require_oauth decorator in flask.views.MethodView
and flask_restful.Resource:
from flask.views import MethodView
class UserAPI(MethodView):
decorators = [require_oauth('profile')]
from flask_restful import Resource
class UserAPI(Resource):
method_decorators = [require_oauth('profile')]