This commit is contained in:
Ivan Nikolskiy 2025-06-10 02:57:33 +02:00
commit 9919e5d5ad
22 changed files with 1055 additions and 0 deletions

21
LICENSE Executable file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
MANIFEST.in Executable file
View File

@ -0,0 +1,5 @@
include README.md
include LICENSE
recursive-include django_hopid/static *
recursive-include django_hopid/templates *
recursive-include django_hopid/templatetags *.py

184
README.md Executable file
View File

@ -0,0 +1,184 @@
# HopID Client for Django
**django-hopid** is a lightweight Django library that simplifies OpenID Connect (OIDC) authentication with an authorization server powered by `django-oauth-toolkit`. It offers decorators, utility functions, URLs, and template tags to streamline secure login integration.
---
## 📚 Table of Contents
* [✅ Features](#✅-features)
* [🛠️ Installation](#🛠️-installation)
* [🚀 Quick Start](#🚀-quick-start)
* [1. Configure settings](#1-configure-settings)
* [2. Include URLs](#2-include-urls)
* [3. Use the callback decorator](#3-use-the-callback-decorator)
* [4. Use the login button tag](#4-use-the-login-button-tag)
* [⚙️ Advanced Usage](#⚙️-advanced-usage)
* [Custom callback response handling](#custom-callback-response-handling)
* [Use login URL in views](#use-login-url-in-views)
* [Logout integration](#logout-integration)
* [📄 License](#📄-license)
---
## ✅ Features
* 🧷 OpenID Connect Authorization Code flow with PKCE and nonce support
* 🔐 Secure token exchange and ID token verification (including issuer, nonce, and access token hash checks)
* 🤝 Seamless user login or creation using the access token
* 🌈 Template tag for styled login button
* ⚖️ Built-in URL routes for callback and logout
* ✨ Minimal configuration, no third-party dependencies
---
## 🛠️ Installation
```bash
pip install django-hopid
```
Add to `INSTALLED_APPS` (for templates):
```python
INSTALLED_APPS = [
...,
'django_hopid',
]
```
Ensure `request` is in template context:
```python
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
...,
'django.template.context_processors.request',
],
},
},
]
```
---
## 🚀 Quick Start
### 1. Configure settings
Add the following to your `settings.py`:
```python
HOPID_URL = "https://auth.example.com"
HOPID_CLIENT_ID = "your-client-id"
HOPID_CLIENT_SECRET = "your-client-secret"
HOPID_CLIENT_URI = "https://yourapp.example.com"
```
### 2. Include URLs
In your app's `urls.py`, include HopID routes:
```python
from django.urls import path, include
urlpatterns = [
path("auth/", include("django_hopid.urls")),
]
```
This provides:
* `/auth/callback/` for OIDC callback
* `/auth/logout/` for logout
### 3. Use the callback decorator
Create your callback view:
```python
from django.shortcuts import render
from django_hopid.decorators import hopid_callback
@hopid_callback()
def hopid_callback_view(request, *args, **kwargs):
user = kwargs.get("user")
return render(request, "home.html", {"user": user})
```
### 4. Use the login button tag
In any template:
```django
{% load hopid_tags %}
{% hopid_login_button %}
```
This renders a styled login button based on your template:
```html
<a href="https://auth.example.com/o/authorize/?..." class="hopid-login-button">
Login with HopID
</a>
```
---
## ⚙️ Advanced Usage
### Custom callback response handling
Pass a custom `response=` to the decorator to override error handling:
```python
@hopid_callback(response=my_error_view)
def hopid_callback_view(request, user=None, error=None):
if error:
return render(request, "error.html", {"error": error})
return render(request, "home.html", {"user": user})
```
### Use login URL in views
If you want to build your own login redirect view:
```python
from django.shortcuts import redirect
from django_hopid.utils import get_hopid_login_url
def login_view(request):
return redirect(get_hopid_login_url(request))
```
### Logout integration
Logout is handled via:
```python
from django.contrib.auth import logout as django_logout
from django.shortcuts import redirect
from django_hopid.utils import get_hopid_logout_url
def logout_view(request):
django_logout(request)
return redirect(get_hopid_logout_url(request))
```
Or use the built-in route `/auth/logout/`.
---
## 📄 License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
---
## 🤝 Contributing
Contributions are welcome! Open issues or pull requests on [Git](https://git.hopsenn.com/hopsenn/django-hopid).

23
django_hopid/__init__.py Executable file
View File

@ -0,0 +1,23 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

76
django_hopid/decorators.py Executable file
View File

@ -0,0 +1,76 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from functools import wraps
from django.contrib.auth import login as auth_login
from . import settings
from .validators import verify_id_token
from .utils import get_jwt_tokens, get_user_from_token
def hopid_callback(response=None):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
def fail(reason):
if callable(response):
return response(request, reason)
return view_func(request, *args, **kwargs, error=reason)
code = request.GET.get('code')
if not code:
return fail("No code returned")
tokens = get_jwt_tokens(code, request.session.pop('pkce_verifier', ''))
error = tokens.get('error', '')
if error:
return fail(error)
access_token = tokens.get('access_token')
id_token = tokens.get('id_token')
if not access_token or not id_token:
return fail("No ID token returned")
claims = verify_id_token(id_token, access_token)
if not claims:
return fail("Invalid ID token")
expected_nonce = request.session.pop('oidc_nonce', None)
if not claims or claims.get("nonce") != expected_nonce:
return fail("Invalid or missing nonce")
profile = get_user_from_token(access_token)
if not profile:
return fail("User info request failed")
auth_login(request, profile)
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator

35
django_hopid/settings.py Executable file
View File

@ -0,0 +1,35 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from django.conf import settings
def get(key, default=None):
return getattr(settings, key, default)
HOPID_URL = get('HOPID_URL', 'https://id.hopsenn.com')
HOPID_CLIENT_ID = get('HOPID_CLIENT_ID', '')
HOPID_CLIENT_SECRET = get('HOPID_CLIENT_SECRET', '')
HOPID_CLIENT_URI = get('HOPID_CLIENT_URI', '')
HOPID_CLIENT_LOGOUT_URI = get('HOPID_CLIENT_LOGOUT_URI', '')
HOPID_CLIENT_CALLBACK_URI = get('HOPID_CLIENT_CALLBACK_URI', '')

View File

@ -0,0 +1,3 @@
<a href="{{ url }}" class="hopid-login-button" style="display:inline-block;padding:10px 20px;border-radius:8px;background:#1e88e5;color:white;text-decoration:none;font-weight:500;">
Login with HopID
</a>

View File

@ -0,0 +1,23 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

View File

@ -0,0 +1,43 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from django import template
from django_hopid.utils import get_hopid_login_url
from django.template.loader import render_to_string
register = template.Library()
@register.simple_tag(takes_context=True)
def hopid_login_url(context):
request = context['request']
return get_hopid_login_url(request)
@register.simple_tag(takes_context=True)
def hopid_login_button(context):
request = context['request']
url = get_hopid_login_url(request)
return render_to_string('django_hopid/hopid_login_button.html', {'url': url})

9
django_hopid/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from django_hopid.views import hopid_logout_view, hopid_callback_view
app_name = "django_hopid"
urlpatterns = [
path("callback/", hopid_callback_view, name="callback"),
path("logout/", hopid_logout_view, name="logout"),
]

113
django_hopid/utils.py Normal file
View File

@ -0,0 +1,113 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import hashlib
import base64
import secrets
import requests
from . import settings
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
def get_hopid_logout_url():
next_url = settings.HOPID_CLIENT_LOGOUT_URI or settings.HOPID_CLIENT_URI
query = urlencode({"next": next_url})
return f"{settings.HOPID_URL}/users/logout/?{query}"
def get_user_from_token(access_token):
userinfo_url = f"{settings.HOPID_URL}/users/userinfo/"
headers = {'Authorization': f'Bearer {access_token}'}
try:
userinfo_response = requests.get(userinfo_url, headers=headers, timeout=5)
except requests.RequestException:
return None
if userinfo_response.status_code != 200:
return None
userinfo = userinfo_response.json()
email = userinfo.get('email')
username = userinfo.get('username')
if not email or not username:
return None
profile, _ = get_user_model().objects.get_or_create(email=email, defaults={'username': username.lower()})
return profile
def get_jwt_tokens(code, code_verifier):
token_url = f"{settings.HOPID_URL}/o/token/"
token_data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': settings.HOPID_CLIENT_CALLBACK_URI or settings.HOPID_CLIENT_URI + '/id/callback/',
'client_id': settings.HOPID_CLIENT_ID,
'client_secret': settings.HOPID_CLIENT_SECRET,
'code_verifier': code_verifier,
}
try:
token_response = requests.post(token_url, data=token_data, timeout=5)
except requests.RequestException:
return {"error": "Token request failed"}
if token_response.status_code != 200:
return {"error": "Token exchange failed"}
tokens = token_response.json()
return tokens
def generate_pkce_pair():
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b'=').decode()
return verifier, challenge
def get_hopid_login_url(request):
nonce = secrets.token_urlsafe(32)
verifier, challenge = generate_pkce_pair()
request.session['pkce_verifier'] = verifier
request.session['oidc_nonce'] = nonce
base = f"{settings.HOPID_URL}/o/authorize/"
params = {
"client_id": settings.HOPID_CLIENT_ID,
"response_type": "code",
"scope": "openid profile email",
"redirect_uri": settings.HOPID_CLIENT_CALLBACK_URI or settings.HOPID_CLIENT_URI + '/id/callback/',
"nonce": nonce,
"code_challenge": challenge,
"code_challenge_method": "S256"
}
return f"{base}?{urlencode(params)}"

View File

@ -0,0 +1,56 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import requests
from jose import jwt, JWTError
from . import settings
def verify_id_token(id_token, access_token):
jwks = requests.get(f"{settings.HOPID_URL}/.well-known/jwks.json").json()
try:
# Get unverified header to find 'kid'
unverified_header = jwt.get_unverified_header(id_token)
kid = unverified_header['kid']
# Find the matching key
key = next(k for k in jwks['keys'] if k['kid'] == kid)
issuer = settings.HOPID_URL.rstrip('/')
# Decode & verify
claims = jwt.decode(
id_token,
key=key,
algorithms=['RS256'],
audience=settings.HOPID_CLIENT_ID,
issuer=issuer,
access_token=access_token
)
return claims # Validated claims
except JWTError as e:
print("Invalid id_token:", e)

13
django_hopid/views.py Normal file
View File

@ -0,0 +1,13 @@
from django.shortcuts import redirect
from django.contrib.auth import logout as django_logout
from .utils import get_hopid_logout_url
from .decorators import hopid_callback
@hopid_callback()
def hopid_callback_view(request, *args, **kwargs):
return redirect("/")
def hopid_logout_view(request):
django_logout(request)
return redirect(get_hopid_logout_url())

View File

@ -0,0 +1,23 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

31
example/demoapp/demoapp/asgi.py Executable file
View File

@ -0,0 +1,31 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoapp.settings')
application = get_asgi_application()

View File

@ -0,0 +1,141 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-jv__th8%4!0ite+vhv%c@(q$1=n8xwg(46@k3zbz7^oeq-+)p&'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_hopid'
]
HOPID_URL = 'http://localhost:8000'
HOPID_CLIENT_ID = 'XUFHPoHC9bKF8Vf93srxqfmyjtXCSwx3KsQdsNDP'
HOPID_CLIENT_SECRET = 'ipzAGxVsdh9nt5rmYXHcNpkIw8XzYOd6WyVApWraMD0ehclLzN7F94kEcUYIweSVsrWgWMOu0Qp8oqqWUpPIhqOxTyIH6qFmaE14KBwWSInx1H3tlVQWLdrSpjmJhEru'
HOPID_CLIENT_URI = 'http://localhost:8001'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'demoapp.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'demoapp'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'demoapp.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -0,0 +1,63 @@
{% load static hopid_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HopID Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
}
h2 { color: #333; }
nav {
background-color: #222;
padding: 10px;
}
nav a {
color: white;
text-decoration: none;
margin: 0 10px;
}
form {
background: white;
padding: 15px;
border-radius: 8px;
max-width: 400px;
}
.my-button, input[type="submit"] {
padding: 10px 20px;
background-color: #333;
color: white;
border: none;
cursor: pointer;
text-decoration: none;
border-radius: 8px;
display: inline-block;
font-weight: bold;
}
.my-button:hover, input[type="submit"]:hover {
background-color: #555;
}
input[type="text"], input[type="password"], input[type="date"], select {
width: 100%;
padding: 8px;
margin: 6px 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<h1>HopID Demo</h1>
{% if request.user.is_authenticated %}
<p>Hello, {{ request.user.username }}!</p>
<a href="/id/logout" class="my-button">Logout</a>
{% else %}
<a href="{% hopid_login_url %}" class="my-button">Login with HopID</a>
{% endif %}
</body>
</html>

39
example/demoapp/demoapp/urls.py Executable file
View File

@ -0,0 +1,39 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.index, name='index'),
path("id/", include("django_hopid.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -0,0 +1,28 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from django.shortcuts import render
def index(request):
return render(request, "templates/index.html")

31
example/demoapp/demoapp/wsgi.py Executable file
View File

@ -0,0 +1,31 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoapp.settings')
application = get_wsgi_application()

44
example/demoapp/manage.py Executable file
View File

@ -0,0 +1,44 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demoapp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

51
setup.py Executable file
View File

@ -0,0 +1,51 @@
"""
MIT License
Copyright (c) 2025 Hopsenn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from setuptools import setup, find_packages
setup(
name='django-hopid',
version='1.0.0',
description='',
author='Hopsenn',
author_email='ivan.nikolskiy@hopsenn.com',
url='https://git.hopsenn.com/hopsenn/django-hopid',
packages=find_packages(),
include_package_data=True,
install_requires=[
'requests>=2.20',
'Django>=3.2',
'python-jose',
'requests'
],
classifiers=[
'Development Status :: 4 - Beta',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
],
python_requires='>=3.6',
)