Skip to content

Commit f2cb000

Browse files
authored
SSO (#579)
* Add bearer auth token * Add TokenExpired exc * Enable TestKit tests for auth tokens.
1 parent 3ab2e6e commit f2cb000

File tree

7 files changed

+112
-26
lines changed

7 files changed

+112
-26
lines changed

docs/source/api.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Example:
116116
117117
import neo4j
118118
119-
auth = neo4j.Auth(scheme="basic", principal="neo4j", credentials="password")
119+
auth = neo4j.Auth("basic", "neo4j", "password")
120120
121121
122122
Auth Token Helper Functions
@@ -128,6 +128,8 @@ Alternatively, one of the auth token helper functions can be used.
128128

129129
.. autofunction:: neo4j.kerberos_auth
130130

131+
.. autofunction:: neo4j.bearer_auth
132+
131133
.. autofunction:: neo4j.custom_auth
132134

133135

neo4j/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"AuthToken",
3030
"basic_auth",
3131
"kerberos_auth",
32+
"bearer_auth",
3233
"custom_auth",
3334
"Bookmark",
3435
"ServerInfo",
@@ -69,6 +70,7 @@
6970
AuthToken,
7071
basic_auth,
7172
kerberos_auth,
73+
bearer_auth,
7274
custom_auth,
7375
Bookmark,
7476
ServerInfo,

neo4j/api.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,30 @@
6161

6262
# TODO: This class is not tested
6363
class Auth:
64-
""" Container for auth details.
64+
"""Container for auth details.
6565
66-
:param scheme: specifies the type of authentication, examples: "basic", "kerberos"
66+
:param scheme: specifies the type of authentication, examples: "basic",
67+
"kerberos"
6768
:type scheme: str
6869
:param principal: specifies who is being authenticated
69-
:type principal: str
70+
:type principal: str or None
7071
:param credentials: authenticates the principal
71-
:type credentials: str
72+
:type credentials: str or None
7273
:param realm: specifies the authentication provider
73-
:type realm: str
74-
:param parameters: extra key word parameters passed along to the authentication provider
75-
:type parameters: str
74+
:type realm: str or None
75+
:param parameters: extra key word parameters passed along to the
76+
authentication provider
77+
:type parameters: Dict[str, Any]
7678
"""
7779

78-
#: By default we should not send any realm
79-
realm = None
80-
8180
def __init__(self, scheme, principal, credentials, realm=None, **parameters):
8281
self.scheme = scheme
83-
self.principal = principal
84-
self.credentials = credentials
82+
# Neo4j servers pre 4.4 require the principal field to always be
83+
# present. Therefore, we transmit it even if it's an empty sting.
84+
if principal is not None:
85+
self.principal = principal
86+
if credentials:
87+
self.credentials = credentials
8588
if realm:
8689
self.realm = realm
8790
if parameters:
@@ -93,13 +96,16 @@ def __init__(self, scheme, principal, credentials, realm=None, **parameters):
9396

9497

9598
def basic_auth(user, password, realm=None):
96-
""" Generate a basic auth token for a given user and password.
99+
"""Generate a basic auth token for a given user and password.
97100
98101
This will set the scheme to "basic" for the auth token.
99102
100-
:param user: user name, this will set the principal
103+
:param user: user name, this will set the
104+
:type user: str
101105
:param password: current password, this will set the credentials
106+
:type password: str
102107
:param realm: specifies the authentication provider
108+
:type realm: str or None
103109
104110
:return: auth token for use with :meth:`GraphDatabase.driver`
105111
:rtype: :class:`neo4j.Auth`
@@ -108,26 +114,49 @@ def basic_auth(user, password, realm=None):
108114

109115

110116
def kerberos_auth(base64_encoded_ticket):
111-
""" Generate a kerberos auth token with the base64 encoded ticket
117+
"""Generate a kerberos auth token with the base64 encoded ticket.
112118
113119
This will set the scheme to "kerberos" for the auth token.
114120
115-
:param base64_encoded_ticket: a base64 encoded service ticket, this will set the credentials
121+
:param base64_encoded_ticket: a base64 encoded service ticket, this will set
122+
the credentials
123+
:type base64_encoded_ticket: str
116124
117125
:return: auth token for use with :meth:`GraphDatabase.driver`
118126
:rtype: :class:`neo4j.Auth`
119127
"""
120128
return Auth("kerberos", "", base64_encoded_ticket)
121129

122130

131+
def bearer_auth(base64_encoded_token):
132+
"""Generate an auth token for Single-Sign-On providers.
133+
134+
This will set the scheme to "bearer" for the auth token.
135+
136+
:param base64_encoded_token: a base64 encoded authentication token generated
137+
by a Single-Sign-On provider.
138+
:type base64_encoded_token: str
139+
140+
:return: auth token for use with :meth:`GraphDatabase.driver`
141+
:rtype: :class:`neo4j.Auth`
142+
"""
143+
return Auth("bearer", None, base64_encoded_token)
144+
145+
123146
def custom_auth(principal, credentials, realm, scheme, **parameters):
124-
""" Generate a custom auth token.
147+
"""Generate a custom auth token.
125148
126149
:param principal: specifies who is being authenticated
150+
:type principal: str or None
127151
:param credentials: authenticates the principal
152+
:type credentials: str or None
128153
:param realm: specifies the authentication provider
154+
:type realm: str or None
129155
:param scheme: specifies the type of authentication
130-
:param parameters: extra key word parameters passed along to the authentication provider
156+
:type scheme: str or None
157+
:param parameters: extra key word parameters passed along to the
158+
authentication provider
159+
:type parameters: Dict[str, Any]
131160
132161
:return: auth token for use with :meth:`GraphDatabase.driver`
133162
:rtype: :class:`neo4j.Auth`

neo4j/exceptions.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
+ CypherTypeError
3131
+ ConstraintError
3232
+ AuthError
33+
+ TokenExpired
3334
+ Forbidden
3435
+ DatabaseError
3536
+ TransientError
@@ -199,6 +200,12 @@ class AuthError(ClientError):
199200
"""
200201

201202

203+
class TokenExpired(AuthError):
204+
""" Raised when the authentication token has expired.
205+
206+
A new driver instance with a fresh authentication token needs to be created.
207+
"""
208+
202209
client_errors = {
203210

204211
# ConstraintError
@@ -228,8 +235,11 @@ class AuthError(ClientError):
228235
"Neo.ClientError.Security.AuthorizationFailed": AuthError,
229236
"Neo.ClientError.Security.Unauthorized": AuthError,
230237

238+
# TokenExpired
239+
"Neo.ClientError.Security.TokenExpired": TokenExpired,
240+
231241
# NotALeader
232-
"Neo.ClientError.Cluster.NotALeader": NotALeader
242+
"Neo.ClientError.Cluster.NotALeader": NotALeader,
233243
}
234244

235245
transient_errors = {

testkitbackend/requests.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,23 @@ def NewDriver(backend, data):
5454
data["authorizationToken"].mark_item_as_read_if_equals(
5555
"name", "AuthorizationToken"
5656
)
57-
auth = neo4j.Auth(
58-
auth_token["scheme"], auth_token["principal"],
59-
auth_token["credentials"], realm=auth_token["realm"])
60-
auth_token.mark_item_as_read_if_equals("ticket", "")
57+
scheme = auth_token["scheme"]
58+
if scheme == "basic":
59+
auth = neo4j.basic_auth(
60+
auth_token["principal"], auth_token["credentials"],
61+
realm=auth_token.get("realm", None)
62+
)
63+
elif scheme == "kerberos":
64+
auth = neo4j.kerberos_auth(auth_token["credentials"])
65+
elif scheme == "bearer":
66+
auth = neo4j.bearer_auth(auth_token["credentials"])
67+
else:
68+
auth = neo4j.custom_auth(
69+
auth_token["principal"], auth_token["credentials"],
70+
auth_token["realm"], auth_token["scheme"],
71+
**auth_token.get("parameters", {})
72+
)
73+
auth_token.mark_item_as_read("parameters", recursive=True)
6174
resolver = None
6275
if data["resolverRegistered"] or data["domainNameResolverRegistered"]:
6376
resolver = resolution_func(backend, data["resolverRegistered"],

testkitbackend/test_config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
"Flaky: test requires the driver to contact servers in a specific order",
3333
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_retry_on_auth_expired_on_begin_using_tx_function":
3434
"Flaky: test requires the driver to contact servers in a specific order",
35+
"stub.authorization.test_authorization.TestAuthorizationV4x3.test_should_fail_on_token_expired_on_begin_using_tx_function":
36+
"Flaky: test requires the driver to contact servers in a specific order",
37+
"stub.authorization.test_authorization.TestAuthorizationV3.test_should_fail_on_token_expired_on_begin_using_tx_function":
38+
"Flaky: test requires the driver to contact servers in a specific order",
39+
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_fail_on_token_expired_on_begin_using_tx_function":
40+
"Flaky: test requires the driver to contact servers in a specific order",
3541
"stub.session_run_parameters.test_session_run_parameters.TestSessionRunParameters.test_empty_query":
3642
"Driver rejects empty queries before sending it to the server",
3743
"tls.tlsversions.TestTlsVersions.test_1_1":
@@ -40,6 +46,9 @@
4046
"features": {
4147
"Feature:API:Result.Single": "Does not raise error when not exactly one record is available. To be fixed in 5.0",
4248
"Feature:API:Result.Peek": true,
49+
"Feature:Auth:Bearer": true,
50+
"Feature:Auth:Custom": true,
51+
"Feature:Auth:Kerberos": true,
4352
"AuthorizationExpiredTreatment": true,
4453
"Optimization:ImplicitDefaultArguments": true,
4554
"Optimization:MinimalResets": true,

tests/unit/test_security.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from neo4j.api import (
2323
kerberos_auth,
2424
basic_auth,
25+
bearer_auth,
2526
custom_auth,
2627
)
2728

@@ -33,7 +34,18 @@ def test_should_generate_kerberos_auth_token_correctly():
3334
assert auth.scheme == "kerberos"
3435
assert auth.principal == ""
3536
assert auth.credentials == "I am a base64 service ticket"
36-
assert not auth.realm
37+
assert not hasattr(auth, "ticket")
38+
assert not hasattr(auth, "realm")
39+
assert not hasattr(auth, "parameters")
40+
41+
42+
def test_should_generate_bearer_auth_token_correctly():
43+
auth = bearer_auth("I am a base64 SSO ticket")
44+
assert auth.scheme == "bearer"
45+
assert auth.credentials == "I am a base64 SSO ticket"
46+
assert not hasattr(auth, "principal")
47+
assert not hasattr(auth, "ticket")
48+
assert not hasattr(auth, "realm")
3749
assert not hasattr(auth, "parameters")
3850

3951

@@ -42,7 +54,7 @@ def test_should_generate_basic_auth_without_realm_correctly():
4254
assert auth.scheme == "basic"
4355
assert auth.principal == "molly"
4456
assert auth.credentials == "meoooow"
45-
assert not auth.realm
57+
assert not hasattr(auth, "realm")
4658
assert not hasattr(auth, "parameters")
4759

4860

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

5769

70+
def test_should_generate_base_auth_with_keyword_realm_correctly():
71+
auth = basic_auth("molly", "meoooow", realm="cat_cafe")
72+
assert auth.scheme == "basic"
73+
assert auth.principal == "molly"
74+
assert auth.credentials == "meoooow"
75+
assert auth.realm == "cat_cafe"
76+
assert not hasattr(auth, "parameters")
77+
78+
5879
def test_should_generate_custom_auth_correctly():
5980
auth = custom_auth("molly", "meoooow", "cat_cafe", "cat", age="1", color="white")
6081
assert auth.scheme == "cat"

0 commit comments

Comments
 (0)