-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from Qup42/oidc
OIDC
- Loading branch information
Showing
9 changed files
with
293 additions
and
195 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
# setup | ||
|
||
django-janus is an [OAuth2](https://www.rfc-editor.org/rfc/rfc6749) authorization server. | ||
The [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) and [Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html) Standards are implemented. | ||
|
||
## installation | ||
|
||
`pip install git+https://github.com/smartlgt/django-janus#egg=django-janus` | ||
|
@@ -8,7 +11,7 @@ | |
|
||
add to installed apps: | ||
|
||
``` | ||
```python3 | ||
INSTALLED_APPS = [ | ||
# other apps | ||
|
||
|
@@ -27,17 +30,17 @@ INSTALLED_APPS = [ | |
``` | ||
|
||
Set a fix site ID or init the database table via manage commands: | ||
``` | ||
```python3 | ||
SITE_ID = 1 | ||
``` | ||
|
||
Oauth config: | ||
``` | ||
```python3 | ||
OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' | ||
``` | ||
|
||
cors for web apps: | ||
``` | ||
```python3 | ||
MIDDLEWARE = ( | ||
# ... | ||
'corsheaders.middleware.CorsMiddleware', | ||
|
@@ -54,7 +57,7 @@ CORS_URLS_REGEX = r"^/oauth2/.*$" | |
|
||
its possible to use any social login, reffer the allauth docs for configuration. | ||
Allauth config e.G.: | ||
``` | ||
```python3 | ||
ACCOUNT_EMAIL_REQUIRED = False | ||
ACCOUNT_UNIQUE_EMAIL = True | ||
ACCOUNT_EMAIL_VERIFICATION = "optional" | ||
|
@@ -65,7 +68,7 @@ ACCOUNT_LOGOUT_ON_GET = True | |
``` | ||
|
||
E-Mail config e.G.: | ||
``` | ||
```python3 | ||
EMAIL_USE_SSL = True | ||
EMAIL_HOST = 'smtp.example.com' | ||
EMAIL_HOST_USER = '[email protected]' | ||
|
@@ -76,7 +79,7 @@ DEFAULT_FROM_EMAIL = 'name <[email protected]>' | |
``` | ||
|
||
(recommended) cleanup old token | ||
``` | ||
```python3 | ||
CELERY_BEAT_SCHEDULE = { | ||
'cleanup_token': { | ||
'task': 'janus.tasks.cleanup_token', | ||
|
@@ -85,8 +88,26 @@ CELERY_BEAT_SCHEDULE = { | |
} | ||
``` | ||
|
||
(optional) setup your ldap server | ||
(optional) enable and configure OIDC | ||
<!-- TODO: the meaning of the custom claims should probably be documented in more detail. --> | ||
```python3 | ||
# Scope in which additional claims are included. These claims are is_staff, is_superuser and groups. | ||
JANUS_OIDC_SCOPE_EXTRA = "profile" | ||
OAUTH2_PROVIDER = { | ||
"OIDC_ENABLED": True, # Enable OIDC | ||
"OIDC_ISS_ENDPOINT": "[...]", | ||
"OAUTH2_VALIDATOR_CLASS": "janus.oauth2.validator.JanusOAuth2Validator", | ||
"OIDC_RSA_PRIVATE_KEY": "[...]", # Generate with `openssl genrsa -out oidc.key 4096` | ||
"SCOPES": { # Claims are returned based on granted scopes. See OIDC Core section 5.4. | ||
"openid": "Connect with your Account", | ||
"profile": "Access your Name and Username", | ||
"email": "Access your Mail-Address", | ||
} | ||
} | ||
``` | ||
|
||
(optional) setup your ldap server | ||
```python3 | ||
# The URL of the LDAP server. | ||
LDAP_AUTH_URL = "ldap.exmaple.com" | ||
|
||
|
@@ -114,7 +135,7 @@ AUTHENTICATION_BACKENDS = ( | |
|
||
## urls.py | ||
|
||
``` | ||
```python3 | ||
urlpatterns = [ | ||
path('admin/', admin.site.urls), | ||
path('accounts/', include('allauth.urls')), | ||
|
@@ -125,7 +146,7 @@ urlpatterns = [ | |
|
||
## first run | ||
migrate your database | ||
``` | ||
```bash | ||
./manage.py migrate | ||
``` | ||
|
||
|
@@ -134,7 +155,7 @@ if you open a browser and look at the index page you should see `Hello from janu | |
|
||
# usage | ||
|
||
## endpoints | ||
## OAuth2 endpoints | ||
### o/authorize/ | ||
OAuth2 authorize endpoint | ||
|
||
|
@@ -144,6 +165,9 @@ OAuth2 access token endpoint | |
### o/revoke_token/ | ||
OAuth2 revoke access or refresh tokens | ||
|
||
### o/introspect/ | ||
OAuth2 introspection endpoint. Requires `introspect` scope. | ||
|
||
### o/profile/ | ||
custom profile endpoint, returns user profile information as json: | ||
````json | ||
|
@@ -161,11 +185,12 @@ custom profile endpoint, returns user profile information as json: | |
```` | ||
|
||
#### extend profile response | ||
##### Old | ||
overwrite settings like this: | ||
`ALLAUTH_JANUS_PROFILE_VIEW = 'app.views.ProfileViewCustom'` | ||
|
||
add a new profie view class and customize as needed | ||
``` | ||
```python3 | ||
from janus.views import ProfileView | ||
class ProfileViewCustom(ProfileView): | ||
|
||
|
@@ -175,10 +200,24 @@ class ProfileViewCustom(ProfileView): | |
return data | ||
``` | ||
|
||
##### New (OIDC) | ||
Extend `JanusOAuth2Validator`. You can then modify the response by [adding additional information to the `UserInfo` service directly](https://django-oauth-toolkit.readthedocs.io/en/latest/oidc.html#adding-information-to-the-userinfo-service) or by [adding claims to the ID token](https://django-oauth-toolkit.readthedocs.io/en/latest/oidc.html#adding-claims-to-the-id-token). | ||
Then set the modified Validator as `OAUTH2_VALIDATOR_CLASS` in the `OAUTH2_PROVIDER` in the settings. See also the section on `enable and configure OIDC` for the configuration of the settings. | ||
|
||
## OIDC endpoints | ||
### `o/userinfo/` | ||
UserInfo endpoint as per section 5.3 of OpenID Connect Core 1.0. | ||
|
||
### `o/.well-known/openid-configuration/` | ||
OpenID Provider Configuration endpoint as per section 4 of OpenID Connect Discovery 1.0. | ||
|
||
### `o/.well-known/jwks.json` | ||
JSON Web Key Set endpoint as per section 3 of OpenID Connect Discovery 1.0. | ||
|
||
## admin custom user class | ||
set `ALLAUTH_JANUS_ADMIN_CLASS = 'app.admin_custom.CustomUserAdmin'` | ||
|
||
``` | ||
```python3 | ||
from janus.admin import JanusUserAdmin | ||
class CustomUserAdmin(JanusUserAdmin): | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = VERSION = (1, 3, 1) | ||
__version__ = VERSION = (1, 3, 2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
from janus.models import ProfileGroup, Profile, GroupPermission, ProfilePermission | ||
|
||
|
||
def get_profile_memberships(user): | ||
all_profiles = Profile.objects.get(user=user).group.all() | ||
|
||
# add the default groups by default | ||
default_profile = ProfileGroup.objects.filter(default=True) | ||
all_profiles = set(all_profiles | default_profile) | ||
|
||
return list(all_profiles) | ||
|
||
|
||
def get_group_permissions(user, application): | ||
""" | ||
Validates the group permissions for a user given a token | ||
:param user: | ||
:param token: | ||
:return: | ||
""" | ||
is_staff = False | ||
is_superuser = False | ||
can_authenticate = False | ||
|
||
if not user.is_authenticated: | ||
return can_authenticate, is_staff, is_superuser | ||
|
||
all_groups = get_profile_memberships(user) | ||
|
||
for g in all_groups: | ||
gp = GroupPermission.objects.filter(profile_group=g, application=application) | ||
if gp.count() == 0: | ||
continue | ||
elif gp.count() == 1: | ||
gp = gp.first() | ||
if gp.can_authenticate: | ||
can_authenticate = True | ||
if gp.is_staff: | ||
is_staff = True | ||
if gp.is_superuser: | ||
is_superuser = True | ||
else: | ||
print('We have a problem') | ||
return can_authenticate, is_staff, is_superuser | ||
|
||
|
||
def get_personal_permissions(user, application): | ||
""" | ||
Validates the personal permissions for a user given a token | ||
:param user: | ||
:param token: | ||
:return: | ||
""" | ||
is_staff = None | ||
is_superuser = None | ||
can_authenticate = None | ||
if not user.is_authenticated: | ||
return can_authenticate, is_staff, is_superuser | ||
|
||
pp = ProfilePermission.objects.filter(profile__user=user, application=application).first() | ||
if pp: | ||
is_staff = True if pp.is_staff else None | ||
is_superuser = True if pp.is_superuser else None | ||
can_authenticate = True if pp.can_authenticate else None | ||
return can_authenticate, is_staff, is_superuser | ||
|
||
|
||
def get_profile_group_memberships(user, application): | ||
""" | ||
collect group names form user profile group memberships | ||
:param user: | ||
:param token: | ||
:return: | ||
""" | ||
|
||
all_profiles = get_profile_memberships(user) | ||
|
||
group_list = set() | ||
|
||
for g in all_profiles: | ||
# get profile-group-permission object | ||
gp = GroupPermission.objects.filter(profile_group=g, application=application) | ||
for elem in gp: | ||
groups = elem.groups.all() | ||
|
||
for g in groups: | ||
# ensure only groups for this application can be returned | ||
if g.application == application: | ||
group_list.add(g.name) | ||
|
||
return group_list | ||
|
||
|
||
def get_profile_personal_memberships(user, application): | ||
""" | ||
collect group names form user profile permission | ||
:param user: | ||
:param token: | ||
:return: | ||
""" | ||
|
||
profile_permissions = ProfilePermission.objects.filter(profile__user=user, application=application).first() | ||
|
||
group_list = set() | ||
|
||
if profile_permissions: | ||
groups = profile_permissions.groups.all() | ||
|
||
for g in groups: | ||
# ensure only groups for this application can be returned | ||
if g.application == application: | ||
group_list.add(g.name) | ||
|
||
return group_list | ||
|
||
|
||
def get_permissions(user, application): | ||
""" | ||
return permissions according to application settings, personal overwrite and default values | ||
:param user: | ||
:param application: | ||
:return: | ||
""" | ||
can_authenticate, is_staff, is_superuser = get_group_permissions(user, application) | ||
|
||
# if set the personal settings overwrite the user settings | ||
pp_authenticate, pp_staff, pp_superuser = get_personal_permissions(user, application) | ||
if pp_staff is not None: | ||
is_staff = pp_staff | ||
|
||
if pp_superuser is not None: | ||
is_superuser = pp_superuser | ||
|
||
if pp_authenticate is not None: | ||
can_authenticate = pp_authenticate | ||
|
||
return can_authenticate, is_staff, is_superuser | ||
|
||
|
||
def get_group_list(user, application): | ||
groups = set() | ||
groups = groups.union(get_profile_group_memberships(user, application)) | ||
groups = groups.union(get_profile_personal_memberships(user, application)) | ||
|
||
return list(groups) |
Oops, something went wrong.