Skip to content

Add bearer auth token for SSO #579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Example:

import neo4j

auth = neo4j.Auth(scheme="basic", principal="neo4j", credentials="password")
auth = neo4j.Auth("basic", "neo4j", "password")


Auth Token Helper Functions
Expand All @@ -128,6 +128,8 @@ Alternatively, one of the auth token helper functions can be used.

.. autofunction:: neo4j.kerberos_auth

.. autofunction:: neo4j.bearer_auth

.. autofunction:: neo4j.custom_auth


Expand Down
2 changes: 2 additions & 0 deletions neo4j/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"AuthToken",
"basic_auth",
"kerberos_auth",
"bearer_auth",
"custom_auth",
"Bookmark",
"ServerInfo",
Expand Down Expand Up @@ -69,6 +70,7 @@
AuthToken,
basic_auth,
kerberos_auth,
bearer_auth,
custom_auth,
Bookmark,
ServerInfo,
Expand Down
65 changes: 47 additions & 18 deletions neo4j/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,30 @@

# TODO: This class is not tested
class Auth:
""" Container for auth details.
"""Container for auth details.

:param scheme: specifies the type of authentication, examples: "basic", "kerberos"
:param scheme: specifies the type of authentication, examples: "basic",
"kerberos"
:type scheme: str
:param principal: specifies who is being authenticated
:type principal: str
:type principal: str or None
:param credentials: authenticates the principal
:type credentials: str
:type credentials: str or None
:param realm: specifies the authentication provider
:type realm: str
:param parameters: extra key word parameters passed along to the authentication provider
:type parameters: str
:type realm: str or None
:param parameters: extra key word parameters passed along to the
authentication provider
:type parameters: Dict[str, Any]
"""

#: By default we should not send any realm
realm = None

def __init__(self, scheme, principal, credentials, realm=None, **parameters):
self.scheme = scheme
self.principal = principal
self.credentials = credentials
# Neo4j servers pre 4.4 require the principal field to always be
# present. Therefore, we transmit it even if it's an empty sting.
if principal is not None:
self.principal = principal
if credentials:
self.credentials = credentials
if realm:
self.realm = realm
if parameters:
Expand All @@ -93,13 +96,16 @@ def __init__(self, scheme, principal, credentials, realm=None, **parameters):


def basic_auth(user, password, realm=None):
""" Generate a basic auth token for a given user and password.
"""Generate a basic auth token for a given user and password.

This will set the scheme to "basic" for the auth token.

:param user: user name, this will set the principal
:param user: user name, this will set the
:type user: str
:param password: current password, this will set the credentials
:type password: str
:param realm: specifies the authentication provider
:type realm: str or None

:return: auth token for use with :meth:`GraphDatabase.driver`
:rtype: :class:`neo4j.Auth`
Expand All @@ -108,26 +114,49 @@ def basic_auth(user, password, realm=None):


def kerberos_auth(base64_encoded_ticket):
""" Generate a kerberos auth token with the base64 encoded ticket
"""Generate a kerberos auth token with the base64 encoded ticket.

This will set the scheme to "kerberos" for the auth token.

:param base64_encoded_ticket: a base64 encoded service ticket, this will set the credentials
:param base64_encoded_ticket: a base64 encoded service ticket, this will set
the credentials
:type base64_encoded_ticket: str

:return: auth token for use with :meth:`GraphDatabase.driver`
:rtype: :class:`neo4j.Auth`
"""
return Auth("kerberos", "", base64_encoded_ticket)


def bearer_auth(base64_encoded_token):
"""Generate an auth token for Single-Sign-On providers.

This will set the scheme to "bearer" for the auth token.

:param base64_encoded_token: a base64 encoded authentication token generated
by a Single-Sign-On provider.
:type base64_encoded_token: str

:return: auth token for use with :meth:`GraphDatabase.driver`
:rtype: :class:`neo4j.Auth`
"""
return Auth("bearer", None, base64_encoded_token)


def custom_auth(principal, credentials, realm, scheme, **parameters):
""" Generate a custom auth token.
"""Generate a custom auth token.

:param principal: specifies who is being authenticated
:type principal: str or None
:param credentials: authenticates the principal
:type credentials: str or None
:param realm: specifies the authentication provider
:type realm: str or None
:param scheme: specifies the type of authentication
:param parameters: extra key word parameters passed along to the authentication provider
:type scheme: str or None
:param parameters: extra key word parameters passed along to the
authentication provider
:type parameters: Dict[str, Any]

:return: auth token for use with :meth:`GraphDatabase.driver`
:rtype: :class:`neo4j.Auth`
Expand Down
12 changes: 11 additions & 1 deletion neo4j/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
+ CypherTypeError
+ ConstraintError
+ AuthError
+ TokenExpired
+ Forbidden
+ DatabaseError
+ TransientError
Expand Down Expand Up @@ -199,6 +200,12 @@ class AuthError(ClientError):
"""


class TokenExpired(AuthError):
""" Raised when the authentication token has expired.

A new driver instance with a fresh authentication token needs to be created.
"""

client_errors = {

# ConstraintError
Expand Down Expand Up @@ -228,8 +235,11 @@ class AuthError(ClientError):
"Neo.ClientError.Security.AuthorizationFailed": AuthError,
"Neo.ClientError.Security.Unauthorized": AuthError,

# TokenExpired
"Neo.ClientError.Security.TokenExpired": TokenExpired,

# NotALeader
"Neo.ClientError.Cluster.NotALeader": NotALeader
"Neo.ClientError.Cluster.NotALeader": NotALeader,
}

transient_errors = {
Expand Down
21 changes: 17 additions & 4 deletions testkitbackend/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,23 @@ def NewDriver(backend, data):
data["authorizationToken"].mark_item_as_read_if_equals(
"name", "AuthorizationToken"
)
auth = neo4j.Auth(
auth_token["scheme"], auth_token["principal"],
auth_token["credentials"], realm=auth_token["realm"])
auth_token.mark_item_as_read_if_equals("ticket", "")
scheme = auth_token["scheme"]
if scheme == "basic":
auth = neo4j.basic_auth(
auth_token["principal"], auth_token["credentials"],
realm=auth_token.get("realm", None)
)
elif scheme == "kerberos":
auth = neo4j.kerberos_auth(auth_token["credentials"])
elif scheme == "bearer":
auth = neo4j.bearer_auth(auth_token["credentials"])
else:
auth = neo4j.custom_auth(
auth_token["principal"], auth_token["credentials"],
auth_token["realm"], auth_token["scheme"],
**auth_token.get("parameters", {})
)
auth_token.mark_item_as_read("parameters", recursive=True)
resolver = None
if data["resolverRegistered"] or data["domainNameResolverRegistered"]:
resolver = resolution_func(backend, data["resolverRegistered"],
Expand Down
9 changes: 9 additions & 0 deletions testkitbackend/test_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
"Flaky: test requires the driver to contact servers in a specific order",
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_retry_on_auth_expired_on_begin_using_tx_function":
"Flaky: test requires the driver to contact servers in a specific order",
"stub.authorization.test_authorization.TestAuthorizationV4x3.test_should_fail_on_token_expired_on_begin_using_tx_function":
"Flaky: test requires the driver to contact servers in a specific order",
"stub.authorization.test_authorization.TestAuthorizationV3.test_should_fail_on_token_expired_on_begin_using_tx_function":
"Flaky: test requires the driver to contact servers in a specific order",
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_fail_on_token_expired_on_begin_using_tx_function":
"Flaky: test requires the driver to contact servers in a specific order",
"stub.session_run_parameters.test_session_run_parameters.TestSessionRunParameters.test_empty_query":
"Driver rejects empty queries before sending it to the server",
"tls.tlsversions.TestTlsVersions.test_1_1":
Expand All @@ -40,6 +46,9 @@
"features": {
"Feature:API:Result.Single": "Does not raise error when not exactly one record is available. To be fixed in 5.0",
"Feature:API:Result.Peek": true,
"Feature:Auth:Bearer": true,
"Feature:Auth:Custom": true,
"Feature:Auth:Kerberos": true,
"AuthorizationExpiredTreatment": true,
"Optimization:ImplicitDefaultArguments": true,
"Optimization:MinimalResets": true,
Expand Down
25 changes: 23 additions & 2 deletions tests/unit/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from neo4j.api import (
kerberos_auth,
basic_auth,
bearer_auth,
custom_auth,
)

Expand All @@ -33,7 +34,18 @@ def test_should_generate_kerberos_auth_token_correctly():
assert auth.scheme == "kerberos"
assert auth.principal == ""
assert auth.credentials == "I am a base64 service ticket"
assert not auth.realm
assert not hasattr(auth, "ticket")
assert not hasattr(auth, "realm")
assert not hasattr(auth, "parameters")


def test_should_generate_bearer_auth_token_correctly():
auth = bearer_auth("I am a base64 SSO ticket")
assert auth.scheme == "bearer"
assert auth.credentials == "I am a base64 SSO ticket"
assert not hasattr(auth, "principal")
assert not hasattr(auth, "ticket")
assert not hasattr(auth, "realm")
assert not hasattr(auth, "parameters")


Expand All @@ -42,7 +54,7 @@ def test_should_generate_basic_auth_without_realm_correctly():
assert auth.scheme == "basic"
assert auth.principal == "molly"
assert auth.credentials == "meoooow"
assert not auth.realm
assert not hasattr(auth, "realm")
assert not hasattr(auth, "parameters")


Expand All @@ -55,6 +67,15 @@ def test_should_generate_base_auth_with_realm_correctly():
assert not hasattr(auth, "parameters")


def test_should_generate_base_auth_with_keyword_realm_correctly():
auth = basic_auth("molly", "meoooow", realm="cat_cafe")
assert auth.scheme == "basic"
assert auth.principal == "molly"
assert auth.credentials == "meoooow"
assert auth.realm == "cat_cafe"
assert not hasattr(auth, "parameters")


def test_should_generate_custom_auth_correctly():
auth = custom_auth("molly", "meoooow", "cat_cafe", "cat", age="1", color="white")
assert auth.scheme == "cat"
Expand Down