├── .gitignore ├── README.md ├── app.py ├── requirements.txt └── website ├── __init__.py ├── app.py ├── models.py ├── oauth2.py ├── routes.py ├── settings.py └── templates ├── authorize.html ├── create_client.html └── home.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of OpenID Connect 1.0 Provider 2 | 3 | This is an example of OpenID Connect 1.0 server in Flask and [Authlib](https://authlib.org/). 4 | 5 | - Documentation: 6 | - Authlib Repo: 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 | ![create a client](https://user-images.githubusercontent.com/290496/64176341-35888100-ce98-11e9-8395-fd4cdc029fd2.png) 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 |
6 | 10 | {% if not user %} 11 |

You haven't logged in. Log in with:

12 |
13 | 14 |
15 | {% endif %} 16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /website/templates/create_client.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | Home 7 | 8 |
9 | 13 | 17 | 21 | 25 | 29 | 33 | 41 | 42 |
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 |
17 | 18 | 19 |
20 | {% endif %} 21 | --------------------------------------------------------------------------------