├── requirements.txt ├── Flask U2F Tutorial - Deployment Guide.pdf ├── .gitignore ├── schema.sql ├── templates ├── login.html ├── register.html ├── security.html ├── u2f_auth.html ├── u2f_add.html ├── layout.html └── timeline.html ├── README ├── server.crt ├── server.key ├── static ├── style.css └── u2f-api.js └── minitwit.py /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | u2fval-client>=1.0.1 3 | -------------------------------------------------------------------------------- /Flask U2F Tutorial - Deployment Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dainnilsson/flask-u2f-tutorial/HEAD/Flask U2F Tutorial - Deployment Guide.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | env 5 | env* 6 | dist 7 | *.egg 8 | *.egg-info 9 | _mailinglist 10 | .tox 11 | .ropeproject/ 12 | u2fval_api_token 13 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | drop table if exists user; 2 | create table user ( 3 | user_id integer primary key autoincrement, 4 | username text not null, 5 | email text not null, 6 | pw_hash text not null 7 | ); 8 | 9 | drop table if exists follower; 10 | create table follower ( 11 | who_id integer, 12 | whom_id integer 13 | ); 14 | 15 | drop table if exists message; 16 | create table message ( 17 | message_id integer primary key autoincrement, 18 | author_id integer not null, 19 | text text not null, 20 | pub_date integer 21 | ); 22 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Sign In{% endblock %} 3 | {% block body %} 4 |

Sign In

5 | {% if error %}
Error: {{ error }}
{% endif %} 6 |
7 |
8 |
Username: 9 |
10 |
Password: 11 |
12 |
13 |
14 |
15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Sign Up{% endblock %} 3 | {% block body %} 4 |

Sign Up

5 | {% if error %}
Error: {{ error }}
{% endif %} 6 |
7 |
8 |
Username: 9 |
10 |
E-Mail: 11 |
12 |
Password: 13 |
14 |
Password (repeat): 15 |
16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/security.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | Security settings 4 | {% endblock %} 5 | {% block body %} 6 |

{{ self.title() }}

7 | 17 | 18 |
19 |

Register a new U2F device

20 |
21 |

23 |

24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/u2f_auth.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | Authenticate a U2F device 4 | {% endblock %} 5 | {% block body %} 6 |

{{ self.title() }}

7 | 8 |
9 |

Insert and touch your U2F device

10 |
11 | 12 |
13 |
14 | 15 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/u2f_add.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | Register a U2F device 4 | {% endblock %} 5 | {% block body %} 6 |

{{ self.title() }}

7 | 8 |
9 |

Insert and touch a new U2F device

10 |
11 | 12 | 13 |
14 |
15 | 16 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | / MiniTwit U2F Tutorial / 3 | 4 | because security matters! 5 | 6 | 7 | ~ What is MiniTwit U2F Tutorial? 8 | 9 | A step-by-step tutorial on adding U2F support to an existing application. 10 | Starting from the MiniTwit example from the Flask microframework, this 11 | tutorial shows the steps needed to secur access to user accounts using 12 | U2F. 13 | 14 | ~ How do I use it? 15 | 16 | 1. Follow along the steps in the tutorial/X tags and compare the code 17 | changes between the tags. If you want to, you can run the code at each 18 | step to see the changes in action. 19 | 20 | 2. To run the project, first install the required dependencies by running: 21 | 22 | $ pip install -r requirements.txt 23 | 24 | Then initialize the database: 25 | 26 | $ python minitwit.py --init-db 27 | 28 | Now run the server: 29 | 30 | $ python minitwit.py 31 | 32 | Update: For a step-by-step deployment guide for Windows, see the 33 | Flask U2F Tutorial - Deployment Guide.pdf file included in this repository. 34 | 35 | -------------------------------------------------------------------------------- /server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDLjCCAhYCCQCsht5srN7D1DANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJT 3 | RTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTUxMDA4MTM0NjUwWhcN 5 | MTYxMDA3MTM0NjUwWjBZMQswCQYDVQQGEwJTRTETMBEGA1UECAwKU29tZS1TdGF0 6 | ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAls 7 | b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs7u7yRegV 8 | 85ifp0ibXRe04fHWDdA63sJ/jGoZX0rQqHMkAYkko9OaumdaNKthaK6bnkKFVkvN 9 | uI7A5mzVVAN8ruj40kTy4RUXbZZHvsX11eo2G/u4LHRK1eBkk7RFftHW5JljPqv+ 10 | gkFpsQI5khn51f59sCMTh8CH5vUuPESZakFywlJK5GM5umBqS56QJZQ2L1KIxzsm 11 | 9X6Dm3935LuekQs1X8G8QrjmKbSM0Etf1IyGW7cKutaBfxF8OSW+1wWJqH4hkELm 12 | Ie7jLgi32JVgDL2VSzgVGNhL4uLVs3FVSZgN9VHFAp+mJwheOtgqR6aNNiMDLJha 13 | pHUErXIR9KSXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEWDSZ+3rbaxbKOYhkRi 14 | BzBaPylH4FL7Ths1xoXfeMe0ExTe1FSr9zsuQQPn93zwH2B1Km+T9y3XPXTbB99y 15 | 7qNt++Mm4mEug6dOM0PM1Nud8Yts/XBTdt9syuF9lmllqBsKowYDPTRSn7GnukCW 16 | k06+/iH+H1bumQWP8s/HHc1jCq5YAO5P7aONxsjzyy1axIA4mbHKYZJQSNqLsiqT 17 | yGBNzKa8tG7b5aBzn6clHyKXSyn4qLN+xOgSKRpDgUMnMxfxipkFHDGtIGm2EJAt 18 | ka/C5FBUujRWqYxXRMIouPNeFSsfRLeYgpcjX25gW/2m70olkzvPeQAjh6Y2djvO 19 | JZo= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}Welcome{% endblock %} | MiniTwit 4 | 5 |
6 |

MiniTwit

7 | 19 | {% with flashes = get_flashed_messages() %} 20 | {% if flashes %} 21 | 26 | {% endif %} 27 | {% endwith %} 28 |
29 | {% block body %}{% endblock %} 30 |
31 | 34 |
35 | -------------------------------------------------------------------------------- /server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEArO7u8kXoFfOYn6dIm10XtOHx1g3QOt7Cf4xqGV9K0KhzJAGJ 3 | JKPTmrpnWjSrYWium55ChVZLzbiOwOZs1VQDfK7o+NJE8uEVF22WR77F9dXqNhv7 4 | uCx0StXgZJO0RX7R1uSZYz6r/oJBabECOZIZ+dX+fbAjE4fAh+b1LjxEmWpBcsJS 5 | SuRjObpgakuekCWUNi9SiMc7JvV+g5t/d+S7npELNV/BvEK45im0jNBLX9SMhlu3 6 | CrrWgX8RfDklvtcFiah+IZBC5iHu4y4It9iVYAy9lUs4FRjYS+Li1bNxVUmYDfVR 7 | xQKfpicIXjrYKkemjTYjAyyYWqR1BK1yEfSklwIDAQABAoIBADHh2drYf1GVqnii 8 | 8Daga64pXnC4G1Bf4QqZniEjc5ksfcntB3oiJ2+CRT2n46d9YqBQzi9X7RWyHrtV 9 | vB7s1PSqH1lmjazhcAwJ+EdJqCB5S82/1KQTbpgHiWp5kI5bPnwWBIi0Ezieqe+q 10 | t1GT6xo9t+LZY8TGa6rH3AEyMTdvDWZYSkAmxbqe5DuYd+jBLpEqcxelbXP2hyan 11 | jMfgaQDEh4SJ+U4jk5Y163JXV2TEZQIhx+6ILdEK3CXHAugbFdXigMXDpNgBMHO0 12 | YQRe+obgJI1EhOjDhlm1Nlm18W335aHGApMwHtnwcyQkOyd6wthQUpfAnqay0crt 13 | Z/W8ocECgYEA2wokn4QFBI4D6axVbf+TUdYsYoRNW9HmyaQTx94MUUDwca4+UORq 14 | L2OIU1qn3EcE8ya9O6NHKZyTJManPqJM9lFeWmZEY71UNuwPqRBE9s2MiOts2uB3 15 | QkKGfyphF6U201LTcEjQZ6aZqMxIFb1ptI7M1yEen33whhStieS54CsCgYEAyh0i 16 | 8dKq2uoNXZL/Ilslf423QsIqt6mBnBoocUxQcNmZHuBFi9FHjx6ddQSeY6p5A5BH 17 | Wex87vaSQoDgHNvH8psWluPc5kKdSS3DakLuh2rQOGHPYlWPSgWFVwImUrOFqkYw 18 | A5hTxoqqjjN8royjTbamQC5/NKxdXZR3QUucK0UCgYAAi3bp2qc0irHhy+bufhs8 19 | sd6sZA3ZM51yVPEjpx66uQGgFsHa66aD9ahqJKiUOKz/edIwqshLhzMqfT//POIa 20 | HruwV97FrLvf4xhq4Dp7rqkx0fwUU0iYppe1C0Lwjx2iyurxtYynJVfufouTWkRA 21 | CbbithdgaCzH+Jcx55q6vwKBgQCkjqp3940yZePx7xAZedqDCvBNw2ciWDl1znpl 22 | HLxV1WTtFa6qEv/PUB7lOph3D0IuG5dsaLajnVAiI2nVNUCLj6gJvIaLV2tWPJNh 23 | fhNVYCsd7Mz8BCuBGhOhbtei+BV5OGI5WxnCif4pf1QhjdIcIP0lPnZFfZ0a5xld 24 | qWECfQKBgQCPXMRJvmhXy46/XhpE89IOVA+6VGTvZjFzxigIZXZcebWKudVtGaT6 25 | efVfUwefQSy+zoJ29kLEPinDCGVsVTAuJZtrUzUIBjWpFABFWcBvj3LBolOH9XqK 26 | RovyIuNqqnw/JaSsjor3DKkdQdsENTGwQUNhxwn33Y/pDGJDyWSnlw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /templates/timeline.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | {% if request.endpoint == 'public_timeline' %} 4 | Public Timeline 5 | {% elif request.endpoint == 'user_timeline' %} 6 | {{ profile_user.username }}'s Timeline 7 | {% else %} 8 | My Timeline 9 | {% endif %} 10 | {% endblock %} 11 | {% block body %} 12 |

{{ self.title() }}

13 | {% if g.user %} 14 | {% if request.endpoint == 'user_timeline' %} 15 |
16 | {% if g.user.user_id == profile_user.user_id %} 17 | This is you! 18 | {% elif followed %} 19 | You are currently following this user. 20 | Unfollow user. 22 | {% else %} 23 | You are not yet following this user. 24 | Follow user. 26 | {% endif %} 27 |
28 | {% elif request.endpoint == 'timeline' %} 29 |
30 |

What's on your mind {{ g.user.username }}?

31 |
32 |

34 |

35 |
36 | {% endif %} 37 | {% endif %} 38 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #CAECE9; 3 | font-family: 'Trebuchet MS', sans-serif; 4 | font-size: 14px; 5 | } 6 | 7 | a { 8 | color: #26776F; 9 | } 10 | 11 | a:hover { 12 | color: #333; 13 | } 14 | 15 | input[type="text"], 16 | input[type="password"] { 17 | background: white; 18 | border: 1px solid #BFE6E2; 19 | padding: 2px; 20 | font-family: 'Trebuchet MS', sans-serif; 21 | font-size: 14px; 22 | -moz-border-radius: 2px; 23 | -webkit-border-radius: 2px; 24 | color: #105751; 25 | } 26 | 27 | input[type="submit"] { 28 | background: #105751; 29 | border: 1px solid #073B36; 30 | padding: 1px 3px; 31 | font-family: 'Trebuchet MS', sans-serif; 32 | font-size: 14px; 33 | font-weight: bold; 34 | -moz-border-radius: 2px; 35 | -webkit-border-radius: 2px; 36 | color: white; 37 | } 38 | 39 | div.page { 40 | background: white; 41 | border: 1px solid #6ECCC4; 42 | width: 700px; 43 | margin: 30px auto; 44 | } 45 | 46 | div.page h1 { 47 | background: #6ECCC4; 48 | margin: 0; 49 | padding: 10px 14px; 50 | color: white; 51 | letter-spacing: 1px; 52 | text-shadow: 0 0 3px #24776F; 53 | font-weight: normal; 54 | } 55 | 56 | div.page div.navigation { 57 | background: #DEE9E8; 58 | padding: 4px 10px; 59 | border-top: 1px solid #ccc; 60 | border-bottom: 1px solid #eee; 61 | color: #888; 62 | font-size: 12px; 63 | letter-spacing: 0.5px; 64 | } 65 | 66 | div.page div.navigation a { 67 | color: #444; 68 | font-weight: bold; 69 | } 70 | 71 | div.page h2 { 72 | margin: 0 0 15px 0; 73 | color: #105751; 74 | text-shadow: 0 1px 2px #ccc; 75 | } 76 | 77 | div.page div.body { 78 | padding: 10px; 79 | } 80 | 81 | div.page div.footer { 82 | background: #eee; 83 | color: #888; 84 | padding: 5px 10px; 85 | font-size: 12px; 86 | } 87 | 88 | div.page div.followstatus { 89 | border: 1px solid #ccc; 90 | background: #E3EBEA; 91 | -moz-border-radius: 2px; 92 | -webkit-border-radius: 2px; 93 | padding: 3px; 94 | font-size: 13px; 95 | } 96 | 97 | div.page ul.messages { 98 | list-style: none; 99 | margin: 0; 100 | padding: 0; 101 | } 102 | 103 | div.page ul.messages li { 104 | margin: 10px 0; 105 | padding: 5px; 106 | background: #F0FAF9; 107 | border: 1px solid #DBF3F1; 108 | -moz-border-radius: 5px; 109 | -webkit-border-radius: 5px; 110 | min-height: 48px; 111 | } 112 | 113 | div.page ul.messages p { 114 | margin: 0; 115 | } 116 | 117 | div.page ul.messages li img { 118 | float: left; 119 | padding: 0 10px 0 0; 120 | } 121 | 122 | div.page ul.messages li small { 123 | font-size: 0.9em; 124 | color: #888; 125 | } 126 | 127 | div.page div.twitbox { 128 | margin: 10px 0; 129 | padding: 5px; 130 | background: #F0FAF9; 131 | border: 1px solid #94E2DA; 132 | -moz-border-radius: 5px; 133 | -webkit-border-radius: 5px; 134 | } 135 | 136 | div.page div.twitbox h3 { 137 | margin: 0; 138 | font-size: 1em; 139 | color: #2C7E76; 140 | } 141 | 142 | div.page div.twitbox p { 143 | margin: 0; 144 | } 145 | 146 | div.page div.twitbox input[type="text"] { 147 | width: 585px; 148 | } 149 | 150 | div.page div.twitbox input[type="submit"] { 151 | width: 70px; 152 | margin-left: 5px; 153 | } 154 | 155 | ul.flashes { 156 | list-style: none; 157 | margin: 10px 10px 0 10px; 158 | padding: 0; 159 | } 160 | 161 | ul.flashes li { 162 | background: #B9F3ED; 163 | border: 1px solid #81CEC6; 164 | -moz-border-radius: 2px; 165 | -webkit-border-radius: 2px; 166 | padding: 4px; 167 | font-size: 13px; 168 | } 169 | 170 | div.error { 171 | margin: 10px 0; 172 | background: #FAE4E4; 173 | border: 1px solid #DD6F6F; 174 | -moz-border-radius: 2px; 175 | -webkit-border-radius: 2px; 176 | padding: 4px; 177 | font-size: 13px; 178 | } 179 | -------------------------------------------------------------------------------- /minitwit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | MiniTwit 4 | ~~~~~~~~ 5 | 6 | A microblogging application written with Flask and sqlite3. 7 | 8 | :copyright: (c) 2010 by Armin Ronacher. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | 12 | import time 13 | from sqlite3 import dbapi2 as sqlite3 14 | from hashlib import md5 15 | from datetime import datetime 16 | from flask import Flask, request, session, url_for, redirect, \ 17 | render_template, abort, g, flash, _app_ctx_stack 18 | from werkzeug import check_password_hash, generate_password_hash 19 | 20 | from u2fval_client.client import Client 21 | from u2fval_client.auth import ApiToken 22 | from u2fval_client import exc 23 | 24 | # U2FVAL server settings 25 | U2FVAL_HOST = 'https://u2fval.appspot.com/api' 26 | try: 27 | with open('u2fval_api_token', 'r') as f: 28 | U2FVAL_API_TOKEN = f.read().strip() 29 | except IOError: 30 | print 'No U2FVAL API key found.' 31 | 32 | # U2FVAL client 33 | u2fval = Client(U2FVAL_HOST, ApiToken(U2FVAL_API_TOKEN)) 34 | 35 | # configuration 36 | DATABASE = '/tmp/minitwit.db' 37 | PER_PAGE = 30 38 | DEBUG = True 39 | SECRET_KEY = 'development key' 40 | 41 | # create our little application :) 42 | app = Flask(__name__) 43 | app.config.from_object(__name__) 44 | app.config.from_envvar('MINITWIT_SETTINGS', silent=True) 45 | 46 | 47 | def get_db(): 48 | """Opens a new database connection if there is none yet for the 49 | current application context. 50 | """ 51 | top = _app_ctx_stack.top 52 | if not hasattr(top, 'sqlite_db'): 53 | top.sqlite_db = sqlite3.connect(app.config['DATABASE']) 54 | top.sqlite_db.row_factory = sqlite3.Row 55 | return top.sqlite_db 56 | 57 | 58 | def get_current_user(): 59 | """Get the currently logged in user ID, as a string. 60 | If the user is not logged in, respond with 401.""" 61 | if 'user_id' not in session: 62 | abort(401) 63 | return str(session['user_id']) 64 | 65 | 66 | @app.teardown_appcontext 67 | def close_database(exception): 68 | """Closes the database again at the end of the request.""" 69 | top = _app_ctx_stack.top 70 | if hasattr(top, 'sqlite_db'): 71 | top.sqlite_db.close() 72 | 73 | 74 | def init_db(): 75 | """Creates the database tables.""" 76 | with app.app_context(): 77 | db = get_db() 78 | with app.open_resource('schema.sql', mode='r') as f: 79 | db.cursor().executescript(f.read()) 80 | db.commit() 81 | 82 | 83 | def query_db(query, args=(), one=False): 84 | """Queries the database and returns a list of dictionaries.""" 85 | cur = get_db().execute(query, args) 86 | rv = cur.fetchall() 87 | return (rv[0] if rv else None) if one else rv 88 | 89 | 90 | def get_user_id(username): 91 | """Convenience method to look up the id for a username.""" 92 | rv = query_db('select user_id from user where username = ?', 93 | [username], one=True) 94 | return rv[0] if rv else None 95 | 96 | 97 | def format_datetime(timestamp): 98 | """Format a timestamp for display.""" 99 | return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') 100 | 101 | 102 | def gravatar_url(email, size=80): 103 | """Return the gravatar image for the given email address.""" 104 | return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ 105 | (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) 106 | 107 | 108 | @app.before_request 109 | def before_request(): 110 | g.user = None 111 | if 'user_id' in session: 112 | g.user = query_db('select * from user where user_id = ?', 113 | [session['user_id']], one=True) 114 | 115 | 116 | @app.route('/') 117 | def timeline(): 118 | """Shows a users timeline or if no user is logged in it will 119 | redirect to the public timeline. This timeline shows the user's 120 | messages as well as all the messages of followed users. 121 | """ 122 | if not g.user: 123 | return redirect(url_for('public_timeline')) 124 | return render_template('timeline.html', messages=query_db(''' 125 | select message.*, user.* from message, user 126 | where message.author_id = user.user_id and ( 127 | user.user_id = ? or 128 | user.user_id in (select whom_id from follower 129 | where who_id = ?)) 130 | order by message.pub_date desc limit ?''', 131 | [session['user_id'], session['user_id'], PER_PAGE])) 132 | 133 | 134 | @app.route('/public') 135 | def public_timeline(): 136 | """Displays the latest messages of all users.""" 137 | return render_template('timeline.html', messages=query_db(''' 138 | select message.*, user.* from message, user 139 | where message.author_id = user.user_id 140 | order by message.pub_date desc limit ?''', [PER_PAGE])) 141 | 142 | 143 | @app.route('/') 144 | def user_timeline(username): 145 | """Display's a users tweets.""" 146 | profile_user = query_db('select * from user where username = ?', 147 | [username], one=True) 148 | if profile_user is None: 149 | abort(404) 150 | followed = False 151 | if g.user: 152 | followed = query_db('''select 1 from follower where 153 | follower.who_id = ? and follower.whom_id = ?''', 154 | [session['user_id'], profile_user['user_id']], 155 | one=True) is not None 156 | return render_template('timeline.html', messages=query_db(''' 157 | select message.*, user.* from message, user where 158 | user.user_id = message.author_id and user.user_id = ? 159 | order by message.pub_date desc limit ?''', 160 | [profile_user['user_id'], PER_PAGE]), followed=followed, 161 | profile_user=profile_user) 162 | 163 | 164 | @app.route('//follow') 165 | def follow_user(username): 166 | """Adds the current user as follower of the given user.""" 167 | if not g.user: 168 | abort(401) 169 | whom_id = get_user_id(username) 170 | if whom_id is None: 171 | abort(404) 172 | db = get_db() 173 | db.execute('insert into follower (who_id, whom_id) values (?, ?)', 174 | [session['user_id'], whom_id]) 175 | db.commit() 176 | flash('You are now following "%s"' % username) 177 | return redirect(url_for('user_timeline', username=username)) 178 | 179 | 180 | @app.route('//unfollow') 181 | def unfollow_user(username): 182 | """Removes the current user as follower of the given user.""" 183 | if not g.user: 184 | abort(401) 185 | whom_id = get_user_id(username) 186 | if whom_id is None: 187 | abort(404) 188 | db = get_db() 189 | db.execute('delete from follower where who_id=? and whom_id=?', 190 | [session['user_id'], whom_id]) 191 | db.commit() 192 | flash('You are no longer following "%s"' % username) 193 | return redirect(url_for('user_timeline', username=username)) 194 | 195 | 196 | @app.route('/add_message', methods=['POST']) 197 | def add_message(): 198 | """Registers a new message for the user.""" 199 | if 'user_id' not in session: 200 | abort(401) 201 | if request.form['text']: 202 | db = get_db() 203 | db.execute('''insert into message (author_id, text, pub_date) 204 | values (?, ?, ?)''', (session['user_id'], request.form['text'], 205 | int(time.time()))) 206 | db.commit() 207 | flash('Your message was recorded') 208 | return redirect(url_for('timeline')) 209 | 210 | 211 | @app.route('/login', methods=['GET', 'POST']) 212 | def login(): 213 | """Logs the user in.""" 214 | if g.user: 215 | return redirect(url_for('timeline')) 216 | error = None 217 | if request.method == 'POST': 218 | user = query_db('''select * from user where 219 | username = ?''', [request.form['username']], one=True) 220 | if user is None: 221 | error = 'Invalid username' 222 | elif not check_password_hash(user['pw_hash'], 223 | request.form['password']): 224 | error = 'Invalid password' 225 | else: 226 | try: 227 | session['u2f_user_id'] = user['user_id'] 228 | auth_req = u2fval.auth_begin(str(user['user_id'])) 229 | return render_template('u2f_auth.html', auth_req=auth_req) 230 | except exc.NoEligableDevicesException as e: 231 | if not e.has_devices(): 232 | flash('You were logged in without U2F') 233 | session['user_id'] = user['user_id'] 234 | return redirect(url_for('timeline')) 235 | error = e.message 236 | return render_template('login.html', error=error) 237 | 238 | 239 | @app.route('/u2f_login_complete', methods=['POST']) 240 | def u2f_login_complete(): 241 | device = u2fval.auth_complete(str(session['u2f_user_id']), 242 | request.form['u2f_data']) 243 | flash('Authenticated using %s' % device['properties']['name']) 244 | session['user_id'] = session['u2f_user_id'] 245 | return redirect(url_for('timeline')) 246 | 247 | 248 | @app.route('/register', methods=['GET', 'POST']) 249 | def register(): 250 | """Registers the user.""" 251 | if g.user: 252 | return redirect(url_for('timeline')) 253 | error = None 254 | if request.method == 'POST': 255 | if not request.form['username']: 256 | error = 'You have to enter a username' 257 | elif not request.form['email'] or \ 258 | '@' not in request.form['email']: 259 | error = 'You have to enter a valid email address' 260 | elif not request.form['password']: 261 | error = 'You have to enter a password' 262 | elif request.form['password'] != request.form['password2']: 263 | error = 'The two passwords do not match' 264 | elif get_user_id(request.form['username']) is not None: 265 | error = 'The username is already taken' 266 | else: 267 | db = get_db() 268 | db.execute('''insert into user ( 269 | username, email, pw_hash) values (?, ?, ?)''', 270 | [request.form['username'], request.form['email'], 271 | generate_password_hash(request.form['password'])]) 272 | db.commit() 273 | flash('You were successfully registered and can login now') 274 | return redirect(url_for('login')) 275 | return render_template('register.html', error=error) 276 | 277 | 278 | @app.route('/security') 279 | def security(): 280 | """Security (U2F) options.""" 281 | devices = u2fval.list_devices(get_current_user()) 282 | return render_template('security.html', devices=devices) 283 | 284 | 285 | @app.route('/u2f_register', methods=['POST']) 286 | def u2f_register(): 287 | """Register a U2F device""" 288 | reg_req = u2fval.register_begin(get_current_user()) 289 | return render_template('u2f_add.html', 290 | name=request.form['name'], 291 | reg_req=reg_req) 292 | 293 | 294 | @app.route('/u2f_register_complete', methods=['POST']) 295 | def u2f_register_complete(): 296 | u2fval.register_complete(get_current_user(), request.form['u2f_data'], 297 | {'name': request.form['name']}) 298 | return redirect(url_for('security')) 299 | 300 | 301 | @app.route('/u2f_unregister/') 302 | def u2f_unregister(handle): 303 | """Remove a registered U2F device""" 304 | u2fval.unregister(get_current_user(), handle) 305 | return redirect(url_for('security')) 306 | 307 | 308 | @app.route('/logout') 309 | def logout(): 310 | """Logs the user out.""" 311 | flash('You were logged out') 312 | session.pop('user_id', None) 313 | return redirect(url_for('public_timeline')) 314 | 315 | 316 | # add some filters to jinja 317 | app.jinja_env.filters['datetimeformat'] = format_datetime 318 | app.jinja_env.filters['gravatar'] = gravatar_url 319 | 320 | 321 | if __name__ == '__main__': 322 | import sys 323 | if '--init-db' in sys.argv: 324 | init_db() 325 | print "Database initialized!" 326 | sys.exit(0) 327 | 328 | import ssl 329 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 330 | context.load_cert_chain('server.crt', 'server.key') 331 | 332 | app.run(ssl_context=context) 333 | -------------------------------------------------------------------------------- /static/u2f-api.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014-2015 Google Inc. All rights reserved. 2 | 3 | //Use of this source code is governed by a BSD-style 4 | //license that can be found in the LICENSE file or at 5 | //https://developers.google.com/open-source/licenses/bsd 6 | 7 | /** 8 | * @fileoverview The U2F api. 9 | */ 10 | 'use strict'; 11 | 12 | 13 | /** 14 | * Namespace for the U2F api. 15 | * @type {Object} 16 | */ 17 | var u2f = u2f || {}; 18 | 19 | /** 20 | * FIDO U2F Javascript API Version 21 | * @number 22 | */ 23 | var js_api_version; 24 | 25 | /** 26 | * The U2F extension id 27 | * @const {string} 28 | */ 29 | // The Chrome packaged app extension ID. 30 | // Uncomment this if you want to deploy a server instance that uses 31 | // the package Chrome app and does not require installing the U2F Chrome extension. 32 | u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; 33 | // The U2F Chrome extension ID. 34 | // Uncomment this if you want to deploy a server instance that uses 35 | // the U2F Chrome extension to authenticate. 36 | // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; 37 | 38 | 39 | /** 40 | * Message types for messsages to/from the extension 41 | * @const 42 | * @enum {string} 43 | */ 44 | u2f.MessageTypes = { 45 | 'U2F_REGISTER_REQUEST': 'u2f_register_request', 46 | 'U2F_REGISTER_RESPONSE': 'u2f_register_response', 47 | 'U2F_SIGN_REQUEST': 'u2f_sign_request', 48 | 'U2F_SIGN_RESPONSE': 'u2f_sign_response', 49 | 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', 50 | 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' 51 | }; 52 | 53 | 54 | /** 55 | * Response status codes 56 | * @const 57 | * @enum {number} 58 | */ 59 | u2f.ErrorCodes = { 60 | 'OK': 0, 61 | 'OTHER_ERROR': 1, 62 | 'BAD_REQUEST': 2, 63 | 'CONFIGURATION_UNSUPPORTED': 3, 64 | 'DEVICE_INELIGIBLE': 4, 65 | 'TIMEOUT': 5 66 | }; 67 | 68 | 69 | /** 70 | * A message for registration requests 71 | * @typedef {{ 72 | * type: u2f.MessageTypes, 73 | * appId: ?string, 74 | * timeoutSeconds: ?number, 75 | * requestId: ?number 76 | * }} 77 | */ 78 | u2f.U2fRequest; 79 | 80 | 81 | /** 82 | * A message for registration responses 83 | * @typedef {{ 84 | * type: u2f.MessageTypes, 85 | * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), 86 | * requestId: ?number 87 | * }} 88 | */ 89 | u2f.U2fResponse; 90 | 91 | 92 | /** 93 | * An error object for responses 94 | * @typedef {{ 95 | * errorCode: u2f.ErrorCodes, 96 | * errorMessage: ?string 97 | * }} 98 | */ 99 | u2f.Error; 100 | 101 | /** 102 | * Data object for a single sign request. 103 | * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} 104 | */ 105 | u2f.Transport; 106 | 107 | 108 | /** 109 | * Data object for a single sign request. 110 | * @typedef {Array} 111 | */ 112 | u2f.Transports; 113 | 114 | /** 115 | * Data object for a single sign request. 116 | * @typedef {{ 117 | * version: string, 118 | * challenge: string, 119 | * keyHandle: string, 120 | * appId: string 121 | * }} 122 | */ 123 | u2f.SignRequest; 124 | 125 | 126 | /** 127 | * Data object for a sign response. 128 | * @typedef {{ 129 | * keyHandle: string, 130 | * signatureData: string, 131 | * clientData: string 132 | * }} 133 | */ 134 | u2f.SignResponse; 135 | 136 | 137 | /** 138 | * Data object for a registration request. 139 | * @typedef {{ 140 | * version: string, 141 | * challenge: string 142 | * }} 143 | */ 144 | u2f.RegisterRequest; 145 | 146 | 147 | /** 148 | * Data object for a registration response. 149 | * @typedef {{ 150 | * version: string, 151 | * keyHandle: string, 152 | * transports: Transports, 153 | * appId: string 154 | * }} 155 | */ 156 | u2f.RegisterResponse; 157 | 158 | 159 | /** 160 | * Data object for a registered key. 161 | * @typedef {{ 162 | * version: string, 163 | * keyHandle: string, 164 | * transports: ?Transports, 165 | * appId: ?string 166 | * }} 167 | */ 168 | u2f.RegisteredKey; 169 | 170 | 171 | /** 172 | * Data object for a get API register response. 173 | * @typedef {{ 174 | * js_api_version: number 175 | * }} 176 | */ 177 | u2f.GetJsApiVersionResponse; 178 | 179 | 180 | //Low level MessagePort API support 181 | 182 | /** 183 | * Sets up a MessagePort to the U2F extension using the 184 | * available mechanisms. 185 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 186 | */ 187 | u2f.getMessagePort = function(callback) { 188 | if (typeof chrome != 'undefined' && chrome.runtime) { 189 | // The actual message here does not matter, but we need to get a reply 190 | // for the callback to run. Thus, send an empty signature request 191 | // in order to get a failure response. 192 | var msg = { 193 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 194 | signRequests: [] 195 | }; 196 | chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { 197 | if (!chrome.runtime.lastError) { 198 | // We are on a whitelisted origin and can talk directly 199 | // with the extension. 200 | u2f.getChromeRuntimePort_(callback); 201 | } else { 202 | // chrome.runtime was available, but we couldn't message 203 | // the extension directly, use iframe 204 | u2f.getIframePort_(callback); 205 | } 206 | }); 207 | } else if (u2f.isAndroidChrome_()) { 208 | u2f.getAuthenticatorPort_(callback); 209 | } else if (u2f.isIosChrome_()) { 210 | u2f.getIosPort_(callback); 211 | } else { 212 | // chrome.runtime was not available at all, which is normal 213 | // when this origin doesn't have access to any extensions. 214 | u2f.getIframePort_(callback); 215 | } 216 | }; 217 | 218 | /** 219 | * Detect chrome running on android based on the browser's useragent. 220 | * @private 221 | */ 222 | u2f.isAndroidChrome_ = function() { 223 | var userAgent = navigator.userAgent; 224 | return userAgent.indexOf('Chrome') != -1 && 225 | userAgent.indexOf('Android') != -1; 226 | }; 227 | 228 | /** 229 | * Detect chrome running on iOS based on the browser's platform. 230 | * @private 231 | */ 232 | u2f.isIosChrome_ = function() { 233 | return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; 234 | }; 235 | 236 | /** 237 | * Connects directly to the extension via chrome.runtime.connect. 238 | * @param {function(u2f.WrappedChromeRuntimePort_)} callback 239 | * @private 240 | */ 241 | u2f.getChromeRuntimePort_ = function(callback) { 242 | var port = chrome.runtime.connect(u2f.EXTENSION_ID, 243 | {'includeTlsChannelId': true}); 244 | setTimeout(function() { 245 | callback(new u2f.WrappedChromeRuntimePort_(port)); 246 | }, 0); 247 | }; 248 | 249 | /** 250 | * Return a 'port' abstraction to the Authenticator app. 251 | * @param {function(u2f.WrappedAuthenticatorPort_)} callback 252 | * @private 253 | */ 254 | u2f.getAuthenticatorPort_ = function(callback) { 255 | setTimeout(function() { 256 | callback(new u2f.WrappedAuthenticatorPort_()); 257 | }, 0); 258 | }; 259 | 260 | /** 261 | * Return a 'port' abstraction to the iOS client app. 262 | * @param {function(u2f.WrappedIosPort_)} callback 263 | * @private 264 | */ 265 | u2f.getIosPort_ = function(callback) { 266 | setTimeout(function() { 267 | callback(new u2f.WrappedIosPort_()); 268 | }, 0); 269 | }; 270 | 271 | /** 272 | * A wrapper for chrome.runtime.Port that is compatible with MessagePort. 273 | * @param {Port} port 274 | * @constructor 275 | * @private 276 | */ 277 | u2f.WrappedChromeRuntimePort_ = function(port) { 278 | this.port_ = port; 279 | }; 280 | 281 | /** 282 | * Format and return a sign request compliant with the JS API version supported by the extension. 283 | * @param {Array} signRequests 284 | * @param {number} timeoutSeconds 285 | * @param {number} reqId 286 | * @return {Object} 287 | */ 288 | u2f.formatSignRequest_ = 289 | function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { 290 | if (js_api_version === undefined || js_api_version < 1.1) { 291 | // Adapt request to the 1.0 JS API 292 | var signRequests = []; 293 | for (var i = 0; i < registeredKeys.length; i++) { 294 | signRequests[i] = { 295 | version: registeredKeys[i].version, 296 | challenge: challenge, 297 | keyHandle: registeredKeys[i].keyHandle, 298 | appId: appId 299 | }; 300 | } 301 | return { 302 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 303 | signRequests: signRequests, 304 | timeoutSeconds: timeoutSeconds, 305 | requestId: reqId 306 | }; 307 | } 308 | // JS 1.1 API 309 | return { 310 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 311 | appId: appId, 312 | challenge: challenge, 313 | registeredKeys: registeredKeys, 314 | timeoutSeconds: timeoutSeconds, 315 | requestId: reqId 316 | }; 317 | }; 318 | 319 | /** 320 | * Format and return a register request compliant with the JS API version supported by the extension.. 321 | * @param {Array} signRequests 322 | * @param {Array} signRequests 323 | * @param {number} timeoutSeconds 324 | * @param {number} reqId 325 | * @return {Object} 326 | */ 327 | u2f.formatRegisterRequest_ = 328 | function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { 329 | if (js_api_version === undefined || js_api_version < 1.1) { 330 | // Adapt request to the 1.0 JS API 331 | for (var i = 0; i < registerRequests.length; i++) { 332 | registerRequests[i].appId = appId; 333 | } 334 | var signRequests = []; 335 | for (var i = 0; i < registeredKeys.length; i++) { 336 | signRequests[i] = { 337 | version: registeredKeys[i].version, 338 | challenge: registerRequests[0], 339 | keyHandle: registeredKeys[i].keyHandle, 340 | appId: appId 341 | }; 342 | } 343 | return { 344 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 345 | signRequests: signRequests, 346 | registerRequests: registerRequests, 347 | timeoutSeconds: timeoutSeconds, 348 | requestId: reqId 349 | }; 350 | } 351 | // JS 1.1 API 352 | return { 353 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 354 | appId: appId, 355 | registerRequests: registerRequests, 356 | registeredKeys: registeredKeys, 357 | timeoutSeconds: timeoutSeconds, 358 | requestId: reqId 359 | }; 360 | }; 361 | 362 | 363 | /** 364 | * Posts a message on the underlying channel. 365 | * @param {Object} message 366 | */ 367 | u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { 368 | this.port_.postMessage(message); 369 | }; 370 | 371 | 372 | /** 373 | * Emulates the HTML 5 addEventListener interface. Works only for the 374 | * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. 375 | * @param {string} eventName 376 | * @param {function({data: Object})} handler 377 | */ 378 | u2f.WrappedChromeRuntimePort_.prototype.addEventListener = 379 | function(eventName, handler) { 380 | var name = eventName.toLowerCase(); 381 | if (name == 'message' || name == 'onmessage') { 382 | this.port_.onMessage.addListener(function(message) { 383 | // Emulate a minimal MessageEvent object 384 | handler({'data': message}); 385 | }); 386 | } else { 387 | console.error('WrappedChromeRuntimePort only supports onMessage'); 388 | } 389 | }; 390 | 391 | /** 392 | * Wrap the Authenticator app with a MessagePort interface. 393 | * @constructor 394 | * @private 395 | */ 396 | u2f.WrappedAuthenticatorPort_ = function() { 397 | this.requestId_ = -1; 398 | this.requestObject_ = null; 399 | } 400 | 401 | /** 402 | * Launch the Authenticator intent. 403 | * @param {Object} message 404 | */ 405 | u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { 406 | var intentUrl = 407 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + 408 | ';S.request=' + encodeURIComponent(JSON.stringify(message)) + 409 | ';end'; 410 | document.location = intentUrl; 411 | }; 412 | 413 | /** 414 | * Tells what type of port this is. 415 | * @return {String} port type 416 | */ 417 | u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { 418 | return "WrappedAuthenticatorPort_"; 419 | }; 420 | 421 | 422 | /** 423 | * Emulates the HTML 5 addEventListener interface. 424 | * @param {string} eventName 425 | * @param {function({data: Object})} handler 426 | */ 427 | u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { 428 | var name = eventName.toLowerCase(); 429 | if (name == 'message') { 430 | var self = this; 431 | /* Register a callback to that executes when 432 | * chrome injects the response. */ 433 | window.addEventListener( 434 | 'message', self.onRequestUpdate_.bind(self, handler), false); 435 | } else { 436 | console.error('WrappedAuthenticatorPort only supports message'); 437 | } 438 | }; 439 | 440 | /** 441 | * Callback invoked when a response is received from the Authenticator. 442 | * @param function({data: Object}) callback 443 | * @param {Object} message message Object 444 | */ 445 | u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = 446 | function(callback, message) { 447 | var messageObject = JSON.parse(message.data); 448 | var intentUrl = messageObject['intentURL']; 449 | 450 | var errorCode = messageObject['errorCode']; 451 | var responseObject = null; 452 | if (messageObject.hasOwnProperty('data')) { 453 | responseObject = /** @type {Object} */ ( 454 | JSON.parse(messageObject['data'])); 455 | } 456 | 457 | callback({'data': responseObject}); 458 | }; 459 | 460 | /** 461 | * Base URL for intents to Authenticator. 462 | * @const 463 | * @private 464 | */ 465 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = 466 | 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; 467 | 468 | /** 469 | * Wrap the iOS client app with a MessagePort interface. 470 | * @constructor 471 | * @private 472 | */ 473 | u2f.WrappedIosPort_ = function() {}; 474 | 475 | /** 476 | * Launch the iOS client app request 477 | * @param {Object} message 478 | */ 479 | u2f.WrappedIosPort_.prototype.postMessage = function(message) { 480 | var str = JSON.stringify(message); 481 | var url = "u2f://auth?" + encodeURI(str); 482 | location.replace(url); 483 | }; 484 | 485 | /** 486 | * Tells what type of port this is. 487 | * @return {String} port type 488 | */ 489 | u2f.WrappedIosPort_.prototype.getPortType = function() { 490 | return "WrappedIosPort_"; 491 | }; 492 | 493 | /** 494 | * Emulates the HTML 5 addEventListener interface. 495 | * @param {string} eventName 496 | * @param {function({data: Object})} handler 497 | */ 498 | u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { 499 | var name = eventName.toLowerCase(); 500 | if (name !== 'message') { 501 | console.error('WrappedIosPort only supports message'); 502 | } 503 | }; 504 | 505 | /** 506 | * Sets up an embedded trampoline iframe, sourced from the extension. 507 | * @param {function(MessagePort)} callback 508 | * @private 509 | */ 510 | u2f.getIframePort_ = function(callback) { 511 | // Create the iframe 512 | var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; 513 | var iframe = document.createElement('iframe'); 514 | iframe.src = iframeOrigin + '/u2f-comms.html'; 515 | iframe.setAttribute('style', 'display:none'); 516 | document.body.appendChild(iframe); 517 | 518 | var channel = new MessageChannel(); 519 | var ready = function(message) { 520 | if (message.data == 'ready') { 521 | channel.port1.removeEventListener('message', ready); 522 | callback(channel.port1); 523 | } else { 524 | console.error('First event on iframe port was not "ready"'); 525 | } 526 | }; 527 | channel.port1.addEventListener('message', ready); 528 | channel.port1.start(); 529 | 530 | iframe.addEventListener('load', function() { 531 | // Deliver the port to the iframe and initialize 532 | iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); 533 | }); 534 | }; 535 | 536 | 537 | //High-level JS API 538 | 539 | /** 540 | * Default extension response timeout in seconds. 541 | * @const 542 | */ 543 | u2f.EXTENSION_TIMEOUT_SEC = 30; 544 | 545 | /** 546 | * A singleton instance for a MessagePort to the extension. 547 | * @type {MessagePort|u2f.WrappedChromeRuntimePort_} 548 | * @private 549 | */ 550 | u2f.port_ = null; 551 | 552 | /** 553 | * Callbacks waiting for a port 554 | * @type {Array} 555 | * @private 556 | */ 557 | u2f.waitingForPort_ = []; 558 | 559 | /** 560 | * A counter for requestIds. 561 | * @type {number} 562 | * @private 563 | */ 564 | u2f.reqCounter_ = 0; 565 | 566 | /** 567 | * A map from requestIds to client callbacks 568 | * @type {Object.} 570 | * @private 571 | */ 572 | u2f.callbackMap_ = {}; 573 | 574 | /** 575 | * Creates or retrieves the MessagePort singleton to use. 576 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 577 | * @private 578 | */ 579 | u2f.getPortSingleton_ = function(callback) { 580 | if (u2f.port_) { 581 | callback(u2f.port_); 582 | } else { 583 | if (u2f.waitingForPort_.length == 0) { 584 | u2f.getMessagePort(function(port) { 585 | u2f.port_ = port; 586 | u2f.port_.addEventListener('message', 587 | /** @type {function(Event)} */ (u2f.responseHandler_)); 588 | 589 | // Careful, here be async callbacks. Maybe. 590 | while (u2f.waitingForPort_.length) 591 | u2f.waitingForPort_.shift()(u2f.port_); 592 | }); 593 | } 594 | u2f.waitingForPort_.push(callback); 595 | } 596 | }; 597 | 598 | /** 599 | * Handles response messages from the extension. 600 | * @param {MessageEvent.} message 601 | * @private 602 | */ 603 | u2f.responseHandler_ = function(message) { 604 | var response = message.data; 605 | var reqId = response['requestId']; 606 | if (!reqId || !u2f.callbackMap_[reqId]) { 607 | console.error('Unknown or missing requestId in response.'); 608 | return; 609 | } 610 | var cb = u2f.callbackMap_[reqId]; 611 | delete u2f.callbackMap_[reqId]; 612 | cb(response['responseData']); 613 | }; 614 | 615 | /** 616 | * Dispatches an array of sign requests to available U2F tokens. 617 | * If the JS API version supported by the extension is unknown, it first sends a 618 | * message to the extension to find out the supported API version and then it sends 619 | * the sign request. 620 | * @param {string=} appId 621 | * @param {string=} challenge 622 | * @param {Array} registeredKeys 623 | * @param {function((u2f.Error|u2f.SignResponse))} callback 624 | * @param {number=} opt_timeoutSeconds 625 | */ 626 | u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 627 | if (js_api_version === undefined) { 628 | // Send a message to get the extension to JS API version, then send the actual sign request. 629 | u2f.getApiVersion( 630 | function (response) { 631 | js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; 632 | console.log("Extension JS API Version: ", js_api_version); 633 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 634 | }); 635 | } else { 636 | // We know the JS API version. Send the actual sign request in the supported API version. 637 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 638 | } 639 | }; 640 | 641 | /** 642 | * Dispatches an array of sign requests to available U2F tokens. 643 | * @param {string=} appId 644 | * @param {string=} challenge 645 | * @param {Array} registeredKeys 646 | * @param {function((u2f.Error|u2f.SignResponse))} callback 647 | * @param {number=} opt_timeoutSeconds 648 | */ 649 | u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 650 | u2f.getPortSingleton_(function(port) { 651 | var reqId = ++u2f.reqCounter_; 652 | u2f.callbackMap_[reqId] = callback; 653 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 654 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 655 | var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); 656 | port.postMessage(req); 657 | }); 658 | }; 659 | 660 | /** 661 | * Dispatches register requests to available U2F tokens. An array of sign 662 | * requests identifies already registered tokens. 663 | * If the JS API version supported by the extension is unknown, it first sends a 664 | * message to the extension to find out the supported API version and then it sends 665 | * the register request. 666 | * @param {string=} appId 667 | * @param {Array} registerRequests 668 | * @param {Array} registeredKeys 669 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 670 | * @param {number=} opt_timeoutSeconds 671 | */ 672 | u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 673 | if (js_api_version === undefined) { 674 | // Send a message to get the extension to JS API version, then send the actual register request. 675 | u2f.getApiVersion( 676 | function (response) { 677 | js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; 678 | console.log("Extension JS API Version: ", js_api_version); 679 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 680 | callback, opt_timeoutSeconds); 681 | }); 682 | } else { 683 | // We know the JS API version. Send the actual register request in the supported API version. 684 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 685 | callback, opt_timeoutSeconds); 686 | } 687 | }; 688 | 689 | /** 690 | * Dispatches register requests to available U2F tokens. An array of sign 691 | * requests identifies already registered tokens. 692 | * @param {string=} appId 693 | * @param {Array} registerRequests 694 | * @param {Array} registeredKeys 695 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 696 | * @param {number=} opt_timeoutSeconds 697 | */ 698 | u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 699 | u2f.getPortSingleton_(function(port) { 700 | var reqId = ++u2f.reqCounter_; 701 | u2f.callbackMap_[reqId] = callback; 702 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 703 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 704 | var req = u2f.formatRegisterRequest_( 705 | appId, registeredKeys, registerRequests, timeoutSeconds, reqId); 706 | port.postMessage(req); 707 | }); 708 | }; 709 | 710 | 711 | /** 712 | * Dispatches a message to the extension to find out the supported 713 | * JS API version. 714 | * If the user is on a mobile phone and is thus using Google Authenticator instead 715 | * of the Chrome extension, don't send the request and simply return 0. 716 | * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback 717 | * @param {number=} opt_timeoutSeconds 718 | */ 719 | u2f.getApiVersion = function(callback, opt_timeoutSeconds) { 720 | u2f.getPortSingleton_(function(port) { 721 | // If we are using Android Google Authenticator or iOS client app, 722 | // do not fire an intent to ask which JS API version to use. 723 | if (port.getPortType) { 724 | var apiVersion; 725 | switch (port.getPortType()) { 726 | case 'WrappedIosPort_': 727 | case 'WrappedAuthenticatorPort_': 728 | apiVersion = 1.1; 729 | break; 730 | 731 | default: 732 | apiVersion = 0; 733 | break; 734 | } 735 | callback({ 'js_api_version': apiVersion }); 736 | return; 737 | } 738 | var reqId = ++u2f.reqCounter_; 739 | u2f.callbackMap_[reqId] = callback; 740 | var req = { 741 | type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, 742 | timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? 743 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), 744 | requestId: reqId 745 | }; 746 | port.postMessage(req); 747 | }); 748 | }; 749 | --------------------------------------------------------------------------------