Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
10
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
feudal
feudalBackend
Commits
a0aa6bfd
Commit
a0aa6bfd
authored
Dec 04, 2018
by
Lukas Burgey
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Rework auth backends
parent
c66fafaa
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
176 additions
and
143 deletions
+176
-143
example-config/home/feudal/config/django_settings.py
example-config/home/feudal/config/django_settings.py
+4
-3
feudal/backend/auth/v1/__init__.py
feudal/backend/auth/v1/__init__.py
+126
-0
feudal/backend/auth/v1/models/__init__.py
feudal/backend/auth/v1/models/__init__.py
+4
-86
feudal/backend/auth/v1/views/webpage.py
feudal/backend/auth/v1/views/webpage.py
+42
-54
No files found.
example-config/home/feudal/config/django_settings.py
View file @
a0aa6bfd
...
...
@@ -9,7 +9,7 @@ DEBUG = True
DEBUG_AUTH
=
False
DEBUG_PUBLISHING
=
False
DEBUG_CREDENTIALS
=
Tru
e
DEBUG_CREDENTIALS
=
Fals
e
ALLOWED_HOSTS
=
[
'hdf-portal-dev.data.kit.edu'
,
...
...
@@ -105,13 +105,14 @@ CORS_ORIGIN_ALLOW_ALL = True
# AUTHENTICATION AND AUTHORIZATION
AUTHENTICATION_BACKENDS
=
[
'feudal.backend.auth.v1.
models.
OIDCTokenAuthBackend'
,
'feudal.backend.auth.v1.OIDCTokenAuthBackend'
,
'django.contrib.auth.backends.ModelBackend'
,
]
REST_FRAMEWORK
=
{
'DEFAULT_AUTHENTICATION_CLASSES'
:
[
'feudal.backend.auth.v1.auth_class.CsrfExemptSessionAuthentication'
,
'feudal.backend.auth.v1.CsrfExemptSessionAuthentication'
,
'feudal.backend.auth.v1.OIDCTokenAuthHTTPBackend'
,
],
'DEFAULT_PERMISSION_CLASSES'
:
[
'rest_framework.permissions.IsAuthenticated'
,
...
...
feudal/backend/auth/v1/__init__.py
View file @
a0aa6bfd
import
logging
import
json
from
urllib.error
import
HTTPError
from
urllib.request
import
Request
,
urlopen
from
rest_framework.authentication
import
BaseAuthentication
,
SessionAuthentication
from
rest_framework.exceptions
import
AuthenticationFailed
from
.
import
utils
from
.models
import
OIDCConfig
LOGGER
=
logging
.
getLogger
(
__name__
)
class
OIDCTokenAuthBackend
:
def
get_userinfo
(
self
,
oidc_client
,
access_token
):
req
=
Request
(
oidc_client
.
provider_info
[
'userinfo_endpoint'
],
)
req
.
add_header
(
'Authorization'
,
access_token
)
# execute request
userinfo_bytes
=
urlopen
(
req
).
read
()
user_info
=
json
.
loads
(
userinfo_bytes
.
decode
(
'UTF-8'
))
user_info
[
'iss'
]
=
oidc_client
.
provider_info
[
'issuer'
]
return
user_info
# raises OIDCConfig.DoesNotExist if no idp can be determined
def
get_idp
(
self
,
request
):
# OPTION 1: issuer set in the 'X-Issuer' header
if
'HTTP_X_ISSUER'
in
request
.
META
:
return
OIDCConfig
.
objects
.
get
(
issuer_uri
=
request
.
META
[
'HTTP_X_ISSUER'
])
# OPTION 2: issuer set in users session (before redirecting to IdP)
idp_id
=
utils
.
get_session
(
request
,
'idp_id'
,
None
)
if
idp_id
is
not
None
:
try
:
return
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
except
OIDCConfig
.
DoesNotExist
:
pass
raise
OIDCConfig
.
DoesNotExist
(
'Unable to determine IdP'
)
def
get_user
(
self
,
user_id
):
from
feudal.backend.models.users
import
User
query
=
User
.
objects
.
filter
(
user_type
=
'oidcuser'
,
pk
=
user_id
,
)
if
query
.
exists
():
if
len
(
query
)
==
1
:
return
query
.
first
()
LOGGER
.
error
(
'OIDCTokenAuthBackend: query for user id %s: %s results'
,
user_id
,
len
(
query
),
)
return
None
LOGGER
.
error
(
'OIDCTokenAuthBackend: query for user id %s: no results'
,
user_id
,
)
return
None
def
authenticate
(
self
,
request
):
if
'HTTP_AUTHORIZATION'
not
in
request
.
META
:
raise
AuthenticationFailed
(
detail
=
'No token. Set the HTTP Authorization header to the value of your access token.'
,
)
access_token
=
request
.
META
[
'HTTP_AUTHORIZATION'
]
from
feudal.backend.models.users
import
User
try
:
idp
=
self
.
get_idp
(
request
)
except
OIDCConfig
.
DoesNotExist
:
raise
AuthenticationFailed
(
detail
=
'Unable to determine IdP. Set
\'
X-Issuer
\'
header to the issuer URI of your access token.'
,
)
# get the user info from the idp
try
:
userinfo
=
self
.
get_userinfo
(
idp
,
access_token
,
)
except
HTTPError
as
http_error
:
raise
AuthenticationFailed
(
detail
=
'Error retrieving user info: {}'
.
format
(
http_error
))
try
:
user
=
User
.
get_user
(
userinfo
,
idp
,
)
LOGGER
.
info
(
'Authenticated: %s'
,
user
)
return
user
except
User
.
DoesNotExist
:
raise
AuthenticationFailed
(
detail
=
'No such user'
)
except
Exception
as
exception
:
LOGGER
.
error
(
exception
)
raise
AuthenticationFailed
(
detail
=
'Error during authentication'
)
class
CsrfExemptSessionAuthentication
(
SessionAuthentication
):
def
enforce_csrf
(
self
,
request
):
# No CSRF enforcing
return
class
OIDCTokenAuthHTTPBackend
(
BaseAuthentication
):
def
authenticate_header
(
self
,
request
):
return
'Bearer realm="openidconnect"'
def
authenticate
(
self
,
request
):
return
(
OIDCTokenAuthBackend
().
authenticate
(
request
),
None
)
feudal/backend/auth/v1/models/__init__.py
View file @
a0aa6bfd
import
logging
import
json
from
urllib.request
import
Request
,
urlopen
import
re
from
django.db
import
models
as
db_models
from
django.core.exceptions
import
ImproperlyConfigured
from
django.db
import
models
as
db_models
from
django_mysql.models
import
JSONField
from
oic.oic
import
Client
from
oic.oic.message
import
RegistrationResponse
from
oic.utils.authn.client
import
CLIENT_AUTHN_METHOD
from
..
import
utils
LOGGER
=
logging
.
getLogger
(
__name__
)
BEARER_TOKEN_EXTRACTOR
=
re
.
compile
(
'^Bearer (.+)$'
)
OIDC_CLIENT
=
{}
...
...
@@ -106,85 +106,3 @@ def default_idp():
if
not
available_idps
.
exists
():
raise
ImproperlyConfigured
(
'No OIDCConfig available'
)
return
available_idps
.
first
()
class
OIDCTokenAuthBackend
:
IdPException
=
Exception
AuthException
=
Exception
(
'Unable to authenticate user'
)
def
get_userinfo
(
self
,
oidc_client
,
access_token
):
user_info
=
None
# do the user info request
req
=
Request
(
oidc_client
.
provider_info
[
'userinfo_endpoint'
],
)
auth
=
(
'Bearer '
+
access_token
)
req
.
add_header
(
'Authorization'
,
auth
)
userinfo_bytes
=
urlopen
(
req
).
read
()
user_info
=
json
.
loads
(
userinfo_bytes
.
decode
(
'UTF-8'
))
user_info
[
'iss'
]
=
oidc_client
.
provider_info
[
'issuer'
]
return
user_info
# raises OIDCConfig.DoesNotExist if no idp can be determined
def
get_idp
(
self
,
idp_id
=
None
,
issuer_uri
=
None
):
if
idp_id
is
None
and
issuer_uri
is
None
:
raise
OIDCConfig
.
DoesNotExist
(
'Need idp_id or issuer_uri to determine IdP'
)
if
issuer_uri
is
not
None
:
return
OIDCConfig
.
objects
.
get
(
issuer_uri
=
issuer_uri
)
return
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
def
authenticate
(
self
,
request
,
token
=
None
,
issuer_uri
=
None
):
if
token
is
None
:
LOGGER
.
error
(
'Cannot authenticate without access token'
)
return
None
idp_id
=
utils
.
get_session
(
request
,
'idp_id'
,
None
)
try
:
oidc_client
=
self
.
get_idp
(
idp_id
=
idp_id
,
issuer_uri
=
issuer_uri
)
# get the user info from the idp
userinfo
=
self
.
get_userinfo
(
oidc_client
,
token
,
)
from
feudal.backend.models.users
import
User
return
User
.
get_user
(
userinfo
,
oidc_client
,
)
except
OIDCConfig
.
DoesNotExist
:
LOGGER
.
error
(
'Unable to determine IdP for authentication'
)
return
None
def
get_user
(
self
,
user_id
):
from
feudal.backend.models.users
import
User
query
=
User
.
objects
.
filter
(
user_type
=
'oidcuser'
,
pk
=
user_id
,
)
if
query
.
exists
():
if
len
(
query
)
==
1
:
return
query
.
first
()
LOGGER
.
error
(
'OIDCTokenAuthBackend: query for user id %s: %s results'
,
user_id
,
len
(
query
),
)
return
None
LOGGER
.
error
(
'OIDCTokenAuthBackend: query for user id %s: no results'
,
user_id
,
)
return
None
feudal/backend/auth/v1/views/webpage.py
View file @
a0aa6bfd
...
...
@@ -13,8 +13,6 @@ from oic.oauth2.exception import HttpError
from
rest_framework
import
generics
,
views
from
rest_framework.permissions
import
AllowAny
from
feudal.backend.views.webpage
import
state_view_data
from
..
import
utils
from
..models
import
OIDCConfig
,
default_idp
from
..models.serializers
import
AuthInfoSerializer
...
...
@@ -24,10 +22,6 @@ LOGGER = logging.getLogger(__name__)
IDP_COOKIE_NAME
=
'idp_id'
class
AuthException
(
Exception
):
pass
def
select_oidc_config
(
request
):
issuer_uri_urlenc
=
request
.
GET
.
get
(
'idp'
,
None
)
idp_id
=
request
.
COOKIES
.
get
(
IDP_COOKIE_NAME
,
None
)
...
...
@@ -39,7 +33,7 @@ def select_oidc_config(request):
return
OIDCConfig
.
objects
.
get
(
issuer_uri
=
issuer_uri
)
# IdP selection using a cookie
el
if
idp_id
is
not
None
:
if
idp_id
is
not
None
:
return
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
return
default_idp
()
...
...
@@ -49,6 +43,15 @@ def select_oidc_config(request):
return
default_idp
()
class
AuthFlowException
(
Exception
):
pass
def
error_response
(
request
,
msg
=
'Server Error'
):
request
.
session
[
'error'
]
=
msg
return
redirect
(
'/'
)
class
Auth
(
View
):
def
get
(
self
,
request
):
try
:
...
...
@@ -67,8 +70,6 @@ class Auth(View):
state
,
)
LOGGER
.
debug
(
'Auth: redirecting %s to IdP %s'
,
state
,
oidc_config
)
# include query parameters in the redirect to the idp
if
len
(
list
(
request
.
GET
.
items
()))
>
1
:
urlparams
=
request
.
GET
.
copy
()
...
...
@@ -81,17 +82,16 @@ class Auth(View):
except
ImproperlyConfigured
:
LOGGER
.
error
(
'No OIDCConfig is not available'
)
# the error is deleted from the session when the state is delivered
request
.
session
[
'error'
]
=
'Server Error'
return
redirect
(
'/'
)
return
error_response
(
request
)
except
:
# the error is deleted from the session when the state is delivered
request
.
session
[
'error'
]
=
'Server Error'
return
redirect
(
'/'
)
except
Exception
as
exception
:
return
error_response
(
request
)
class
AuthCallback
(
View
):
permission_classes
=
(
AllowAny
,)
def
get
(
self
,
request
):
try
:
state
=
utils
.
get_session
(
request
,
'state'
,
None
)
...
...
@@ -102,9 +102,11 @@ class AuthCallback(View):
if
idp_id
is
None
:
LOGGER
.
error
(
"Session for %s does not contain an idp_id. Hence we don't now which idp authenticated the user"
,
state
)
oidc_config
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
oidc_client
=
oidc_config
.
oidc_client
LOGGER
.
debug
(
'AuthCallback: %s returned from IdP %s'
,
state
,
oidc_config
)
try
:
oidc_config
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
oidc_client
=
oidc_config
.
oidc_client
except
OIDCConfig
.
DoesNotExist
:
raise
AuthFlowException
(
'Unable to determine idp'
)
aresp
=
oidc_client
.
parse_response
(
AuthorizationResponse
,
...
...
@@ -112,63 +114,49 @@ class AuthCallback(View):
)
if
'error'
in
aresp
:
LOGGER
.
debug
(
'AuthCallback: error response: %s'
,
aresp
)
raise
AuthException
(
'Erroneous callback from IdP {}'
.
format
(
oidc_config
))
raise
Auth
Flow
Exception
(
'Erroneous callback from IdP {}'
.
format
(
oidc_config
))
if
not
state
==
aresp
[
'state'
]:
raise
AuthException
(
'AuthCallback: States do not match'
)
raise
AuthFlowException
(
'AuthCallback: States do not match'
)
if
'code'
not
in
aresp
:
raise
AuthFlowException
(
'AuthCallback: Did not receive a code'
)
ac_token_response
=
None
ac
cess
_token_response
=
None
try
:
ac_token_response
=
(
ac
cess
_token_response
=
(
oidc_client
.
do_access_token_request
(
state
=
state
,
request_args
=
{
'code'
:
aresp
[
'code'
]
},
request_args
=
aresp
,
)
)
except
HttpError
as
exception
:
# this exception is throw
LOGGER
.
error
(
'AuthCallback: Access Token Request: %s'
,
exception
)
# the error is deleted from the session when the state is delivered
request
.
session
[
'error'
]
=
'Server Communication Error'
return
redirect
(
'/'
)
return
error_response
(
request
,
msg
=
'Server communication error'
)
# TODO retry in the future
# pyoidc_userinfo = oidc_client.do_user_info_request(
# state=aresp["state"],
# )
# LOGGER.debug("pyoidc: %s", pyoidc_userinfo)
access_token
=
access_token_response
.
get
(
'access_token'
,
''
)
request
.
META
[
'HTTP_AUTHORIZATION'
]
=
'Bearer '
+
access_token
user
=
authenticate
(
request
,
token
=
ac_token_response
[
'access_token'
],
)
response
=
redirect
(
'/'
)
user
=
authenticate
(
request
)
if
user
is
None
:
# authentication failed -> "401"
LOGGER
.
error
(
'User failed to log in'
)
request
.
session
[
'error'
]
=
'Authentication failed'
elif
not
user
.
is_active
:
return
error_response
(
request
,
msg
=
'Authentication failed'
)
if
not
user
.
is_active
:
# user is deactivated -> "403"
LOGGER
.
info
(
'%s tried to log in'
,
user
)
request
.
session
[
'error'
]
=
'Account deactivated'
else
:
# user authenticated -> back to frontend
login
(
request
,
user
)
LOGGER
.
info
(
'IdP %s authenticated user as %s'
,
oidc_config
,
user
)
return
error_response
(
request
,
msg
=
'Account deactivated'
)
return
response
# user authenticated -> back to frontend
login
(
request
,
user
)
return
redirect
(
'/'
)
except
AuthException
as
exception
:
except
Auth
Flow
Exception
as
exception
:
LOGGER
.
error
(
'AuthCallback: %s'
,
exception
)
# the error is deleted from the session when the state is delivered
request
.
session
[
'error'
]
=
'Server Error'
return
redirect
(
'/'
)
return
error_response
(
request
)
class
LogoutView
(
views
.
APIView
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment