Skip to content

Add Entra ID Authentication Support for Redis ( go-redis ) #1

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

Open
wants to merge 40 commits into
base: main
Choose a base branch
from

Conversation

ndyakov
Copy link
Contributor

@ndyakov ndyakov commented Mar 25, 2025

Entra ID Authentication Support for go-redis

Disclaimer: AI generated PR Description

Overview

This PR introduces Entra ID authentication support for go-redis using the go-redis-entraid Go library. The implementation provides a robust and flexible authentication system with support for multiple Entra ID identity types, automatic token refresh, and thread-safe token management.

Key Features

  • Support for multiple Entra ID identity types (Managed Identity, Confidential Client, Default Azure Identity)
  • Automatic token refresh and management
  • Thread-safe token handling
  • Comprehensive test coverage
  • Performance benchmarking
  • Code quality and formatting tools

Implementation Details

Core Authentication Components

  1. Token Management

    • New TokenManager interface and implementation
    • Token refresh and expiration handling
    • Thread-safe token operations
    • Error handling and retry mechanisms
  2. Identity Providers

    • Managed Identity Provider
    • Confidential Client Provider
    • Default Azure Identity Provider
    • Support for different credential types (certificate, secret)
  3. Credentials Provider

    • Streaming credentials provider implementation
    • Token update notifications
    • Error handling and recovery

Testing Infrastructure

  • Comprehensive test suite for all components
  • Mock implementations for testing
  • Benchmark tests for performance monitoring
  • Test coverage thresholds (85% file, 90% package, 85% total)
  • Higher coverage requirements for critical packages (95% for internal, 100% for token)

Development Tools and CI/CD

  • Pre-commit hooks for code formatting and linting
  • GitHub Actions workflow for benchmarking
  • Race detection in tests
  • Code coverage reporting
  • Linting configuration (golangci-lint)

Documentation

  • Quick start guide
  • Architecture overview
  • Authentication providers comparison
  • Configuration options
  • Contributing guidelines

Breaking Changes

None - This is a new feature addition.

Dependencies

Added new dependencies:

  • Azure SDK
  • Microsoft Authentication Library
  • JWT handling
  • Redis client

Testing

  • Unit tests for all components
  • Integration tests for authentication flows
  • Performance benchmarks
  • Race condition detection
  • Coverage requirements

Documentation

  • Package documentation
  • Usage examples
  • Configuration guides
  • Contributing guidelines

Future Considerations

  • Enhanced error handling
  • Performance optimizations
  • Extended configuration options

Reviewer Suggestions

Where to Start Reviewing

  1. Core Authentication Components

    • Start with entraid/credentials_provider.go to understand the main authentication flow
    • Review manager/token_manager.go for token management implementation
    • Check identity/ package for different identity provider implementations
  2. Testing

    • Begin with entraid/credentials_provider_test.go for core functionality tests
    • Review manager/token_manager_test.go for token management tests
    • Check identity/*_test.go files for provider-specific tests
  3. Configuration and Options

    • Review identity/providers.go for available options and constants
    • Check manager/options.go for token manager configuration
    • Examine internal/idp_response.go for response handling
  4. Development Tools

    • Review .githooks/pre-commit for pre-commit hooks
    • Check .github/workflows/benchmark.yml for benchmarking setup
    • Examine golangci.yml for linting configuration
  5. Documentation

    • Start with README.md for project overview
    • Review CONTRIBUTING.md for development guidelines
    • Check package documentation in each major component

@ndyakov ndyakov force-pushed the main branch 2 times, most recently from 443b1ef to dc67db5 Compare March 31, 2025 21:07
@ndyakov ndyakov force-pushed the intro branch 3 times, most recently from 27ddb0a to c48eb03 Compare March 31, 2025 23:32
@ndyakov ndyakov force-pushed the intro branch 3 times, most recently from aa4f39c to 3c46773 Compare April 9, 2025 03:43
@ndyakov ndyakov requested a review from Copilot April 14, 2025 20:07
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 42 out of 44 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • .githooks/pre-commit: Language not supported
  • go.mod: Language not supported
Comments suppressed due to low confidence (1)

.testcoverage.yml:22

  • The overall project coverage threshold (85%) is lower than the package threshold (90%). Verify that these values are intended and consistent with your quality goals.
total: 85

}
// acquire token using the managed identity client
// the resource is the URL of the resource that the identity has access to
authResult, err := m.client.AcquireToken(context.TODO(), resource)
Copy link
Preview

Copilot AI Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using context.Background() instead of context.TODO() if cancellation support is not required for token acquisition. This may improve readability and intent clarity.

Suggested change
authResult, err := m.client.AcquireToken(context.TODO(), resource)
authResult, err := m.client.AcquireToken(context.Background(), resource)

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep it as TODO if we decide to pass the client context in the future.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're not passing the client context right now?

Copy link

@elena-kolevska elena-kolevska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!
I also really appreciate the details in the Readme!

// Use this when you want either a system assigned identity or a user assigned identity.
// The system assigned identity is automatically managed by Azure and does not require any additional configuration.
// The user assigned identity is a separate resource that can be managed independently.
func NewManagedIdentityCredentialsProvider(options ManagedIdentityCredentialsProviderOptions) (auth.StreamingCredentialsProvider, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have these as entraid.credentialsprovider.NewManagedIdentity, entraid.credentialsprovider.NewConfidential and so on...

package internal

// IsClosed checks if a channel is closed.
func IsClosed(ch <-chan struct{}) bool {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the utility of this helper function, but the name is misleading, cause it might return true even if the channel is not closed (when the channel is not empty).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Being in the internal package and since we only use the chanel for synchronisation I did not consider that. Any suggestions for a better name for it? I personally don't mind leaving it like this.

Copy link

@elena-kolevska elena-kolevska Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look into how it's used, but it's usually a readiness signal, so maybe something like IsReady? Again, I don't have the whole context, so feel free to disconsider.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some consideration, I would prefer to keep it named as it is. Updated the comment to note that this function returns true no only for closed channels.

Removed unused functionality.
Introduced getter for TTL.
Improved tests to use defined time.Time.
Copy link
Member

@bobymicroby bobymicroby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left out some comments

// Start starts the token manager and returns a channel that will receive updates.
Start(listener TokenListener) (CloseFunc, error)
// Close closes the token manager and releases any resources.
Close() error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the TokenManager interface has both a Close() method and also returns a CloseFunc from Start(). This creates confusion about the proper way to close the token manager.

Having both approaches breaks variance and makes the API less intuitive to use. I recommend either:

  1. Remove the Close() method from the interface and only rely on the CloseFunc returned by Start(), or
  2. Remove the CloseFunc from the return values of Start() and only use the Close() method.

The first option is preferable as it correctly ties the lifecycle of the token manager.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the negative of Start should be Stop ; Close should be the negative of Open

// It is used to get the type of the authentication result, the authentication result itself (can be AuthResult or AccessToken),
type IdentityProviderResponse interface {
// Type returns the type of the auth result
Type() string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the identity provider response implementation and have a suggestion about the type handling design:

The current IDPResp implementation works but has some potential issues with its type handling:

func (a *IDPResp) AuthResult() public.AuthResult {
    if a.authResultVal == nil {
        return public.AuthResult{}  // Returns empty value, not an error
    }
    return *a.authResultVal
}

This pattern requires callers to remember to check Type() or HasAuthResult() before using returned values. If they forget, they'll get empty structs rather than errors, which could lead to subtle bugs.

type TokenResponse interface {
    ExpiresOn() time.Time
    ReceivedAt() time.Time
    RawToken() string
}

type AuthResultResponse interface {
    TokenResponse
    GetAuthResult() public.AuthResult
}

type AccessTokenResponse interface {
    TokenResponse
    GetAccessToken() azcore.AccessToken
}

With this approach, the compiler would help enforce correct usage through type assertions:

// Example usage
func ParseResponse(resp TokenResponse) (*Token, error) {
    if ar, ok := resp.(AuthResultResponse); ok {
        result := ar.GetAuthResult()
        // Use auth result...
    } else if at, ok := resp.(AccessTokenResponse); ok {
        token := at.GetAccessToken()
        // Use access token...
    } else {
        return nil, errors.New("unsupported token type")
    }
    // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for sharing your insight, that is a great comment and suggestions.

  • I agree that the current interfaces could result in users not checking the Type and thus result in issues for the developers, since no clear err will be returned.

  • Tried to keep anything token related outside of the IdentityProviderResponse since there is a parser later on to parse from IdentityProviderResponse to a Token and right now I am not completely sold on the idea to have getters for the ReceivedAt and ExpiresOn, but will consider it.

I prepared something that mimics the suggested approach in 22dc6ae , leaving only the Type() in the IdentityProviderInterface and after looking at it, I do think that a more explicit getters, that will return an error, can further improve the design.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added errors in f493bda and I think it looks better. Let me know if you agree.

ndyakov added 5 commits April 16, 2025 11:11
Kindly contributed by @bobymicroby
- Change type names to make more sense (e.g. Start / Stop )
- Add context.Context to the IDP RequestToken
- Add RequestTimeout to TokenManagerOptions which will be utilized by
  the context
- Change the LowerRefreshBoundMs from int64 to time.Duration and use
  better name (dropping the Ms suffix)

TODO:
 - Address changes in the documentation
A more idiomatic approach for Go would be to use time.Duration instead
of int representation of Milliseconds.
@ndyakov ndyakov requested a review from Copilot April 17, 2025 10:53
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds Entra ID authentication support for go-redis by introducing new token management components and multiple identity provider implementations (managed, confidential, and Azure default) along with extensive testing and CI/CD improvements.

  • Adds new authentication flows and error handling for various Entra ID identity types.
  • Introduces comprehensive tests with mocks, benchmarks, and updated configuration files for code quality and coverage.

Reviewed Changes

Copilot reviewed 43 out of 45 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
identity/providers.go Added constants for identity and credential types.
identity/managed_identity_provider.go & tests Implements managed identity provider and tests scenarios.
identity/confidential_identity_provider.go & tests Implements confidential identity provider with various credential options and error handling.
identity/azure_default_identity_provider.go & tests Implements default Azure identity provider with token request logic.
identity/authority_configuration.go & tests Provides authority building logic and covers various authority types.
entraid and credentials_provider.go Provides integration and subscription logic for token updates.
CI/CD & configuration files Updated workflows, test coverage thresholds, and linter configurations for improved quality and performance.
Files not reviewed (2)
  • .githooks/pre-commit: Language not supported
  • go.mod: Language not supported

ndyakov added 2 commits April 17, 2025 16:46
IdentityProviderResponse getters will return error in the case where the type is incorrect or the response is not set.
@ndyakov ndyakov marked this pull request as ready for review April 25, 2025 12:33
ndyakov added 5 commits April 28, 2025 11:20
Set default requestTimeout to 30 seconds
Remove Stop method, fix tests.
Simplify the default identity provider response parser by extracting the raw token from the response and then parsing it as jwt token.
Copy link

Merging this branch changes the coverage (1 decrease, 5 increase)

Impacted Packages Coverage Δ 🤖
github.com/redis-developer/go-redis-entraid 97.50% (-2.50%) 👎
github.com/redis-developer/go-redis-entraid/identity 97.80% (+97.80%) 🌟
github.com/redis-developer/go-redis-entraid/internal 100.00% (+100.00%) 🌟
github.com/redis-developer/go-redis-entraid/manager 98.16% (+98.16%) 🌟
github.com/redis-developer/go-redis-entraid/shared 100.00% (+100.00%) 🌟
github.com/redis-developer/go-redis-entraid/token 100.00% (+100.00%) 🌟

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/redis-developer/go-redis-entraid/credentials_provider.go 97.62% (+97.62%) 42 (+42) 41 (+41) 1 (+1) 🌟
github.com/redis-developer/go-redis-entraid/entraid.go 0.00% (ø) 0 0 0
github.com/redis-developer/go-redis-entraid/identity/authority_configuration.go 100.00% (+100.00%) 11 (+11) 11 (+11) 0 🌟
github.com/redis-developer/go-redis-entraid/identity/azure_default_identity_provider.go 100.00% (+100.00%) 14 (+14) 14 (+14) 0 🌟
github.com/redis-developer/go-redis-entraid/identity/confidential_identity_provider.go 100.00% (+100.00%) 41 (+41) 41 (+41) 0 🌟
github.com/redis-developer/go-redis-entraid/identity/managed_identity_provider.go 92.00% (+92.00%) 25 (+25) 23 (+23) 2 (+2) 🌟
github.com/redis-developer/go-redis-entraid/identity/providers.go 0.00% (ø) 0 0 0
github.com/redis-developer/go-redis-entraid/internal/errors.go 0.00% (ø) 0 0 0
github.com/redis-developer/go-redis-entraid/internal/idp_response.go 100.00% (+100.00%) 32 (+32) 32 (+32) 0 🌟
github.com/redis-developer/go-redis-entraid/internal/utils.go 100.00% (+100.00%) 3 (+3) 3 (+3) 0 🌟
github.com/redis-developer/go-redis-entraid/manager/defaults.go 100.00% (+100.00%) 69 (+69) 69 (+69) 0 🌟
github.com/redis-developer/go-redis-entraid/manager/errors.go 0.00% (ø) 0 0 0
github.com/redis-developer/go-redis-entraid/manager/token_manager.go 96.81% (+96.81%) 94 (+94) 91 (+91) 3 (+3) 🌟
github.com/redis-developer/go-redis-entraid/providers.go 97.06% (+97.06%) 34 (+34) 33 (+33) 1 (+1) 🌟
github.com/redis-developer/go-redis-entraid/shared/identity_provider_response.go 100.00% (+100.00%) 1 (+1) 1 (+1) 0 🌟
github.com/redis-developer/go-redis-entraid/token/token.go 100.00% (+100.00%) 11 (+11) 11 (+11) 0 🌟
github.com/redis-developer/go-redis-entraid/token_listener.go 100.00% (+100.00%) 3 (+3) 3 (+3) 0 🌟
github.com/redis-developer/go-redis-entraid/version.go 100.00% (ø) 1 1 0

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/redis-developer/go-redis-entraid/credentials_provider_test.go
  • github.com/redis-developer/go-redis-entraid/entraid_test.go
  • github.com/redis-developer/go-redis-entraid/identity/authority_configuration_test.go
  • github.com/redis-developer/go-redis-entraid/identity/azure_default_identity_provider_test.go
  • github.com/redis-developer/go-redis-entraid/identity/confidential_identity_provider_test.go
  • github.com/redis-developer/go-redis-entraid/identity/identity_test.go
  • github.com/redis-developer/go-redis-entraid/identity/managed_identity_provider_test.go
  • github.com/redis-developer/go-redis-entraid/identity/providers_test.go
  • github.com/redis-developer/go-redis-entraid/internal/idp_response_test.go
  • github.com/redis-developer/go-redis-entraid/internal/utils_test.go
  • github.com/redis-developer/go-redis-entraid/manager/manager_test.go
  • github.com/redis-developer/go-redis-entraid/manager/token_manager_test.go
  • github.com/redis-developer/go-redis-entraid/providers_test.go
  • github.com/redis-developer/go-redis-entraid/shared/identity_provider_response_test.go
  • github.com/redis-developer/go-redis-entraid/token/token_test.go
  • github.com/redis-developer/go-redis-entraid/token_listener_test.go
  • github.com/redis-developer/go-redis-entraid/version_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants