diff --git a/.gitignore b/.gitignore
index 158c4d4..b7b7c08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
*.sqlite
*.pyc
-venv/*
+.ropeproject/
diff --git a/README.md b/README.md
index 345cfed..9bae937 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,44 @@
-# How to create an OAuth 2.0 Provider
+# Example for OAuth 2 Server
-This is an example of OAuth 2.0 server in [Authlib](https://authlib.org/).
-If you are looking for old Flask-OAuthlib implementation, check the
-`flask-oauthlib` branch.
+This is an example of OAuth 2 Server.
-- Documentation:
-- Authlib Repo:
+Find more details on
-## Sponsors
+# Installation
-
-
-
-
If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at auth0.com/overview.
-
-
+ $ pip install -r requirements.txt
-## Take a quick look
+# Usage
-This is a ready to run example, let's take a quick experience at first. To
-run the example, we need to install all the dependencies:
+1. Start your provider server with:
-```bash
-$ pip install -r requirements.txt
-```
+ $ python app.py
-Set Flask and Authlib environment variables:
+2. Visit [http://127.0.0.1:5000/](http://127.0.0.1:5000/) and fill a username.
-```bash
-# disable check https (DO NOT SET THIS IN PRODUCTION)
-$ export AUTHLIB_INSECURE_TRANSPORT=1
-```
+ []()
-Create Database and run the development server:
+3. And then visit [http://127.0.0.1:5000/client](http://127.0.0.1:5000/client)
-```bash
-$ flask run
-```
+ []()
-Now, you can open your browser with `http://127.0.0.1:5000/`, login with any
-name you want.
+4. Take the client key and client secret, and modify our [client.py](client.py)
+script with the key and secret.
+Specifically update `CLIENT_ID` and `CLIENT_SECRET` variables on lines 5-6.
-Before testing, we need to create a client:
+5. Start the client server with:
-
+ $ python client.py
-### Password flow example
-Get your `client_id` and `client_secret` for testing. In this example, we
-have enabled `password` grant types, let's try:
+6. Visit [http://localhost:8000](http://localhost:8000), everything should work
+correctly. We will be redirected to a confirm page, if we choose yes, client
+will obtain a pair of access token and secret.
-```
-$ curl -u ${client_id}:${client_secret} -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=password -F username=${username} -F password=valid -F scope=profile
-```
+ []()
+ []()
-Because this is an example, every user's password is `valid`. Now you can access `/api/me`:
+7. Finally, if you visit [http://localhost:8000](http://localhost:8000)
+you can access the authorized data:
-```bash
-$ curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/api/me
-```
-
-### Authorization code flow example
-
-To test the authorization code flow, you can just open this URL in your browser.
-```bash
-$ open http://127.0.0.1:5000/oauth/authorize?response_type=code&client_id=${client_id}&scope=profile
-```
-
-After granting the authorization, you should be redirected to `${redirect_uri}/?code=${code}`
-
-Then your app can send the code to the authorization server to get an access token:
-
-```bash
-$ curl -u ${client_id}:${client_secret} -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=authorization_code -F scope=profile -F code=${code}
-```
-
-Now you can access `/api/me`:
-
-```bash
-$ curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/api/me
-```
-
-For now, you can read the source in example or follow the long boring tutorial below.
-
-**IMPORTANT**: To test implicit grant, you need to `token_endpoint_auth_method` to `none`.
-
-## Preparation
-
-Assume this example doesn't exist at all. Let's write an OAuth 2.0 server
-from scratch step by step.
-
-### Create folder structure
-
-Here is our Flask website structure:
-
-```
-app.py --- FLASK_APP
-website/
- app.py --- Flask App Factory
- __init__.py --- module initialization (empty)
- models.py --- SQLAlchemy Models
- oauth2.py --- OAuth 2.0 Provider Configuration
- routes.py --- Routes views
- templates/
-```
-
-### Installation
-
-Create a virtualenv and install all the requirements. You can also put the
-dependencies into `requirements.txt`:
-
-```
-Flask
-Flask-SQLAlchemy
-Authlib
-```
-
-### Hello World!
-
-Create a home route view to say "Hello World!". It is used to test if things
-working well.
-
-
-```python
-# website/routes.py
-from flask import Blueprint
-bp = Blueprint(__name__, 'home')
-
-@bp.route('/')
-def home():
- return 'Hello World!'
-```
-
-```python
-# website/app.py
-from flask import Flask
-from .routes import bp
-
-def create_app(config=None):
- app = Flask(__name__)
- # load app sepcified configuration
- if config is not None:
- if isinstance(config, dict):
- app.config.update(config)
- elif config.endswith('.py'):
- app.config.from_pyfile(config)
- setup_app(app)
- return app
-
-def setup_app(app):
- app.register_blueprint(bp, url_prefix='')
-```
-
-```python
-# app.py
-from website.app import create_app
-
-app = create_app({
- 'SECRET_KEY': 'secret',
-})
-```
-
-Create an empty ```__init__.py``` file in the ```website``` folder.
-
-The "Hello World!" example should run properly:
-
- $ FLASK_APP=app.py flask run
-
-## Define Models
-
-We will use SQLAlchemy and SQLite for our models. You can also use other
-databases and other ORM engines. Authlib has some built-in SQLAlchemy mixins
-which will make it easier for creating models.
-
-Let's create the models in `website/models.py`. We need four models, which are
-
-- User: you need a user to test and create your application
-- OAuth2Client: the oauth client model
-- OAuth2AuthorizationCode: for `grant_type=code` flow
-- OAuth2Token: save the `access_token` in this model.
-
-Check how to define these models in `website/models.py`.
-
-Once you've created your own `website/models.py` (or copied our version), you'll need to import the database object `db`. Add the line `from .models import db` just after `from flask import Flask` in your scratch-built version of `website/app.py`.
-
-To initialize the database upon startup, if no tables exist, you'll add a few lines to the `setup_app()` function in `website/app.py` so that it now looks like:
-
-```python
-def setup_app(app):
- # Create tables if they do not exist already
- @app.before_first_request
- def create_tables():
- db.create_all()
-
- db.init_app(app)
- app.register_blueprint(bp, url_prefix='')
-```
-
-You can try running the app again as above to make sure it works.
-
-## Implement Grants
-
-The source code is in `website/oauth2.py`. There are four standard grant types:
-
-- Authorization Code Grant
-- Implicit Grant
-- Client Credentials Grant
-- Resource Owner Password Credentials Grant
-
-And Refresh Token is implemented as a Grant in Authlib. You don't have to do
-anything on Implicit and Client Credentials grants, but there are missing
-methods to be implemented in other grants. Check out the source code in
-`website/oauth2.py`.
-
-Once you've created your own `website/oauth2.py`, import the oauth2 config object from the oauth2 module. Add the line `from .oauth2 import config_oauth` just after the import you added above in your scratch-built version of `website/app.py`.
-
-To initialize the oauth object, add `config_oauth(app)` to the `setup_app()` function, just before the line that starts with `app.register_blueprint` so it looks like:
-
-```python
-def setup_app(app):
- # Create tables if they do not exist already
- @app.before_first_request
- def create_tables():
- db.create_all()
-
- db.init_app(app)
- config_oauth(app)
- app.register_blueprint(bp, url_prefix='')
-```
-You can try running the app again as above to make sure it still works.
-
-## `@require_oauth`
-
-Authlib has provided a `ResourceProtector` for you to create the decorator
-`@require_oauth`, which can be easily implemented:
-
-```py
-from authlib.flask.oauth2 import ResourceProtector
-
-require_oauth = ResourceProtector()
-```
-
-For now, only Bearer Token is supported. Let's add bearer token validator to
-this ResourceProtector:
-
-```py
-from authlib.flask.oauth2.sqla import create_bearer_token_validator
-
-# helper function: create_bearer_token_validator
-bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
-require_oauth.register_token_validator(bearer_cls())
-```
-
-Check the full implementation in `website/oauth2.py`.
-
-
-## OAuth Routes
-
-For OAuth server itself, we only need to implement routes for authentication,
-and issuing tokens. Since we have added token revocation feature, we need a
-route for revoking too.
-
-Checkout these routes in `website/routes.py`. Their path begin with `/oauth/`.
-
-
-## Other Routes
-
-But that is not enough. In this demo, you will need to have some web pages to
-create and manage your OAuth clients. Check that `/create_client` route.
-
-And we have an API route for testing. Check the code of `/api/me`.
-
-
-## Finish
-
-Here you go. You've got an OAuth 2.0 server.
-
-Read more information on .
-
-## License
-
-Same license with [Authlib](https://authlib.org/plans).
+ []()
diff --git a/app.py b/app.py
index b9978ff..4b51b16 100644
--- a/app.py
+++ b/app.py
@@ -1,9 +1,260 @@
-from website.app import create_app
+# coding: utf-8
+from datetime import datetime, timedelta
+from flask import Flask
+from flask import session, request
+from flask import render_template, redirect, jsonify
+from flask_sqlalchemy import SQLAlchemy
+from werkzeug.security import gen_salt
+from flask_oauthlib.provider import OAuth2Provider
-app = create_app({
- 'SECRET_KEY': 'secret',
- 'OAUTH2_REFRESH_TOKEN_GENERATOR': True,
- 'SQLALCHEMY_TRACK_MODIFICATIONS': False,
+
+app = Flask(__name__, template_folder='templates')
+app.debug = True
+app.secret_key = 'secret'
+app.config.update({
'SQLALCHEMY_DATABASE_URI': 'sqlite:///db.sqlite',
})
+db = SQLAlchemy(app)
+oauth = OAuth2Provider(app)
+
+
+class User(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(40), unique=True)
+
+
+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 []
+
+
+class Grant(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_id = db.Column(
+ db.String(40), db.ForeignKey('client.client_id'),
+ nullable=False,
+ )
+ client = db.relationship('Client')
+
+ code = db.Column(db.String(255), index=True, nullable=False)
+
+ redirect_uri = db.Column(db.String(255))
+ expires = db.Column(db.DateTime)
+
+ _scopes = db.Column(db.Text)
+
+ def delete(self):
+ db.session.delete(self)
+ db.session.commit()
+ return self
+
+ @property
+ def scopes(self):
+ if self._scopes:
+ return self._scopes.split()
+ return []
+
+
+class Token(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ client_id = db.Column(
+ db.String(40), db.ForeignKey('client.client_id'),
+ nullable=False,
+ )
+ client = db.relationship('Client')
+
+ user_id = db.Column(
+ db.Integer, db.ForeignKey('user.id')
+ )
+ user = db.relationship('User')
+
+ # currently only bearer is supported
+ token_type = db.Column(db.String(40))
+
+ access_token = db.Column(db.String(255), unique=True)
+ refresh_token = db.Column(db.String(255), unique=True)
+ expires = db.Column(db.DateTime)
+ _scopes = db.Column(db.Text)
+
+ @property
+ def scopes(self):
+ if self._scopes:
+ return self._scopes.split()
+ return []
+
+
+def current_user():
+ if 'id' in session:
+ uid = session['id']
+ return User.query.get(uid)
+ return None
+
+
+@app.route('/', methods=('GET', 'POST'))
+def home():
+ if request.method == 'POST':
+ username = request.form.get('username')
+ user = User.query.filter_by(username=username).first()
+ if not user:
+ user = User(username=username)
+ db.session.add(user)
+ db.session.commit()
+ session['id'] = user.id
+ return redirect('/')
+ user = current_user()
+ return render_template('home.html', user=user)
+
+
+@app.route('/client')
+def client():
+ user = current_user()
+ if not user:
+ return redirect('/')
+ item = Client(
+ client_id=gen_salt(40),
+ client_secret=gen_salt(50),
+ _redirect_uris=' '.join([
+ 'http://localhost:8000/authorized',
+ 'http://127.0.0.1:8000/authorized',
+ 'http://127.0.1:8000/authorized',
+ 'http://127.1:8000/authorized',
+ ]),
+ _default_scopes='email',
+ user_id=user.id,
+ )
+ db.session.add(item)
+ db.session.commit()
+ return jsonify(
+ client_id=item.client_id,
+ client_secret=item.client_secret,
+ )
+
+
+@oauth.clientgetter
+def load_client(client_id):
+ return Client.query.filter_by(client_id=client_id).first()
+
+
+@oauth.grantgetter
+def load_grant(client_id, code):
+ return Grant.query.filter_by(client_id=client_id, code=code).first()
+
+
+@oauth.grantsetter
+def save_grant(client_id, code, request, *args, **kwargs):
+ # decide the expires time yourself
+ expires = datetime.utcnow() + timedelta(seconds=100)
+ grant = Grant(
+ client_id=client_id,
+ code=code['code'],
+ redirect_uri=request.redirect_uri,
+ _scopes=' '.join(request.scopes),
+ user=current_user(),
+ expires=expires
+ )
+ db.session.add(grant)
+ db.session.commit()
+ return grant
+
+
+@oauth.tokengetter
+def load_token(access_token=None, refresh_token=None):
+ if access_token:
+ return Token.query.filter_by(access_token=access_token).first()
+ elif refresh_token:
+ return Token.query.filter_by(refresh_token=refresh_token).first()
+
+
+@oauth.tokensetter
+def save_token(token, request, *args, **kwargs):
+ toks = Token.query.filter_by(
+ client_id=request.client.client_id,
+ user_id=request.user.id
+ )
+ # make sure that every client has only one token connected to a user
+ for t in toks:
+ db.session.delete(t)
+
+ expires_in = token.pop('expires_in')
+ expires = datetime.utcnow() + timedelta(seconds=expires_in)
+
+ tok = Token(
+ access_token=token['access_token'],
+ refresh_token=token['refresh_token'],
+ token_type=token['token_type'],
+ _scopes=token['scope'],
+ expires=expires,
+ client_id=request.client.client_id,
+ user_id=request.user.id,
+ )
+ db.session.add(tok)
+ db.session.commit()
+ return tok
+
+
+@app.route('/oauth/token', methods=['GET', 'POST'])
+@oauth.token_handler
+def access_token():
+ return None
+
+
+@app.route('/oauth/authorize', methods=['GET', 'POST'])
+@oauth.authorize_handler
+def authorize(*args, **kwargs):
+ user = current_user()
+ if not user:
+ return redirect('/')
+ if request.method == 'GET':
+ client_id = kwargs.get('client_id')
+ client = Client.query.filter_by(client_id=client_id).first()
+ kwargs['client'] = client
+ kwargs['user'] = user
+ return render_template('authorize.html', **kwargs)
+
+ confirm = request.form.get('confirm', 'no')
+ return confirm == 'yes'
+
+
+@app.route('/api/me')
+@oauth.require_oauth()
+def me():
+ user = request.oauth.user
+ return jsonify(username=user.username)
+
+
+if __name__ == '__main__':
+ db.create_all()
+ app.run()
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..263b71b
--- /dev/null
+++ b/client.py
@@ -0,0 +1,59 @@
+from flask import Flask, url_for, session, request, jsonify
+from flask_oauthlib.client import OAuth
+
+
+CLIENT_ID = 'GbRmKgbSMmlE2NlugMeFfQIba8hoVyBFsWS8Igsq'
+CLIENT_SECRET = 'BfP7jsN8dSsXjGLfTTPiEvarMJOpkZQ2Y7IVVee8X929LfolMV'
+
+
+app = Flask(__name__)
+app.debug = True
+app.secret_key = 'secret'
+oauth = OAuth(app)
+
+remote = oauth.remote_app(
+ 'remote',
+ consumer_key=CLIENT_ID,
+ consumer_secret=CLIENT_SECRET,
+ request_token_params={'scope': 'email'},
+ base_url='http://127.0.0.1:5000/api/',
+ request_token_url=None,
+ access_token_url='http://127.0.0.1:5000/oauth/token',
+ authorize_url='http://127.0.0.1:5000/oauth/authorize'
+)
+
+
+@app.route('/')
+def index():
+ if 'remote_oauth' in session:
+ resp = remote.get('me')
+ return jsonify(resp.data)
+ next_url = request.args.get('next') or request.referrer or None
+ return remote.authorize(
+ callback=url_for('authorized', next=next_url, _external=True)
+ )
+
+
+@app.route('/authorized')
+def authorized():
+ resp = remote.authorized_response()
+ if resp is None:
+ return 'Access denied: reason=%s error=%s' % (
+ request.args['error_reason'],
+ request.args['error_description']
+ )
+ print(resp)
+ session['remote_oauth'] = (resp['access_token'], '')
+ return jsonify(oauth_token=resp['access_token'])
+
+
+@remote.tokengetter
+def get_oauth_token():
+ return session.get('remote_oauth')
+
+
+if __name__ == '__main__':
+ import os
+ os.environ['DEBUG'] = 'true'
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true'
+ app.run(host='localhost', port=8000)
diff --git a/img/00_oauth_server_form.png b/img/00_oauth_server_form.png
new file mode 100644
index 0000000..c0c0c38
Binary files /dev/null and b/img/00_oauth_server_form.png differ
diff --git a/img/01_oauth_server.png b/img/01_oauth_server.png
new file mode 100644
index 0000000..2c01f50
Binary files /dev/null and b/img/01_oauth_server.png differ
diff --git a/img/02_oauth_client.png b/img/02_oauth_client.png
new file mode 100644
index 0000000..0eaa439
Binary files /dev/null and b/img/02_oauth_client.png differ
diff --git a/img/03_oauth_client_authorized.png b/img/03_oauth_client_authorized.png
new file mode 100644
index 0000000..19c9611
Binary files /dev/null and b/img/03_oauth_client_authorized.png differ
diff --git a/img/04_oauth_client_retrieves_data.png b/img/04_oauth_client_retrieves_data.png
new file mode 100644
index 0000000..e0a98d1
Binary files /dev/null and b/img/04_oauth_client_retrieves_data.png differ
diff --git a/requirements.txt b/requirements.txt
index a9a0f2d..6dca7dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
-Flask
-Flask-SQLAlchemy
-Authlib
+Flask==0.10.1
+Flask-SQLAlchemy==1.0
+werkzeug==0.9.4
+Flask-OAuthlib>=0.5.0
diff --git a/templates/authorize.html b/templates/authorize.html
new file mode 100644
index 0000000..b9d8f0a
--- /dev/null
+++ b/templates/authorize.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Authorization
+
+
+
Client: {{ client.client_id }}
+
User: {{ user.username }}
+
+
+
diff --git a/templates/home.html b/templates/home.html
new file mode 100644
index 0000000..d2cc058
--- /dev/null
+++ b/templates/home.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ {% if user %}
+
You are {{ user.username }}
+ {% else %}
+
You are not authenticated
+ {% endif %}
+
+
Type any username:
+
+
+
diff --git a/website/__init__.py b/website/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/website/app.py b/website/app.py
deleted file mode 100644
index af99dba..0000000
--- a/website/app.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import os
-from flask import Flask
-from .models import db
-from .oauth2 import config_oauth
-from .routes import bp
-
-
-def create_app(config=None):
- app = Flask(__name__)
-
- # load default configuration
- app.config.from_object('website.settings')
-
- # load environment configuration
- if 'WEBSITE_CONF' in os.environ:
- app.config.from_envvar('WEBSITE_CONF')
-
- # load app specified configuration
- if config is not None:
- if isinstance(config, dict):
- app.config.update(config)
- elif config.endswith('.py'):
- app.config.from_pyfile(config)
-
- setup_app(app)
- return app
-
-
-def setup_app(app):
-
- db.init_app(app)
- # Create tables if they do not exist already
- with app.app_context():
- db.create_all()
- config_oauth(app)
- app.register_blueprint(bp, url_prefix='')
diff --git a/website/models.py b/website/models.py
deleted file mode 100644
index b79068a..0000000
--- a/website/models.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import time
-from flask_sqlalchemy import SQLAlchemy
-from authlib.integrations.sqla_oauth2 import (
- OAuth2ClientMixin,
- OAuth2AuthorizationCodeMixin,
- OAuth2TokenMixin,
-)
-
-db = SQLAlchemy()
-
-
-class User(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- username = db.Column(db.String(40), unique=True)
-
- def __str__(self):
- return self.username
-
- def get_user_id(self):
- return self.id
-
- def check_password(self, password):
- return password == 'valid'
-
-
-class OAuth2Client(db.Model, OAuth2ClientMixin):
- __tablename__ = 'oauth2_client'
-
- id = db.Column(db.Integer, primary_key=True)
- user_id = db.Column(
- db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
- user = db.relationship('User')
-
-
-class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
- __tablename__ = 'oauth2_code'
-
- id = db.Column(db.Integer, primary_key=True)
- user_id = db.Column(
- db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
- user = db.relationship('User')
-
-
-class OAuth2Token(db.Model, OAuth2TokenMixin):
- __tablename__ = 'oauth2_token'
-
- id = db.Column(db.Integer, primary_key=True)
- user_id = db.Column(
- db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
- user = db.relationship('User')
-
- def is_refresh_token_active(self):
- if self.revoked:
- return False
- expires_at = self.issued_at + self.expires_in * 2
- return expires_at >= time.time()
diff --git a/website/oauth2.py b/website/oauth2.py
deleted file mode 100644
index 2f7d71f..0000000
--- a/website/oauth2.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from authlib.integrations.flask_oauth2 import (
- AuthorizationServer,
- ResourceProtector,
-)
-from authlib.integrations.sqla_oauth2 import (
- create_query_client_func,
- create_save_token_func,
- create_revocation_endpoint,
- create_bearer_token_validator,
-)
-from authlib.oauth2.rfc6749 import grants
-from authlib.oauth2.rfc7636 import CodeChallenge
-from .models import db, User
-from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
-
-
-class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
- TOKEN_ENDPOINT_AUTH_METHODS = [
- 'client_secret_basic',
- 'client_secret_post',
- 'none',
- ]
-
- def save_authorization_code(self, code, request):
- code_challenge = request.data.get('code_challenge')
- code_challenge_method = request.data.get('code_challenge_method')
- auth_code = OAuth2AuthorizationCode(
- code=code,
- client_id=request.client.client_id,
- redirect_uri=request.redirect_uri,
- scope=request.scope,
- user_id=request.user.id,
- code_challenge=code_challenge,
- code_challenge_method=code_challenge_method,
- )
- db.session.add(auth_code)
- db.session.commit()
- return auth_code
-
- def query_authorization_code(self, code, client):
- auth_code = OAuth2AuthorizationCode.query.filter_by(
- code=code, client_id=client.client_id).first()
- if auth_code and not auth_code.is_expired():
- return auth_code
-
- def delete_authorization_code(self, authorization_code):
- db.session.delete(authorization_code)
- db.session.commit()
-
- def authenticate_user(self, authorization_code):
- return User.query.get(authorization_code.user_id)
-
-
-class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
- def authenticate_user(self, username, password):
- user = User.query.filter_by(username=username).first()
- if user is not None and user.check_password(password):
- return user
-
-
-class RefreshTokenGrant(grants.RefreshTokenGrant):
- def authenticate_refresh_token(self, refresh_token):
- token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first()
- if token and token.is_refresh_token_active():
- return token
-
- def authenticate_user(self, credential):
- return User.query.get(credential.user_id)
-
- def revoke_old_credential(self, credential):
- credential.revoked = True
- db.session.add(credential)
- db.session.commit()
-
-
-query_client = create_query_client_func(db.session, OAuth2Client)
-save_token = create_save_token_func(db.session, OAuth2Token)
-authorization = AuthorizationServer(
- query_client=query_client,
- save_token=save_token,
-)
-require_oauth = ResourceProtector()
-
-
-def config_oauth(app):
- authorization.init_app(app)
-
- # support all grants
- authorization.register_grant(grants.ImplicitGrant)
- authorization.register_grant(grants.ClientCredentialsGrant)
- authorization.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)])
- authorization.register_grant(PasswordGrant)
- authorization.register_grant(RefreshTokenGrant)
-
- # support revocation
- revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
- authorization.register_endpoint(revocation_cls)
-
- # protect resource
- bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
- require_oauth.register_token_validator(bearer_cls())
diff --git a/website/routes.py b/website/routes.py
deleted file mode 100644
index da00c59..0000000
--- a/website/routes.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import time
-from flask import Blueprint, request, session, url_for
-from flask import render_template, redirect, jsonify
-from werkzeug.security import gen_salt
-from authlib.integrations.flask_oauth2 import current_token
-from authlib.oauth2 import OAuth2Error
-from .models import db, User, OAuth2Client
-from .oauth2 import authorization, require_oauth
-
-
-bp = Blueprint('home', __name__)
-
-
-def current_user():
- if 'id' in session:
- uid = session['id']
- return User.query.get(uid)
- return None
-
-
-def split_by_crlf(s):
- return [v for v in s.splitlines() if v]
-
-
-@bp.route('/', methods=('GET', 'POST'))
-def home():
- if request.method == 'POST':
- username = request.form.get('username')
- user = User.query.filter_by(username=username).first()
- if not user:
- user = User(username=username)
- db.session.add(user)
- db.session.commit()
- session['id'] = user.id
- # if user is not just to log in, but need to head back to the auth page, then go for it
- next_page = request.args.get('next')
- if next_page:
- return redirect(next_page)
- return redirect('/')
- user = current_user()
- if user:
- clients = OAuth2Client.query.filter_by(user_id=user.id).all()
- else:
- clients = []
-
- return render_template('home.html', user=user, clients=clients)
-
-
-@bp.route('/logout')
-def logout():
- del session['id']
- return redirect('/')
-
-
-@bp.route('/create_client', methods=('GET', 'POST'))
-def create_client():
- user = current_user()
- if not user:
- return redirect('/')
- if request.method == 'GET':
- return render_template('create_client.html')
-
- client_id = gen_salt(24)
- client_id_issued_at = int(time.time())
- client = OAuth2Client(
- client_id=client_id,
- client_id_issued_at=client_id_issued_at,
- user_id=user.id,
- )
-
- form = request.form
- client_metadata = {
- "client_name": form["client_name"],
- "client_uri": form["client_uri"],
- "grant_types": split_by_crlf(form["grant_type"]),
- "redirect_uris": split_by_crlf(form["redirect_uri"]),
- "response_types": split_by_crlf(form["response_type"]),
- "scope": form["scope"],
- "token_endpoint_auth_method": form["token_endpoint_auth_method"]
- }
- client.set_client_metadata(client_metadata)
-
- if form['token_endpoint_auth_method'] == 'none':
- client.client_secret = ''
- else:
- client.client_secret = gen_salt(48)
-
- db.session.add(client)
- db.session.commit()
- return redirect('/')
-
-
-@bp.route('/oauth/authorize', methods=['GET', 'POST'])
-def authorize():
- user = current_user()
- # if user log status is not true (Auth server), then to log it in
- if not user:
- return redirect(url_for('home.home', next=request.url))
- if request.method == 'GET':
- try:
- grant = authorization.get_consent_grant(end_user=user)
- except OAuth2Error as error:
- return error.error
- return render_template('authorize.html', user=user, grant=grant)
- if not user and 'username' in request.form:
- username = request.form.get('username')
- user = User.query.filter_by(username=username).first()
- if request.form['confirm']:
- grant_user = user
- else:
- grant_user = None
- return authorization.create_authorization_response(grant_user=grant_user)
-
-
-@bp.route('/oauth/token', methods=['POST'])
-def issue_token():
- return authorization.create_token_response()
-
-
-@bp.route('/oauth/revoke', methods=['POST'])
-def revoke_token():
- return authorization.create_endpoint_response('revocation')
-
-
-@bp.route('/api/me')
-@require_oauth('profile')
-def api_me():
- user = current_token.user
- return jsonify(id=user.id, username=user.username)
diff --git a/website/settings.py b/website/settings.py
deleted file mode 100644
index e69de29..0000000
diff --git a/website/templates/authorize.html b/website/templates/authorize.html
deleted file mode 100644
index 10868ca..0000000
--- a/website/templates/authorize.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
The application {{grant.client.client_name}} is requesting:
-{{ grant.request.scope }}
-