Skip to content

Commit 29306d9

Browse files
committed
Add bearer auth token for SSO
1 parent 752bcd3 commit 29306d9

File tree

6 files changed

+110
-19
lines changed

6 files changed

+110
-19
lines changed

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 & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,32 @@ class Auth:
6666
:param scheme: specifies the type of authentication, examples: "basic", "kerberos"
6767
:type scheme: str
6868
:param principal: specifies who is being authenticated
69-
:type principal: str
69+
:type principal: str or None
7070
:param credentials: authenticates the principal
71-
:type credentials: str
71+
:type credentials: str or None
7272
:param realm: specifies the authentication provider
73-
:type realm: str
73+
:type realm: str or None
7474
:param parameters: extra key word parameters passed along to the authentication provider
75-
:type parameters: str
75+
:type parameters: Dict[str, Any]
7676
"""
7777

78-
#: By default we should not send any realm
79-
realm = None
80-
78+
# TODO in 5.0: change signature to
79+
# def __init__(self, scheme, principal=None, credentials=None,
80+
# ticket=None, realm=None, **parameters):
8181
def __init__(self, scheme, principal, credentials, realm=None, **parameters):
8282
self.scheme = scheme
83-
self.principal = principal
84-
self.credentials = credentials
83+
# Neo4j servers pre 4.4 require the principal field to always be
84+
# present. Therefore, we transmit it even if it's an empty sting.
85+
if principal is not None:
86+
self.principal = principal
87+
if credentials:
88+
self.credentials = credentials
89+
# TODO in 5.0: add ticket
90+
# :param ticket: alternative to authenticate the principal (depends on
91+
# scheme)
92+
# :type ticket: str or None
93+
# if ticket is not None:
94+
# self.ticket = ticket
8595
if realm:
8696
self.realm = realm
8797
if parameters:
@@ -108,7 +118,7 @@ def basic_auth(user, password, realm=None):
108118

109119

110120
def kerberos_auth(base64_encoded_ticket):
111-
""" Generate a kerberos auth token with the base64 encoded ticket
121+
""" Generate a kerberos auth token with the base64 encoded ticket.
112122
113123
This will set the scheme to "kerberos" for the auth token.
114124
@@ -117,7 +127,24 @@ def kerberos_auth(base64_encoded_ticket):
117127
:return: auth token for use with :meth:`GraphDatabase.driver`
118128
:rtype: :class:`neo4j.Auth`
119129
"""
120-
return Auth("kerberos", "", base64_encoded_ticket)
130+
token = Auth("kerberos", "", None)
131+
# token field is not supported by any other auth scheme. So we inject it.
132+
token.ticket = base64_encoded_ticket
133+
return token
134+
135+
136+
def bearer_auth(base64_encoded_token):
137+
""" Generate an auth token for Single-Sign-On providers.
138+
139+
This will set the scheme to "bearer" for the auth token.
140+
141+
:param base64_encoded_token: a base64 encoded authentication token generated
142+
by a Single-Sign-On provider.
143+
144+
:return: auth token for use with :meth:`GraphDatabase.driver`
145+
:rtype: :class:`neo4j.Auth`
146+
"""
147+
return Auth("bearer", None, base64_encoded_token)
121148

122149

123150
def custom_auth(principal, credentials, realm, scheme, **parameters):
@@ -134,6 +161,15 @@ def custom_auth(principal, credentials, realm, scheme, **parameters):
134161
"""
135162
return Auth(scheme, principal, credentials, realm, **parameters)
136163

164+
# TODO in 5.0: alter custom_auth to
165+
# def custom_auth(principal, credentials, ticket, realm, scheme, **parameters):
166+
# """...
167+
# :param ticket: alternative to authenticate the principal (depends on
168+
# scheme)
169+
# ..."""
170+
# return Auth(scheme, principal=principal, credentials=credentials,
171+
# ticket=ticket, realm=realm, **parameter
172+
137173

138174
class Bookmark:
139175
"""A Bookmark object contains an immutable list of bookmark string values.

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["ticket"])
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
@@ -26,12 +26,21 @@
2626
"Flaky: test requires the driver to contact servers in a specific order",
2727
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_retry_on_auth_expired_on_begin_using_tx_function":
2828
"Flaky: test requires the driver to contact servers in a specific order",
29+
"stub.authorization.test_authorization.TestAuthorizationV4x3.test_should_fail_on_token_expired_on_begin_using_tx_function":
30+
"Flaky: test requires the driver to contact servers in a specific order",
31+
"stub.authorization.test_authorization.TestAuthorizationV3.test_should_fail_on_token_expired_on_begin_using_tx_function":
32+
"Flaky: test requires the driver to contact servers in a specific order",
33+
"stub.authorization.test_authorization.TestAuthorizationV4x1.test_should_fail_on_token_expired_on_begin_using_tx_function":
34+
"Flaky: test requires the driver to contact servers in a specific order",
2935
"stub.session_run_parameters.test_session_run_parameters.TestSessionRunParameters.test_empty_query":
3036
"Driver rejects empty queries before sending it to the server",
3137
"tls.tlsversions.TestTlsVersions.test_1_1":
3238
"TLSv1.1 and below are disabled in the driver"
3339
},
3440
"features": {
41+
"Feature:Auth:Bearer": true,
42+
"Feature:Auth:Custom": true,
43+
"Feature:Auth:Kerberos": true,
3544
"AuthorizationExpiredTreatment": true,
3645
"Optimization:ImplicitDefaultArguments": true,
3746
"Optimization:MinimalResets": true,

tests/unit/test_security.py

Lines changed: 24 additions & 3 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

@@ -32,8 +33,19 @@ def test_should_generate_kerberos_auth_token_correctly():
3233
auth = kerberos_auth("I am a base64 service ticket")
3334
assert auth.scheme == "kerberos"
3435
assert auth.principal == ""
35-
assert auth.credentials == "I am a base64 service ticket"
36-
assert not auth.realm
36+
assert auth.ticket == "I am a base64 service ticket"
37+
assert not hasattr(auth, "credentials")
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)