Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 122 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,145 +9,173 @@
![Django Versions](https://img.shields.io/pypi/frameworkversions/django/django-passkeys)
![Python Versions](https://img.shields.io/pypi/pyversions/django-passkeys)

An extension to Django's *ModelBackend* that supports passkeys.

An extension to Django *ModelBackend* backend to support passkeys.
Passkeys are an extension to Web Authentication API that allow the user to log in to a service using a securely stored
key on the same or another device.

Passkeys is an extension to Web Authentication API that will allow the user to login to a service using another device.
This app is a slimmed-down version of [django-mfa2](https://github.com/mkalioby/django-mfa2).

This app is a slim-down version of [django-mfa2](https://github.com/mkalioby/django-mfa2)
Passkeys are supported on **all major browsers and operating systems**.
See [passkeys.io](https://www.passkeys.io/compatible-devices) for details.
On May 3, 2023, Google enabled Passkeys for the users to login, killing the password for enrolled users.

Passkeys are now supported on
* Apple Ecosystem (iPhone 16.0+, iPadOS 16.1, Mac OS X Ventura)
* Chromium based browsers (on PC and Laptop) allows picking up credentials from Android and iPhone/iPadOS.
* Android Credentials creation for ResidentKeys is currently in live now.
## Installation

On May 3, 2023, Google allowed the use of Passkeys for the users to login, killing the password for enrolled users.
`pip install django-passkeys`

# Installation
Currently, django-passkeys supports Django 2.0+ and Python 3.7+.

`pip install django-passkeys`
## Usage

Currently, it support Django 2.0+, Python 3.7+
**Important note**: Passkeys only work in a secure context, i.e., when using SSL/HTTPS.

# Usage
1. in your settings.py add the application to your installed apps
```python
INSTALLED_APPS=(
'......',
'passkeys',
'......')
```
2. Collect Static Files
`python manage.py collectstatic`
3. Run migrate
`python manage.py migrate`
4. Add the following settings to your file
### Setup

1. In your `settings.py`, add the application to your installed apps.
```python
AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend'] # Change your authentication backend
FIDO_SERVER_ID="localhost" # Server rp id for FIDO2, it the full domain of your project
INSTALLED_APPS=(
'......',
'passkeys',
'......',
)
```
**Note**: Only apps listed before `passkeys` are able to override templates and static files of this app.
2. Run `collectstatic`:
`python manage.py collectstatic`
3. Run `migrate`:
`python manage.py migrate`
4. Make the following changes to your settings.py file:
```python
# Update authentication backends, replacing 'django.contrib.auth.ModelBackend' with 'passkeys.backend.PasskeyModelBackend'
AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend']
# Server RP ID for FIDO2 - Use the full domain name (but without subdomains like "www.") of your project
FIDO_SERVER_ID="localhost"
# Server Name - You can choose any name here, preferably the one of your application.
FIDO_SERVER_NAME="TestApp"

# Optionally, restrict the use to cross platform (roaming) or platform authenticators.
# See https://www.w3.org/TR/webauthn/#sctn-authenticator-attachment-modality for details
import passkeys
KEY_ATTACHMENT = None | passkeys.Attachment.CROSS_PLATFORM | passkeys.Attachment.PLATFORM
```
**Note**: Starting v1.1, `FIDO_SERVER_ID` and/or `FIDO_SERVER_NAME` can be a callable to support multi-tenants web applications, the `request` is passed to the called function.
5. Add passkeys to urls.py
```python

urls_patterns= [
'...',
url(r'^passkeys/', include('passkeys.urls')),
'....',
KEY_ATTACHMENT = None # No restriction (default if left out)
KEY_ATTACHMENT = passkeys.Attachment.CROSS_PLATFORM # Only cross-platform / roaming authenticators allowed
KEY_ATTACHMENT = passkeys.Attachment.PLATFORM # Only platform authenticators allowed
```
**Note**: Starting with v1.1, `FIDO_SERVER_ID` and/or `FIDO_SERVER_NAME` can be a callable to support multi-tenants
web applications, the `request` is passed to the called function.
5. Add passkeys to urls.py:
```python
urlpatterns= [
'...',
path('passkeys/', include('passkeys.urls')),
'....',
]
```
6. To match the look and feel of your project, Passkeys includes `base.html` but it needs blocks named `head` & `content` to added its content to it.
**Notes:**

1. You can override `PassKeys_base.html` which is used by `Passkeys.html` so you can control the styling better and current `Passkeys_base.html` extends `base.html`
1. Currently, `PassKeys_base.html` needs JQuery and bootstrap.

7. Somewhere in your app, add a link to 'passkeys:home'
```<li><a href="{% url 'passkeys:home' %}">Passkeys</a> </li>```
8. In your login view, change the authenticate call to include the request as follows
```python
user=authenticate(request, username=request.POST["username"],password=request.POST["password"])
6. In your login view, change the authenticate call to include the request as follows:
```python
user = authenticate(request, username=request.POST["username"], password=request.POST["password"])
```
**Note**: If you use `django.contrib.auth`'s LoginView, you must override the `form_class` used.
7. Integrate passkey login in your login template (e.g. `login.html`):
* Give an id to your login form, e.g 'loginForm', which will be used below.
* Inside the form, add
```html
<input type="hidden" name="passkeys" id="passkeys" />
<button class="btn btn-block btn-dark" type="button" onclick="authn('loginForm')">
<img src="{% static 'passkeys/imgs/FIDO-Passkey_Icon-White.png' %}" style="width: 24px" alt="Passkey icon" />
</button>
{% include 'passkeys.js' %}
```
8. To match the look and feel of your project, Passkeys includes `base.html`, but it needs blocks named `head` &
`content` to add its content to them.
**Notes:**
1. `Passkeys_base.html` extends `base.html`.
2. You can override `PassKeys_base.html`, which is used by `Passkeys.html` so you can control the styling better.
3. Currently, `PassKeys_base.html` depends on JQuery and Bootstrap.
9. Somewhere in your app, add a link to 'passkeys:home'
```html
<a href="{% url 'passkeys:home' %}">Manage Passkeys</a>
```

For more information about how to set it up, please see the 'example' app and the EXAMPLE.md document.

### Detect if user is using passkeys

Once the backend is used, there will be a `passkey` key in `request.session`.
If the user used a passkey, then `request.session['passkey']['passkey']` will be `True` and the key information will be
available in the following format:

8. Finally, In your `login.html`
* Give an id to your login form e.g 'loginForm', the id should be provided when calling `authn` function
* Inside the form, add
```html
<input type="hidden" name="passkeys" id="passkeys"/>
<button class="btn btn-block btn-dark" type="button" onclick="authn('loginForm')"><img src="{% static 'passkeys/imgs/FIDO-Passkey_Icon-White.png' %}" style="width: 24px"></button>
{%include 'passkeys.js' %}
```
For Example, See 'example' app and look at EXAMPLE.md to see how to set it up.

# Detect if user is using passkeys
Once the backend is used, there will be a `passkey` key in request.session.
If the user used a passkey then `request.session['passkey']['passkey']` will be True and the key information will be there like this
```python
# request.session['passkey'] if the user signed in with a passkey
{'passkey': True, 'name': 'Chrome', 'id': 2, 'platform': 'Chrome on Apple', 'cross_platform': False}
```
`cross_platform`: means that the user used a key from another platform so there is no key local to the device used to login e.g used an Android phone on Mac OS X or iPad.
If the user didn't use a passkey then it will be set to False

* `cross_platform`: means that the user used a key from another platform, so there is no key locally to the device used to
login. This might be the case e.g if a user used an Android phone to log in on Windows.

If the user didn't use a passkey, then `request.session['passkey']['passkey']` will be set to `False`:

```python
{'passkey':False}
# request.session['passkey'] if the used didn't sign in with a passkey
{'passkey': False}
```

### Check if the user can be enrolled for a platform authenticator

# Check if the user can be enrolled for a platform authenticator

If you want to check if the user can be enrolled to use a platform authenticator, you can do the following in your main page.
If you want to check if the user can be enrolled to use a platform authenticator, you can do the following in your main
page:

```html
<div id="pk" class="alert alert-info" style="display: none">Your device supports passkeys, <a href="{%url 'passkeys:enroll'%}">Enroll</a> </div>

<div id="pk" class="alert alert-info" style="display: none">
Your device supports passkeys! <a href="{% url 'passkeys:enroll' %}">Enroll now</a>
</div>
<script type="text/javascript">
function register_pk()
{
{% include 'check_passkeys.js' %}

function showPasskeyEnrollmentInfo() {
$('#pk').show();
}
{% include 'check_passkeys.js'%}
$(document).ready(check_passkey(true,register_pk))

$(document).ready(check_passkey(true, showPasskeyEnrollmentInfo))
</script>
```
check_passkey function paramters are as follows
* `platform_authenticator`: if the service requires only a platform authenticator (e.g TouchID, Windows Hello or Android SafetyNet)
* `success_func`: function to call if a platform authenticator is found or if the user didn't login by a passkey
* `fail_func`: function to call if no platform authenticator is found (optional).

The `check_passkey` function parameters are as follows:

## Using Conditional UI
* `platform_authenticator`: if the service requires only a platform authenticator (e.g TouchID, Windows Hello or Android
SafetyNet)
* `success_func`: function to call if a platform authenticator is found and if the user didn't login with a passkey
* `fail_func`: function to call if no platform authenticator is found (optional).

Conditional UI is a way for the browser to prompt the user to use the passkey to login to the system as shown in
### Use Conditional UI

![conditionalUI.png](imgs%2FconditionalUI.png)
Conditional UI is a way for the browser to automatically prompt the user to use the passkey to log in, if he has one.
The following screenshot shows how this might look in macOS:

Starting version v1.2. you can use Conditional UI by adding the following to your login page
![conditionalUI.png](imgs/conditionalUI.png)

1. Add `webauthn` to autocomplete of the username field as shown below.
```html
<input name="username" placeholder="username" autocomplete="username webauthn">
```
add the following to the page js.
Starting with v1.2, you can use Conditional UI by adding the following to your login page:

```js
window.onload = checkConditionalUI('loginForm');
```
where `loginForm` is name of your login form.
1. Add `webauthn` to the autocomplete attribute of the username field as shown below.
```html
<input name="username" placeholder="username" autocomplete="username webauthn">
```
2. Add the following to the page JavaScript, where `loginForm` is the id of your login form.
```js
window.addEventListener("load", () => checkConditionalUI('loginForm'));
```

## Security contact information

To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

# Contributors
## Contributors

* [mahmoodnasr](https://github.com/mahmoodnasr)
* [jacopsd](https://github.com/jacopsd)
* [jacopsd](https://github.com/jacopsd)
* [gasparbrogueira](https://github.com/gasparbrogueira)
* [pulse-mind](https://github.com/pulse-mind)




* [rafaelurben](https://github.com/rafaelurben/)
3 changes: 3 additions & 0 deletions passkeys/FIDO2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import fido2.features
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
Expand Down Expand Up @@ -58,6 +59,7 @@ def get_current_platform(request):
else: return "Key"


@login_required()
def reg_begin(request):
"""Starts registering a new FIDO Device, called from API"""
enable_json_mapping()
Expand All @@ -74,6 +76,7 @@ def reg_begin(request):


@csrf_exempt
@login_required()
def reg_complete(request):
"""Completes the registeration, called by API"""
try:
Expand Down
2 changes: 1 addition & 1 deletion passkeys/templates/PassKeys.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
{% endfor %}
{% else %}
<tr>
<td colspan="7" align="center">You didn't have any keys yet.</td>
<td colspan="7" align="center">You don't have any keys yet.</td>
</tr>
{% endif %}
</table>
Expand Down