-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce re-authentication: auth rotation and session auth support (#…
…467) # ADR 012: re-authentication This PR introduces two new auth mechanics for different use-cases 1) Auth Rotation 2) Session Auth (a.k.a. user switching) **Note that all APIs introduced in this PR are previews** See https://github.com/neo4j/neo4j-go-driver/#preview-features ## 1) Auth Rotation This is used for auth tokens that are expected to expire (e.g., SSO). An `auth.TokenManager` instance may be passed to the driver instead of a static auth token. ```go import ( "context" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) type myTokenManager struct {} func (m *myTokenManager) GetAuthToken(ctx context.Context) (neo4j.AuthToken, error) { // [...] see API documentation for details } func (m *myTokenManager) OnTokenExpired(ctx context.Context, token neo4j.AuthToken) error { // [...] see API documentation for details } driver, err := neo4j.NewDriverWithContext("neo4j://example.com:7687", &myTokenManager{}) // [...] ``` The easiest way to get started is using the provided `TokenManager` implementation. For example: ```go import ( "context" "fmt" "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/neo4j/neo4j-go-driver/v5/neo4j/auth" "time" ) myProvider := func(ctx context.Context) (neo4j.AuthToken, *time.Time, error) { // some way of getting a token token, err := getSsoToken(ctx) if err != nil { return neo4j.AuthToken{}, nil, err } // assume we know our tokens expire every 60 seconds expiresIn := time.Now().Add(60 * time.Second) // Include a little buffer so that we fetch a new token *before* the old one expires expiresIn = expiresIn.Add(-10 * time.Second) // note: we can return nil instead of `&expiresIn` if we don't expect the token to expire return token, &expiresIn, nil } driver, err = neo4j.NewDriverWithContext(getUrl(), auth.ExpirationBasedTokenManager(myProvider)) // [...] ``` **Note** This API is explicitly *not* designed for switching users. In fact, the token returned by each manager must always belong to the same identity. Switching identities using the `AuthManager` is undefined behavior. ## 2) Session Auth For the purpose of switching users, sessions can be configured with a static auth token. This is very similar to impersonation in that all work in the session will be executed in the security context of the user associated with the auth token. The major difference is that impersonation does not require or verify authentication information of the target user, however it requires the impersonating user to have the permission to impersonate. **Note** This requires Bolt protocol version 5.3 or higher (Neo4j server 5.8+). ```go import ( "context" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) // [...] sessionAuth1 := neo4j.BasicAuth("jane", "doe", "") session1 := driver.NewSession(ctx, neo4j.SessionConfig{Auth: &sessionAuth1}) // [...] sessionAuth2 := neo4j.BasicAuth("john", "doe", "") session2 := driver.NewSession(ctx, neo4j.SessionConfig{Auth: &sessionAuth2}) ```
- Loading branch information
Showing
71 changed files
with
2,387 additions
and
727 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* Copyright (c) "Neo4j" | ||
* Neo4j Sweden AB [https://neo4j.com] | ||
* | ||
* This file is part of Neo4j. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package auth | ||
|
||
import ( | ||
"context" | ||
"github.com/neo4j/neo4j-go-driver/v5/neo4j/internal/auth" | ||
"github.com/neo4j/neo4j-go-driver/v5/neo4j/internal/racing" | ||
"reflect" | ||
"time" | ||
) | ||
|
||
// TokenManager is an interface for components that can provide auth tokens. | ||
// The `neo4j` package provides default implementations of `auth.TokenManager` for common authentication schemes. | ||
// See `neo4j.NewDriverWithContext`. | ||
// Custom implementations of this class can be used to provide more complex authentication refresh functionality. | ||
// | ||
// WARNING: | ||
// | ||
// The manager *must not* interact with the driver in any way as this can cause deadlocks and undefined behaviour. | ||
// Furthermore, the manager is expected to be thread-safe. | ||
// | ||
// TokenManager is part of the re-authentication preview feature | ||
// (see README on what it means in terms of support and compatibility guarantees) | ||
type TokenManager interface { | ||
// GetAuthToken retrieves an auth.Token or returns an error if the retrieval fails. | ||
// auth.Token can be created with built-in functions such as: | ||
// - `neo4j.NoAuth` | ||
// - `neo4j.BasicAuth` | ||
// - `neo4j.KerberosAuth` | ||
// - `neo4j.BearerAuth` | ||
// - `neo4j.CustomAuth` | ||
// | ||
// The token returned must always belong to the same identity. | ||
// Switching identities using the `TokenManager` is undefined behavior. | ||
GetAuthToken(ctx context.Context) (auth.Token, error) | ||
// OnTokenExpired is called by the driver when the provided token expires | ||
// OnTokenExpired should invalidate the current token if it matches the provided one | ||
OnTokenExpired(context.Context, auth.Token) error | ||
} | ||
|
||
type authTokenWithExpirationProvider = func(context.Context) (auth.Token, *time.Time, error) | ||
|
||
type expirationBasedTokenManager struct { | ||
provider authTokenWithExpirationProvider | ||
token *auth.Token | ||
expiration *time.Time | ||
mutex racing.Mutex | ||
now *func() time.Time | ||
} | ||
|
||
func (m *expirationBasedTokenManager) GetAuthToken(ctx context.Context) (auth.Token, error) { | ||
if !m.mutex.TryLock(ctx) { | ||
return auth.Token{}, racing.LockTimeoutError( | ||
"could not acquire lock in time when getting token in ExpirationBasedTokenManager") | ||
} | ||
defer m.mutex.Unlock() | ||
if m.token == nil || m.expiration != nil && (*m.now)().After(*m.expiration) { | ||
token, expiration, err := m.provider(ctx) | ||
if err != nil { | ||
return auth.Token{}, err | ||
} | ||
m.token = &token | ||
m.expiration = expiration | ||
} | ||
return *m.token, nil | ||
} | ||
|
||
func (m *expirationBasedTokenManager) OnTokenExpired(ctx context.Context, token auth.Token) error { | ||
if !m.mutex.TryLock(ctx) { | ||
return racing.LockTimeoutError( | ||
"could not acquire lock in time when handling token expiration in ExpirationBasedTokenManager") | ||
} | ||
defer m.mutex.Unlock() | ||
if m.token != nil && reflect.DeepEqual(token.Tokens, m.token.Tokens) { | ||
m.token = nil | ||
} | ||
return nil | ||
} | ||
|
||
// ExpirationBasedTokenManager creates a token manager for potentially expiring auth info. | ||
// | ||
// The first and only argument is a provider function that returns auth information and an optional expiration time. | ||
// If the expiration time is nil, the auth info is assumed to never expire. | ||
// | ||
// WARNING: | ||
// | ||
// The provider function *must not* interact with the driver in any way as this can cause deadlocks and undefined | ||
// behaviour. | ||
// | ||
// The provider function only ever return auth information belonging to the same identity. | ||
// Switching identities is undefined behavior. | ||
// | ||
// ExpirationBasedTokenManager is part of the re-authentication preview feature | ||
// (see README on what it means in terms of support and compatibility guarantees) | ||
func ExpirationBasedTokenManager(provider authTokenWithExpirationProvider) TokenManager { | ||
now := time.Now | ||
return &expirationBasedTokenManager{provider: provider, mutex: racing.NewMutex(), now: &now} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Copyright (c) "Neo4j" | ||
* Neo4j Sweden AB [https://neo4j.com] | ||
* | ||
* This file is part of Neo4j. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package auth_test | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/neo4j/neo4j-go-driver/v5/neo4j" | ||
"github.com/neo4j/neo4j-go-driver/v5/neo4j/auth" | ||
"os" | ||
"time" | ||
) | ||
|
||
func ExampleExpirationBasedTokenManager() { | ||
myProvider := func(ctx context.Context) (neo4j.AuthToken, *time.Time, error) { | ||
// some way to getting a token | ||
token, err := getSsoToken(ctx) | ||
if err != nil { | ||
return neo4j.AuthToken{}, nil, err | ||
} | ||
// assume we know our tokens expire every 60 seconds | ||
|
||
expiresIn := time.Now().Add(60 * time.Second) | ||
// Include a little buffer so that we fetch a new token *before* the old one expires | ||
expiresIn = expiresIn.Add(-10 * time.Second) | ||
// or return nil instead of `&expiresIn` if we don't expect it to expire | ||
return token, &expiresIn, nil | ||
} | ||
|
||
_, _ = neo4j.NewDriverWithContext(getUrl(), auth.ExpirationBasedTokenManager(myProvider)) | ||
} | ||
|
||
func getSsoToken(context.Context) (neo4j.AuthToken, error) { | ||
return neo4j.NoAuth(), nil | ||
} | ||
|
||
func getUrl() string { | ||
return fmt.Sprintf("%s://%s:%s", os.Getenv("TEST_NEO4J_SCHEME"), os.Getenv("TEST_NEO4J_HOST"), os.Getenv("TEST_NEO4J_PORT")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright (c) "Neo4j" | ||
* Neo4j Sweden AB [https://neo4j.com] | ||
* | ||
* This file is part of Neo4j. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
//go:build internal_testkit | ||
|
||
package auth | ||
|
||
import "time" | ||
|
||
func SetTimer(t TokenManager, timer func() time.Time) { | ||
if t, ok := t.(*expirationBasedTokenManager); ok { | ||
t.now = &timer | ||
} | ||
} | ||
|
||
func ResetTime(t TokenManager) { | ||
if t, ok := t.(*expirationBasedTokenManager); ok { | ||
now := time.Now | ||
t.now = &now | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.