Skip to content

Commit 304189f

Browse files
authored
Flask plugin (#17)
* Flask plugin init commit
2 parents 3508afc + 712cc0d commit 304189f

23 files changed

+766
-0
lines changed

plugins/flask_yoti/LICENSE

Whitespace-only changes.

plugins/flask_yoti/MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include LICENSE
2+
include README.md
3+
recursive-include flask_yoti/templates *.html
4+
recursive-include example/templates *.html

plugins/flask_yoti/README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Flask Yoti #
2+
3+
## Plugin configuration ##
4+
### General settings ###
5+
6+
* `flask_yoti` is a [Flask Blueprint](http://flask.pocoo.org/docs/0.11/blueprints/)
7+
and all you have to do to add it to your Flask app is register it like this:
8+
```python
9+
# your_flask_project/app.py
10+
from flask import Flask
11+
from flask_yoti import flask_yoti_blueprint
12+
13+
app = Flask(__name__)
14+
app.register_blueprint(flask_yoti_blueprint, url_prefix='/yoti')
15+
```
16+
*Don't forget to set an `app.secret_key` to be able to use `sessions`*
17+
18+
* And then use the following settings to configure the plugin:
19+
20+
21+
```python
22+
# your_flask_project/app.py
23+
24+
...
25+
26+
app.config.update({
27+
'YOTI_APPLICATION_ID': '...',
28+
'YOTI_CLIENT_SDK_ID': '...',
29+
'YOTI_KEY_FILE_PATH': '...',
30+
'YOTI_VERIFICATION_KEY': '...',
31+
...
32+
})
33+
```
34+
* **`YOTI_APPLICATION_ID`** - **required**, *can be also set by env variable with the same name*<br>
35+
Your Yoti application's ID, found under the `INTEGRATIONS` tab of your
36+
Yoti application's settings page ([Yoti Dashboard](https://www.yoti.com/dashboard/)).<br>
37+
It is used to configure the [Yoti Login Button](https://www.yoti.com/developers/#login-button-setup).<br>
38+
Example: `ca84f68b-1b48-458b-96bf-963868edc8b6`
39+
40+
* **`YOTI_CLIENT_SDK_ID`** - **required**, *can be also set by env variable with the same name*<br>
41+
Your Yoti application's SDK ID, found under the `INTEGRATIONS` tab of your
42+
Yoti application's settings page ([Yoti Dashboard](https://www.yoti.com/dashboard/)).<br>
43+
Example: `39aef70a-89d6-4644-a687-b3e891613da6`
44+
45+
* **`YOTI_KEY_FILE_PATH`** - **required**, *can be also set by env variable with the same name*<br>
46+
The full path to your private key downloaded from your Yoti application's
47+
settings page under the `KEYS` tab ([Yoti Dashboard](https://www.yoti.com/dashboard/)).<br>
48+
Example: `/home/user/.ssh/access-security.pem`
49+
50+
* **`YOTI_VERIFICATION_KEY`** - *can be also set by env variable with the same name*<br>
51+
A key, used to verify your callback URL. Can be found under the
52+
`INTEGRATIONS` tab of your Yoti application's settings page (Callback URL -> VERIFY).<br>
53+
Example: `b14886f972d0c717`
54+
55+
56+
### Endpoints configuration ###
57+
58+
`flask_yoti` plugin provides some default endpoints:
59+
- `yoti_auth` (`/yoti/auth`) - is a callback used for receiving an
60+
authentication token (shouldn't be changed)
61+
- `yoti_login` (`/yoti/login`) - a view with just a login button. Can (and should)
62+
be overridden by `'YOTI_LOGIN_VIEW'` setting
63+
- `yoti_profile` (`/yoti/profile`) - a view with user profile details. It's
64+
also given just for example and should be overridden by your view, using
65+
`'YOTI_REDIRECT_TO'` setting
66+
67+
```python
68+
# your_flask_project/app.py
69+
70+
...
71+
72+
app.config.update({
73+
...
74+
'YOTI_LOGIN_VIEW': '...',
75+
'YOTI_REDIRECT_TO': '...',
76+
'YOTI_LOGIN_BUTTON_LABEL': '...',
77+
})
78+
```
79+
* **`YOTI_LOGIN_VIEW`**<br>
80+
If *not* authenticated user is trying to access a view with
81+
`@yoti_authenticated` decorator, he/she will be redirected to this view.
82+
Example: `login`<br>
83+
In this case you should have something like this in your Flask app:
84+
```python
85+
@app.route('/login')
86+
def login():
87+
render_template('login.html')
88+
```
89+
Default value: `flask_yoti.login` (with `/yoti/login/` URL)
90+
91+
* **`YOTI_REDIRECT_TO`**<br>
92+
View name to which user is redirected after successful authentication.<br>
93+
Example: `profile`<br>
94+
In this case you should have something like this in your Flask app::
95+
```python
96+
@app.route('/profile')
97+
@yoti_authenticated
98+
def login():
99+
user_profile = session.get('yoti_user_profile')
100+
render_template('profile.html', **user_profile)
101+
```
102+
Default value: `flask_yoti.profile` (with `/yoti/profile/` URL)
103+
104+
<br>
105+
106+
107+
### Yoti application configuration ###
108+
109+
Your Yoti application's callback URL should point to `your_site.com/yoti/auth`.<br>
110+
If you want to add a verification tag into any template using Jinja2 or DTL as a
111+
template language (other than `/yoti/auth/`, because it's already has one),
112+
you can use a `{{ yoti_site_verification }}` tag inside `<head>` of that template.
113+
114+
## Plugin usage ##
115+
116+
1. First you need to add a login button to some of your view's templates.
117+
- You can do it by using one of the predefined login buttons:
118+
```
119+
{{ yoti_login_button_sm }}
120+
{{ yoti_login_button_md }}
121+
{{ yoti_login_button_lg }}
122+
```
123+
- or with `{{ yoti_login_button(size='small', text='Log In with Yoti')`<br>
124+
Available button sizes: `small`, `medium`, `large`
125+
126+
By clicking this button, user will be redirected to the Yoti Authentication page.
127+
128+
*Remember to add an appropriate script to your page with login
129+
button in order for it to work. See: [Yoti Developers Documentation](https://www.yoti.com/developers/#login-button-setup)*
130+
131+
2. After successful authentication, user will be redirected to a view,
132+
provided by the `YOTI_REDIRECT_TO` setting.
133+
3. In order to have an access to an authenticated user's information inside a view,
134+
you should use a `@yoti_authenticated` decorator.
135+
Example:
136+
```python
137+
from flask_yoti import yoti_authenticated
138+
139+
@yoti_authenticated
140+
def profile_view(request):
141+
user_id = request.yoti_user_id
142+
user_profile = request.yoti_user_profile
143+
return render(request, 'profile.html', user_profile)
144+
```
145+
146+
4. All *not authenticated* users trying to access endpoint with this decorator,
147+
will be redirected to an endpoint, provided by the `YOTI_LOGIN_VIEW` setting.
148+
149+
## Tests ##
150+
151+
To run unit tests just type `py.test` inside `flask_yoti` dir.

plugins/flask_yoti/example/app.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
3+
from flask import Flask, render_template, session
4+
from flask_yoti import flask_yoti_blueprint
5+
from flask_yoti.decorators import yoti_authenticated
6+
7+
YOTI_APPLICATION_ID = os.environ.get('YOTI_APPLICATION_ID')
8+
9+
app = Flask(__name__)
10+
app.secret_key = os.urandom(24)
11+
12+
app.config.update(
13+
YOTI_APPLICATION_ID=YOTI_APPLICATION_ID,
14+
YOTI_VERIFICATION_KEY=os.environ.get('YOTI_VERIFICATION_KEY'),
15+
YOTI_CLIENT_SDK_ID=os.environ.get('YOTI_CLIENT_SDK_ID'),
16+
YOTI_KEY_FILE_PATH=os.environ.get('YOTI_KEY_FILE_PATH'),
17+
YOTI_LOGIN_VIEW='login',
18+
YOTI_REDIRECT_TO='profile'
19+
)
20+
21+
app.register_blueprint(flask_yoti_blueprint, url_prefix='/yoti')
22+
23+
24+
@app.route('/')
25+
def index():
26+
return render_template('index.html', app_id=YOTI_APPLICATION_ID)
27+
28+
29+
@app.route('/login')
30+
def login():
31+
return render_template('login.html')
32+
33+
34+
@app.route('/profile')
35+
@yoti_authenticated
36+
def profile():
37+
user_profile = session.get('yoti_user_profile', {})
38+
return render_template('profile.html', **user_profile)
39+
40+
41+
if __name__ == '__main__':
42+
app.run(debug=True)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<!-- Your header content -->
5+
<script src="https://sdk.yoti.com/clients/browser.js"></script>
6+
<meta charset="UTF-8">
7+
<title>Index</title>
8+
</head>
9+
10+
<body style="background-color:papayawhip;">
11+
<!-- Your website content -->
12+
13+
<!-- This span will create the Yoti button -->
14+
Button: {{ yoti_login_button('small', 'Custom label') }}
15+
16+
<!-- This script snippet will also be required in your HTML body during the early pilot period -->
17+
<script>
18+
_ybg.config.service = 'https://www.yoti.com/connect/';
19+
_ybg.init();
20+
</script>
21+
</body>
22+
</html>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="yoti-site-verification" content="{{ verification_key }}">
6+
<title>PROFILE</title>
7+
</head>
8+
<body style="background-color:papayawhip;">
9+
<h3><a href="/">Home</a></h3>
10+
<table>
11+
{% if user_id %}
12+
<tr>
13+
<td>USER ID:</td>
14+
<td>{{user_id}}</td>
15+
</tr>
16+
{% endif %}
17+
18+
{% if selfie %}
19+
<tr>
20+
<td>SELFIE:</td>
21+
<td><img width="200" alt="photo" src="{{selfie}}" /></td>
22+
</tr>
23+
{% endif %}
24+
25+
{% if phone_number %}
26+
<tr>
27+
<td>PHONE:</td>
28+
<td>{{phone_number}}</td>
29+
</tr>
30+
{% endif %}
31+
32+
{% if names %}
33+
<tr>
34+
<td>NAMES:</td>
35+
<td>{{names}}</td>
36+
</tr>
37+
{% endif %}
38+
39+
{% if birthdate %}
40+
<tr>
41+
<td>DATE OF BIRTH</td>
42+
<td>{{birthdate}}</td>
43+
</tr>
44+
{% endif %}
45+
46+
{% if nationality %}
47+
<tr>
48+
<td>NATIONALITY</td>
49+
<td>{{nationality}}</td>
50+
</tr>
51+
{% endif %}
52+
53+
{% if gender %}
54+
<tr>
55+
<td>GENDER</td>
56+
<td>{{gender}}</td>
57+
</tr>
58+
{% endif %}
59+
</table>
60+
</body>
61+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .flask_yoti import flask_yoti_blueprint
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask import Markup
2+
3+
from .login_button import get_login_button_html
4+
from .settings import get_config_value
5+
6+
7+
def yoti_context():
8+
context = login_button_context()
9+
context.update(site_verification_context())
10+
context.update(application_context())
11+
return context
12+
13+
14+
def login_button_context():
15+
return {
16+
'yoti_login_button': get_login_button_html,
17+
'yoti_login_button_sm': get_login_button_html('small'),
18+
'yoti_login_button_md': get_login_button_html('medium'),
19+
'yoti_login_button_lg': get_login_button_html('large')
20+
}
21+
22+
23+
def site_verification_context():
24+
verification_key = get_config_value('YOTI_VERIFICATION_KEY')
25+
raw_text = '<meta name="yoti-site-verification" content="{0}">'.format(
26+
verification_key
27+
)
28+
return {'yoti_site_verification': Markup(raw_text)}
29+
30+
31+
def application_context():
32+
yoti_application_id = get_config_value('YOTI_APPLICATION_ID')
33+
return {'yoti_application_id': yoti_application_id}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from time import time
2+
3+
4+
class ActivityDetailsStorage(object):
5+
6+
def __init__(self):
7+
self.storage = {}
8+
self._timestamps = {}
9+
self._timeout_sec = 15
10+
11+
def save(self, activity_details):
12+
user_id = activity_details.user_id
13+
self.storage[user_id] = activity_details
14+
self._timestamps[user_id] = time() + self._timeout_sec
15+
16+
def get(self, user_id):
17+
self._clear_outdated()
18+
return self.storage.pop(user_id, None)
19+
20+
def _clear_outdated(self):
21+
current_time = time()
22+
to_remove = []
23+
for user_id, timestamp in self._timestamps.items():
24+
if timestamp <= current_time:
25+
to_remove.append(user_id)
26+
27+
for user_id in to_remove:
28+
del self.storage[user_id]
29+
del self._timestamps[user_id]
30+
31+
32+
activity_details_storage = ActivityDetailsStorage()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from functools import wraps
2+
from flask import redirect, session, url_for
3+
4+
from .context_storage import activity_details_storage
5+
from .settings import get_config_value
6+
from .helpers import is_cookie_session
7+
8+
9+
def yoti_authenticated(view_func):
10+
@wraps(view_func)
11+
def _decorated(*args, **kwargs):
12+
user_id = session.get('yoti_user_id')
13+
if not is_cookie_session(session):
14+
activity_details = session.get('activity_details')
15+
else:
16+
activity_details = activity_details_storage.get(user_id)
17+
if not activity_details or activity_details.outcome != 'SUCCESS':
18+
yoti_login_view = get_config_value('YOTI_LOGIN_VIEW')
19+
return redirect(url_for(yoti_login_view))
20+
21+
session['yoti_user_id'] = activity_details.user_id
22+
session['yoti_user_profile'] = activity_details.user_profile
23+
24+
return view_func(*args, **kwargs)
25+
26+
return _decorated

0 commit comments

Comments
 (0)