Skip to content

Commit

Permalink
Merge pull request #7 from Qup42/oidc
Browse files Browse the repository at this point in the history
OIDC
  • Loading branch information
aritas1 authored Jan 3, 2023
2 parents f07a224 + 91c23ca commit eeb3dc4
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 195 deletions.
65 changes: 52 additions & 13 deletions README.md
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`
Expand All @@ -8,7 +11,7 @@

add to installed apps:

```
```python3
INSTALLED_APPS = [
# other apps

Expand All @@ -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',
Expand All @@ -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"
Expand All @@ -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]'
Expand All @@ -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',
Expand All @@ -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"

Expand Down Expand Up @@ -114,7 +135,7 @@ AUTHENTICATION_BACKENDS = (

## urls.py

```
```python3
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
Expand All @@ -125,7 +146,7 @@ urlpatterns = [

## first run
migrate your database
```
```bash
./manage.py migrate
```

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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):

Expand All @@ -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):

Expand Down
2 changes: 1 addition & 1 deletion janus/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = VERSION = (1, 3, 1)
__version__ = VERSION = (1, 3, 2)
6 changes: 2 additions & 4 deletions janus/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ApplicationExtension
from django.contrib.auth.admin import UserAdmin

from janus.oauth2.util import get_group_list


###################################################
Expand Down Expand Up @@ -54,15 +55,12 @@ def is_multipart(self):
def get_applications(self):
user = self.instance

from janus.views import ProfileView
pv = ProfileView()

ret = {}

applications = get_application_model().objects.all()

for application in applications:
ret[application.name] = pv.get_group_list(user, application)
ret[application.name] = get_group_list(user, application)

return ret

Expand Down
7 changes: 7 additions & 0 deletions janus/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@
ALLAUTH_JANUS_PROFILE_VIEW = getattr(settings, 'ALLAUTH_JANUS_PROFILE_VIEW', 'janus.views.ProfileView')

ALLAUTH_JANUS_ADMIN_CLASS = getattr(settings, 'ALLAUTH_JANUS_ADMIN_CLASS', 'janus.admin.JanusUserAdmin')

# Enable OIDC only if it is enabled in django-oauth-toolkit.
OAUTH_SETTINGS = getattr(settings, 'OAUTH2_PROVIDER', {})
JANUS_OIDC_ENABLED = OAUTH_SETTINGS.get('OIDC_ENABLED', False)

# janus supports some non-standard claims. Set which scope is required to return the claims.
JANUS_OIDC_SCOPE_EXTRA = getattr(settings, 'JANUS_OIDC_SCOPE_EXTRA', 'janus')
145 changes: 145 additions & 0 deletions janus/oauth2/util.py
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)
Loading

0 comments on commit eeb3dc4

Please sign in to comment.