diff --git a/README.md b/README.md index e841201..82838a0 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ Passkeys are now supported on On May 3, 2023, Google allowed the use of Passkeys for the users to login, killing the password for enrolled users. -# Installation +## Installation `pip install django-passkeys` Currently, it support Django 2.0+, Python 3.7+ -# Usage +## Usage 1. in your settings.py add the application to your installed apps ```python INSTALLED_APPS=( @@ -44,23 +44,29 @@ Currently, it support Django 2.0+, Python 3.7+ 4. Add the following settings to your file ```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 - FIDO_SERVER_NAME="TestApp" - import passkeys - KEY_ATTACHMENT = None | passkeys.Attachment.CROSS_PLATFORM | passkeys.Attachment.PLATFORM + import passkeys + + # Change your authentication backend + AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend'] + + # Server rp id for FIDO2, it the full domain of your project + FIDO_SERVER_ID="localhost" + + FIDO_SERVER_NAME="TestApp" + + 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 - + ```python urls_patterns= [ - '...', - url(r'^passkeys/', include('passkeys.urls')), - '....', + '...', + 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/passkeys_base.html` which is used by `passkeys/passkeys.html` so you can control the styling better and current `passkeys/passkeys_base.html` extends `base.html` @@ -77,13 +83,20 @@ Currently, it support Django 2.0+, Python 3.7+ * Give an id to your login form e.g 'loginForm', the id should be provided when calling `authn` function * Inside the form, add ```html - - - {%include 'passkeys/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 +## 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 @@ -95,20 +108,33 @@ If the user didn't use a passkey then it will be set to False {'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. ```html - - + ``` check_passkey function paramters are as follows @@ -121,7 +147,7 @@ check_passkey function paramters are as follows Conditional UI is a way for the browser to prompt the user to use the passkey to login to the system as shown in - + Starting version v1.2. you can use Conditional UI by adding the following to your login page @@ -132,9 +158,9 @@ Starting version v1.2. you can use Conditional UI by adding the following to you add the following to the page js. ```js -window.onload = checkConditionalUI('loginForm'); +window.onload = DjangoPasskeys.checkConditionalUI('login-form'); ``` -where `loginForm` is name of your login form. +where `login-form` is id of your login form. ## Security contact information diff --git a/example/test_app/settings.py b/example/test_app/settings.py index db758fb..29ce705 100644 --- a/example/test_app/settings.py +++ b/example/test_app/settings.py @@ -18,7 +18,6 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ @@ -30,7 +29,6 @@ ALLOWED_HOSTS = ['*'] - # Application definition INSTALLED_APPS = [ @@ -60,7 +58,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR ,'example','templates' )], + 'DIRS': [os.path.join(BASE_DIR, 'example', 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -75,7 +73,6 @@ WSGI_APPLICATION = 'test_app.wsgi.application' - # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases @@ -86,7 +83,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators @@ -105,7 +101,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ @@ -119,17 +114,24 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ STATIC_URL = '/static/' -#STATIC_ROOT=(os.path.join(BASE_DIR,'static')) -STATICFILES_DIRS=[os.path.join(BASE_DIR,'static')] -LOGIN_URL="/auth/login" +# STATIC_ROOT=(os.path.join(BASE_DIR,'static')) +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] +LOGIN_URL = "/auth/login" AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend'] -FIDO_SERVER_ID="localhost" # Server rp id for FIDO2, it the full domain of your project -FIDO_SERVER_NAME="TestApp" -KEY_ATTACHMENT = None # Set None to allow all authenticator attachment +# Server rp id for FIDO2, it the full domain of your project +FIDO_SERVER_ID = "localhost" + +FIDO_SERVER_NAME = "TestApp" + +# Set None to allow all authenticator attachment +KEY_ATTACHMENT = None + +CSRF_TRUSTED_ORIGINS = [ + "https://localhost" +] diff --git a/example/test_app/templates/home.html b/example/test_app/templates/home.html index f788751..0fca189 100644 --- a/example/test_app/templates/home.html +++ b/example/test_app/templates/home.html @@ -1,33 +1,47 @@ {% extends 'base.html' %} {% load static %} -{% block content %} -Are you sure you want to delete '${name}'?
+You may lose access to this system if this your only 2FA.
+ `; + + const button = document.createElement('button'); + button.setAttribute('class', 'btn btn-danger btn-action'); + button.onclick = () => keyDelete(id); + button.innerText = "Confirm"; + + showModal(title, body, button); + } + + /** + * Delete a passkey. + * + * @param id + */ + const keyDelete = function (id) { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + fetch(urlKeyDelete(), { + method: "post", + headers: { + "X-CSRFToken": csrfToken, + "Content-Type": "application/x-www-form-urlencoded" + }, + body: `id=${id}` + }) + .then(response => response.text()) + .then(data => { + const title = "Confirm delete"; + const body = 'The key has been deleted successfully.
' + updateModal(title, body); + }); + } + + + const makeCredReq = function (makeCredReq) { + makeCredReq.publicKey.challenge = base64url.decode(makeCredReq.publicKey.challenge); + makeCredReq.publicKey.user.id = base64url.decode(makeCredReq.publicKey.user.id); + + for (let excludeCred of makeCredReq.publicKey.excludeCredentials) { + excludeCred.id = base64url.decode(excludeCred.id); + } + + return makeCredReq + } + + /** + * Start passkey registration + */ + const beginReg = function () { + fetch(urlRegBegin(), {}).then(function (response) { + if (response.ok) { + return response.json().then(function (req) { + return makeCredReq(req) + }); + } + throw new Error('Error getting registration data!'); + }).then(function (options) { + //options.publicKey.attestation="direct" + return navigator.credentials.create(options); + }).then(function (attestation) { + attestation["key_name"] = document.getElementById("key_name").value; + return fetch(urlRegComplete(), { + method: 'POST', + body: JSON.stringify(publicKeyCredentialToJSON(attestation)) + } + ); + }).then(function (response) { + const stat = response.ok ? 'successful' : 'unsuccessful'; + return response.json() + }).then(function (res) { + if (res["status"] === 'OK') { + const title = "Register new passkey"; + const body = 'Passkey was successfully registered.
'; + updateModal(title, body); + } else { + const title = "Register new passkey"; + const body = `Passkey registration failed!
${res["message"]}
`; + updateModal(title, body); + } + }, function (reason) { + const title = "Register new passkey"; + const body = `Passkey registration failed!
${reason}
`; + updateModal(title, body); + }); + } + + /** + * Open passkey registration dialog. + */ + const registration = function () { + const title = "Register new passkey" + const body = ` +Please enter a name for your new token.
+ + `; + + const button = document.createElement('button'); + button.setAttribute('class', 'btn btn-primary btn-action'); + button.onclick = beginReg; + button.innerText = "Start"; + + showModal(title, body, button); + } + + /** + * Update and show the passkey modal. + * + * @param title + * @param body + * @param actionButton + */ + const showModal = function (title, body, actionButton) { + updateModal(title, body, actionButton); + const modal = new bootstrap.Modal('#django-passkeys-modal', {}); + modal.show(); + } + + /** + * Update the already visible passkey modal. + * + * @param title + * @param body + * @param actionButton + */ + const updateModal = function (title, body, actionButton) { + // update title + const titleElem = document.querySelector('#django-passkeys-modal .modal-title'); + titleElem.innerText = title; + + // update body + const bodyElem = document.querySelector('#django-passkeys-modal .modal-body'); + bodyElem.innerHTML = body; + + // remove existing action buttons + for (let button of document.querySelectorAll('#django-passkeys-modal .btn-action')) { + button.remove(); + } + + // insert action button + if (actionButton) { + const footer = document.querySelector('#django-passkeys-modal .modal-footer'); + footer.prepend(actionButton); + } + } + + // create the global DjangoPasskey variable + if (typeof window.DjangoPasskeys === 'undefined') { + window.DjangoPasskeys = { + 'init': init, + 'checkConditionalUI': checkConditionalUI, + 'authn': authn, + 'checkPasskeysSupport': checkPasskeysSupport, + 'keyToggle': keyToggle, + 'keyDeleteConfirm': keyDeleteConfirm, + 'registration': registration, + } + } +})(); \ No newline at end of file diff --git a/passkeys/templates/passkeys/check_passkeys.js b/passkeys/templates/passkeys/check_passkeys.js deleted file mode 100644 index 2241ddd..0000000 --- a/passkeys/templates/passkeys/check_passkeys.js +++ /dev/null @@ -1,33 +0,0 @@ -function check_passkey(platform_authenticator = true,success_func, fail_func) -{ - {% if request.session.passkey.cross_platform != False %} - if (platform_authenticator) - { - PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() - .then((available) => { - if (available) { - success_func(); - } - else{ - fail_func(); - } - }) - } - success_func(); - {% endif%} -} - -function check_passkeys(success_func, fail_func) -{ - PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() - .then((available) => { - if (available) { - success_func(); - } else { - fail_func() - } - }).catch((err) => { - // Something went wrong - console.error(err); - }); -} \ No newline at end of file diff --git a/passkeys/templates/passkeys/modal.html b/passkeys/templates/passkeys/modal.html index 2e43f02..ce481b1 100644 --- a/passkeys/templates/passkeys/modal.html +++ b/passkeys/templates/passkeys/modal.html @@ -1,18 +1,16 @@ -