7 |
8 | ---
9 |
10 | ## Take a quick look
11 |
12 | This is a ready to run example, let's take a quick experience at first. To
13 | run the example, we need to install all the dependencies:
14 |
15 | $ pip install -r requirements.txt
16 |
17 | Set Flask and Authlib environment variables:
18 |
19 | # disable check https (DO NOT SET THIS IN PRODUCTION)
20 | $ export AUTHLIB_INSECURE_TRANSPORT=1
21 |
22 | Create Database and run the development server:
23 |
24 | $ flask initdb
25 | $ flask run
26 |
27 | Now, you can open your browser with `http://127.0.0.1:5000/`, login with any
28 | name you want.
29 |
30 | Before testing, we need to create a client:
31 |
32 | 
33 |
34 | **NOTE: YOU MUST ADD `openid` SCOPE IN YOUR CLIENT**
35 |
36 | Let's take `authorization_code` grant type as an example. Visit:
37 |
38 | ```
39 | http://127.0.0.1:5000/oauth/authorize?client_id=${CLIENT_ID}&scope=openid+profile&response_type=code&nonce=abc
40 | ```
41 |
42 | After that, you will be redirect to a URL. For instance:
43 |
44 | ```
45 | https://example.com/?code=RSv6j745Ri0DhBSvi2RQu5JKpIVvLm8SFd5ObjOZZSijohe0
46 | ```
47 |
48 | Copy the code value, use `curl` to get the access token:
49 |
50 | ```
51 | curl -u "${CLIENT_ID}:${CLIENT_SECRET}" -XPOST http://127.0.0.1:5000/oauth/token -F grant_type=authorization_code -F code=RSv6j745Ri0DhBSvi2RQu5JKpIVvLm8SFd5ObjOZZSijohe0
52 | ```
53 |
54 | Now you can access the userinfo endpoint:
55 |
56 | ```bash
57 | $ curl -H "Authorization: Bearer ${access_token}" http://127.0.0.1:5000/oauth/userinfo
58 | ```
59 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | from website.app import create_app
2 |
3 |
4 | app = create_app({
5 | 'SECRET_KEY': 'secret',
6 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False,
7 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///db.sqlite',
8 | })
9 |
10 |
11 | @app.cli.command()
12 | def initdb():
13 | from website.models import db
14 | db.create_all()
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | Flask-SQLAlchemy
3 | Authlib==0.13
4 |
--------------------------------------------------------------------------------
/website/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/authlib/example-oidc-server/2aa7d991b4d354c4376b8cdc67c097ba56b55d57/website/__init__.py
--------------------------------------------------------------------------------
/website/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask
3 | from .models import db
4 | from .oauth2 import config_oauth
5 | from .routes import bp
6 |
7 |
8 | def create_app(config=None):
9 | app = Flask(__name__)
10 |
11 | # load default configuration
12 | app.config.from_object('website.settings')
13 |
14 | # load environment configuration
15 | if 'WEBSITE_CONF' in os.environ:
16 | app.config.from_envvar('WEBSITE_CONF')
17 |
18 | # load app sepcified configuration
19 | if config is not None:
20 | if isinstance(config, dict):
21 | app.config.update(config)
22 | elif config.endswith('.py'):
23 | app.config.from_pyfile(config)
24 |
25 | setup_app(app)
26 | return app
27 |
28 |
29 | def setup_app(app):
30 | db.init_app(app)
31 | config_oauth(app)
32 | app.register_blueprint(bp, url_prefix='')
33 |
--------------------------------------------------------------------------------
/website/models.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from authlib.integrations.sqla_oauth2 import (
3 | OAuth2ClientMixin,
4 | OAuth2TokenMixin,
5 | OAuth2AuthorizationCodeMixin
6 | )
7 |
8 | db = SQLAlchemy()
9 |
10 |
11 | class User(db.Model):
12 | id = db.Column(db.Integer, primary_key=True)
13 | username = db.Column(db.String(40), unique=True)
14 |
15 | def __str__(self):
16 | return self.username
17 |
18 | def get_user_id(self):
19 | return self.id
20 |
21 |
22 | class OAuth2Client(db.Model, OAuth2ClientMixin):
23 | __tablename__ = 'oauth2_client'
24 |
25 | id = db.Column(db.Integer, primary_key=True)
26 | user_id = db.Column(
27 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
28 | user = db.relationship('User')
29 |
30 |
31 | class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin):
32 | __tablename__ = 'oauth2_code'
33 |
34 | id = db.Column(db.Integer, primary_key=True)
35 | user_id = db.Column(
36 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
37 | user = db.relationship('User')
38 |
39 |
40 | class OAuth2Token(db.Model, OAuth2TokenMixin):
41 | __tablename__ = 'oauth2_token'
42 |
43 | id = db.Column(db.Integer, primary_key=True)
44 | user_id = db.Column(
45 | db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
46 | user = db.relationship('User')
47 |
--------------------------------------------------------------------------------
/website/oauth2.py:
--------------------------------------------------------------------------------
1 | from authlib.integrations.flask_oauth2 import (
2 | AuthorizationServer, ResourceProtector)
3 | from authlib.integrations.sqla_oauth2 import (
4 | create_query_client_func,
5 | create_save_token_func,
6 | create_bearer_token_validator,
7 | )
8 | from authlib.oauth2.rfc6749.grants import (
9 | AuthorizationCodeGrant as _AuthorizationCodeGrant,
10 | )
11 | from authlib.oidc.core.grants import (
12 | OpenIDCode as _OpenIDCode,
13 | OpenIDImplicitGrant as _OpenIDImplicitGrant,
14 | OpenIDHybridGrant as _OpenIDHybridGrant,
15 | )
16 | from authlib.oidc.core import UserInfo
17 | from werkzeug.security import gen_salt
18 | from .models import db, User
19 | from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
20 |
21 |
22 | DUMMY_JWT_CONFIG = {
23 | 'key': 'secret-key',
24 | 'alg': 'HS256',
25 | 'iss': 'https://authlib.org',
26 | 'exp': 3600,
27 | }
28 |
29 | def exists_nonce(nonce, req):
30 | exists = OAuth2AuthorizationCode.query.filter_by(
31 | client_id=req.client_id, nonce=nonce
32 | ).first()
33 | return bool(exists)
34 |
35 |
36 | def generate_user_info(user, scope):
37 | return UserInfo(sub=str(user.id), name=user.username)
38 |
39 |
40 | def create_authorization_code(client, grant_user, request):
41 | code = gen_salt(48)
42 | nonce = request.data.get('nonce')
43 | item = OAuth2AuthorizationCode(
44 | code=code,
45 | client_id=client.client_id,
46 | redirect_uri=request.redirect_uri,
47 | scope=request.scope,
48 | user_id=grant_user.id,
49 | nonce=nonce,
50 | )
51 | db.session.add(item)
52 | db.session.commit()
53 | return code
54 |
55 |
56 | class AuthorizationCodeGrant(_AuthorizationCodeGrant):
57 | def create_authorization_code(self, client, grant_user, request):
58 | return create_authorization_code(client, grant_user, request)
59 |
60 | def parse_authorization_code(self, code, client):
61 | item = OAuth2AuthorizationCode.query.filter_by(
62 | code=code, client_id=client.client_id).first()
63 | if item and not item.is_expired():
64 | return item
65 |
66 | def delete_authorization_code(self, authorization_code):
67 | db.session.delete(authorization_code)
68 | db.session.commit()
69 |
70 | def authenticate_user(self, authorization_code):
71 | return User.query.get(authorization_code.user_id)
72 |
73 |
74 | class OpenIDCode(_OpenIDCode):
75 | def exists_nonce(self, nonce, request):
76 | return exists_nonce(nonce, request)
77 |
78 | def get_jwt_config(self, grant):
79 | return DUMMY_JWT_CONFIG
80 |
81 | def generate_user_info(self, user, scope):
82 | return generate_user_info(user, scope)
83 |
84 |
85 | class ImplicitGrant(_OpenIDImplicitGrant):
86 | def exists_nonce(self, nonce, request):
87 | return exists_nonce(nonce, request)
88 |
89 | def get_jwt_config(self, grant):
90 | return DUMMY_JWT_CONFIG
91 |
92 | def generate_user_info(self, user, scope):
93 | return generate_user_info(user, scope)
94 |
95 |
96 | class HybridGrant(_OpenIDHybridGrant):
97 | def create_authorization_code(self, client, grant_user, request):
98 | return create_authorization_code(client, grant_user, request)
99 |
100 | def exists_nonce(self, nonce, request):
101 | return exists_nonce(nonce, request)
102 |
103 | def get_jwt_config(self):
104 | return DUMMY_JWT_CONFIG
105 |
106 | def generate_user_info(self, user, scope):
107 | return generate_user_info(user, scope)
108 |
109 |
110 | authorization = AuthorizationServer()
111 | require_oauth = ResourceProtector()
112 |
113 |
114 | def config_oauth(app):
115 | query_client = create_query_client_func(db.session, OAuth2Client)
116 | save_token = create_save_token_func(db.session, OAuth2Token)
117 | authorization.init_app(
118 | app,
119 | query_client=query_client,
120 | save_token=save_token
121 | )
122 |
123 | # support all openid grants
124 | authorization.register_grant(AuthorizationCodeGrant, [
125 | OpenIDCode(require_nonce=True),
126 | ])
127 | authorization.register_grant(ImplicitGrant)
128 | authorization.register_grant(HybridGrant)
129 |
130 | # protect resource
131 | bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
132 | require_oauth.register_token_validator(bearer_cls())
133 |
--------------------------------------------------------------------------------
/website/routes.py:
--------------------------------------------------------------------------------
1 | import time
2 | from flask import Blueprint, request, session
3 | from flask import render_template, redirect, jsonify
4 | from werkzeug.security import gen_salt
5 | from authlib.integrations.flask_oauth2 import current_token
6 | from authlib.oauth2 import OAuth2Error
7 | from .models import db, User, OAuth2Client
8 | from .oauth2 import authorization, require_oauth, generate_user_info
9 |
10 |
11 | bp = Blueprint(__name__, 'home')
12 |
13 |
14 | def current_user():
15 | if 'id' in session:
16 | uid = session['id']
17 | return User.query.get(uid)
18 | return None
19 |
20 |
21 | @bp.route('/', methods=('GET', 'POST'))
22 | def home():
23 | if request.method == 'POST':
24 | username = request.form.get('username')
25 | user = User.query.filter_by(username=username).first()
26 | if not user:
27 | user = User(username=username)
28 | db.session.add(user)
29 | db.session.commit()
30 | session['id'] = user.id
31 | return redirect('/')
32 | user = current_user()
33 | if user:
34 | clients = OAuth2Client.query.filter_by(user_id=user.id).all()
35 | else:
36 | clients = []
37 | return render_template('home.html', user=user, clients=clients)
38 |
39 |
40 | def split_by_crlf(s):
41 | return [v for v in s.splitlines() if v]
42 |
43 |
44 | @bp.route('/create_client', methods=('GET', 'POST'))
45 | def create_client():
46 | user = current_user()
47 | if not user:
48 | return redirect('/')
49 | if request.method == 'GET':
50 | return render_template('create_client.html')
51 | form = request.form
52 | client_id = gen_salt(24)
53 | client = OAuth2Client(client_id=client_id, user_id=user.id)
54 | # Mixin doesn't set the issue_at date
55 | client.client_id_issued_at = int(time.time())
56 | if client.token_endpoint_auth_method == 'none':
57 | client.client_secret = ''
58 | else:
59 | client.client_secret = gen_salt(48)
60 |
61 | client_metadata = {
62 | "client_name": form["client_name"],
63 | "client_uri": form["client_uri"],
64 | "grant_types": split_by_crlf(form["grant_type"]),
65 | "redirect_uris": split_by_crlf(form["redirect_uri"]),
66 | "response_types": split_by_crlf(form["response_type"]),
67 | "scope": form["scope"],
68 | "token_endpoint_auth_method": form["token_endpoint_auth_method"]
69 | }
70 | client.set_client_metadata(client_metadata)
71 | db.session.add(client)
72 | db.session.commit()
73 | return redirect('/')
74 |
75 |
76 | @bp.route('/oauth/authorize', methods=['GET', 'POST'])
77 | def authorize():
78 | user = current_user()
79 | if request.method == 'GET':
80 | try:
81 | grant = authorization.validate_consent_request(end_user=user)
82 | except OAuth2Error as error:
83 | return jsonify(dict(error.get_body()))
84 | return render_template('authorize.html', user=user, grant=grant)
85 | if not user and 'username' in request.form:
86 | username = request.form.get('username')
87 | user = User.query.filter_by(username=username).first()
88 | if request.form['confirm']:
89 | grant_user = user
90 | else:
91 | grant_user = None
92 | return authorization.create_authorization_response(grant_user=grant_user)
93 |
94 |
95 | @bp.route('/oauth/token', methods=['POST'])
96 | def issue_token():
97 | return authorization.create_token_response()
98 |
99 |
100 | @bp.route('/oauth/userinfo')
101 | @require_oauth('profile')
102 | def api_me():
103 | return jsonify(generate_user_info(current_token.user, current_token.scope))
104 |
--------------------------------------------------------------------------------
/website/settings.py:
--------------------------------------------------------------------------------
1 | OAUTH2_JWT_ENABLED = True
2 |
3 | OAUTH2_JWT_ISS = 'https://authlib.org'
4 | OAUTH2_JWT_KEY = 'secret-key'
5 | OAUTH2_JWT_ALG = 'HS256'
6 |
--------------------------------------------------------------------------------
/website/templates/authorize.html:
--------------------------------------------------------------------------------
1 | {{grant.client.client_name}} is requesting:
2 | {{ grant.request.scope }}
3 |
4 |
5 |
19 |
--------------------------------------------------------------------------------
/website/templates/create_client.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | Home
7 |
8 |
43 |
--------------------------------------------------------------------------------
/website/templates/home.html:
--------------------------------------------------------------------------------
1 | {% if user %}
2 |
3 | Logged in as {{user}}
4 |
5 | {% for client in clients %}
6 |
7 | {{ client.client_info|tojson }}
8 | {{ client.client_metadata|tojson }}
9 |
10 |
11 | {% endfor %}
12 |
13 |
Create Client
14 |
15 | {% else %}
16 |
20 | {% endif %}
21 |
--------------------------------------------------------------------------------