Repository navigation
feat(security): implement pre-authentication framework #4
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,31 @@ | ||
| name: Codecov | ||
|
|
||
| on: [push, pull_request] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| fetch-depth: 2 | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | ||
|
|
||
| - name: Install dependencies | ||
| run: go mod download | ||
|
|
||
| - name: Run tests | ||
| run: go test -coverprofile=coverage.txt -coverpkg=./... ./pkg/... ./internal/chain ./internal/manager ./internal/autoconfig | ||
|
|
||
| - name: Upload results to Codecov | ||
| uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 | ||
| with: | ||
| token: ${{ secrets.CODECOV_TOKEN }} | ||
| slug: BeamFoundry/rays-web-security | ||
| fail_ci_if_error: true | ||
This file contains hidden or 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 hidden or 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,146 @@ | ||
| --- | ||
| title: "Pre-Authentication" | ||
| weight: 5 | ||
| --- | ||
|
|
||
| # Pre-Authentication | ||
|
|
||
| In enterprise architectures, authentication is often offloaded to an external infrastructure component before the request ever reaches your application: | ||
|
|
||
| * **Reverse Proxies & API Gateways**: NGINX, Apache, Envoy, or Traefik validating client tokens or mutual TLS (mTLS) and injecting identity headers (e.g. `X-Remote-User`, `X-Forwarded-User`). | ||
| * **Identity & Access Gateways**: Cloudflare Access, OAuth2-Proxy, AWS ALB, Google Cloud IAP, or CA SiteMinder asserting verified identity. | ||
| * **Direct Mutual TLS (mTLS)**: Clients authenticating with an X.509 client certificate directly terminated by the application. | ||
|
|
||
| Rays Web Security provides first-class support for **Pre-Authentication**, inspired by Spring Security's pre-authentication model. | ||
|
|
||
| ## Enabling Pre-Authentication | ||
|
|
||
| Enable and configure pre-authentication by invoking `.PreAuth()` on your `HttpSecurityBuilder`: | ||
|
|
||
| ```go | ||
| package configurations | ||
|
|
||
| import ( | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/config" | ||
| . "github.com/BeamFoundry/rays/pkg/core" | ||
| ) | ||
|
|
||
| type SecurityConfig struct { | ||
| Configuration | ||
| } | ||
|
|
||
| func (this *SecurityConfig) HttpSecurity() *HttpSecurity { | ||
| return (&HttpSecurityBuilder{}). | ||
| AuthorizedHTTPRequests(). | ||
| Paths("/api/admin/**").HasRole("ADMIN"). | ||
| Paths("/api/**").Authenticated(). | ||
| And(). | ||
| PreAuth(). | ||
| PrincipalHeader("X-Remote-User"). | ||
| RolesHeader("X-Remote-Roles"). | ||
| RolesDelimiter(","). | ||
| And(). | ||
| SessionCreationPolicy().Stateless(). | ||
| Build() | ||
| } | ||
|
|
||
| var _, _ = any(&SecurityConfig{}).(Stereotype) | ||
| ``` | ||
|
|
||
| ## Configuration Options | ||
|
|
||
| The `PreAuth()` builder provides the following methods: | ||
|
|
||
| | Method | Default Value | Description | | ||
| | :--- | :--- | :--- | | ||
| | `PrincipalHeader(string)` | `"X-Remote-User"` | HTTP request header used to extract the user principal (username/identity). | | ||
| | `CredentialsHeader(string)` | `""` (empty) | Optional HTTP header to extract pre-authenticated credentials (e.g. certificate or proxy token). | | ||
| | `RolesHeader(string)` | `""` (empty) | Optional HTTP header to extract roles or granted authorities. | | ||
| | `RolesDelimiter(string)` | `","` | Delimiter used to split multiple roles in `RolesHeader`. | | ||
| | `AllowMtlsFallback(bool)` | `true` | When `true`, falls back to direct mTLS client certificates if no valid header identity is present. | | ||
| | `TrustedProxies(...string)` | `[]` (empty) | List of allow-listed proxy identifiers (CommonName or Subject) allowed to forward identity headers. | | ||
| | `ExceptionOnMissing(bool)` | `false` | When `true`, immediately rejects requests with `403 Forbidden` if pre-authenticated credentials are missing, rather than proceeding down the filter chain. | | ||
|
|
||
| ## Security & Header Spoofing Protection | ||
|
|
||
| Pre-authentication trusts the identity forwarded in HTTP headers. If an untrusted client can send requests directly to your backend, they could forge `X-Remote-User` and impersonate any user. | ||
|
|
||
| Rays Web Security includes built-in safeguards to prevent header spoofing: | ||
|
|
||
| ### 1. Mutual Exclusivity Between Headers & mTLS | ||
|
|
||
| When a client establishes a direct connection with a verified mTLS certificate, an untrusted request header cannot override the verified mTLS identity. The framework enforces direct mTLS identity and ignores spoofed user headers. | ||
|
|
||
| ### 2. Trusted Proxy Restrictions (`TrustedProxies`) | ||
|
|
||
| When your backend terminates mTLS connections from both reverse proxies and direct clients, you can configure `TrustedProxies`: | ||
|
|
||
| ```go | ||
| func (this *SecurityConfig) HttpSecurity() *HttpSecurity { | ||
| return (&HttpSecurityBuilder{}). | ||
| AuthorizedHTTPRequests(). | ||
| AnyRequest().Authenticated(). | ||
| And(). | ||
| PreAuth(). | ||
| PrincipalHeader("X-Remote-User"). | ||
| TrustedProxies("proxy.internal.net", "CN=gateway.infra"). | ||
| AllowMtlsFallback(true). | ||
| And(). | ||
| Build() | ||
| } | ||
| ``` | ||
|
|
||
| * **Verified Proxy**: If the incoming connection presents a verified mTLS certificate matching `TrustedProxies`, the header `X-Remote-User` is accepted. | ||
| * **Direct Client**: If the client connects with their own mTLS certificate (not in `TrustedProxies`), header override is blocked and their own certificate identity is used. | ||
| * **Unverified Connection**: Plaintext or unverified connections cannot assert user identity via headers when trusted proxies are configured. | ||
|
|
||
| ### 3. Role Stripping on mTLS Fallback | ||
|
|
||
| When authentication falls back to direct mTLS client certificates, any request-supplied header roles (`RolesHeader`) are automatically cleared. Authorities must be derived from the verified certificate or loaded via an `AuthenticationUserDetailsService`. | ||
|
|
||
| ## User Details & Authority Mapping | ||
|
|
||
| Once a principal is extracted (from headers or mTLS), authentication is processed by an `AuthenticationProvider` delegating to an `AuthenticationUserDetailsService`: | ||
|
|
||
| ```go | ||
| type AuthenticationUserDetailsService interface { | ||
| LoadUserDetails(token Authentication) (UserDetails, AuthenticationException) | ||
| } | ||
| ``` | ||
|
|
||
| Rays Web Security includes two built-in implementations: | ||
|
|
||
| 1. **`PreAuthenticatedGrantedAuthoritiesUserDetailsService`**: Creates a `UserDetails` instance using the principal string and authorities parsed directly from the pre-authentication token / roles header. | ||
| 2. **`UserDetailsByNameServiceWrapper`**: Adapts an existing `UserDetailsService` (such as a database or in-memory user repository) to load full account details and permissions for the pre-authenticated username. | ||
|
|
||
| By default, `internal/autoconfig` automatically configures a `PreAuthenticatedAuthenticationProvider` and provides a default `AuthenticationUserDetailsService`: | ||
| * If a `UserDetailsService` beam is found in the IoC container, it is automatically wrapped using `UserDetailsByNameServiceWrapper`. | ||
| * If no `UserDetailsService` beam is present, it defaults to `PreAuthenticatedGrantedAuthoritiesUserDetailsService`. | ||
|
|
||
| ### Registering a Custom `AuthenticationUserDetailsService` | ||
|
|
||
| To map pre-authenticated identities to database users or custom permissions, register an `AuthenticationUserDetailsService` Beam in your configuration: | ||
|
|
||
| ```go | ||
| package configurations | ||
|
|
||
| import ( | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/auth" | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/core" | ||
| . "github.com/BeamFoundry/rays/pkg/core" | ||
| ) | ||
|
|
||
| type PreAuthConfig struct { | ||
| Configuration | ||
| } | ||
|
|
||
| func (this *PreAuthConfig) PreAuthUserDetailsService( | ||
| that struct { | ||
| userDetailsService UserDetailsService `@:"Inject"` | ||
| }, | ||
| ) AuthenticationUserDetailsService { | ||
| return UserDetailsByNameServiceWrapper{}.New(that.userDetailsService) | ||
| } | ||
|
|
||
| var _, _ = any(&PreAuthConfig{}).(Stereotype) | ||
| ``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or 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 hidden or 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,104 @@ | ||
| package autoconfig | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
|
|
||
| . "github.com/BeamFoundry/rays/pkg/core" | ||
| . "github.com/BeamFoundry/rays/pkg/lang" | ||
|
|
||
| . "github.com/BeamFoundry/rays-web-security/pkg/auth" | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/core" | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/password" | ||
| . "github.com/BeamFoundry/rays-web-security/pkg/testing" | ||
| ) | ||
|
|
||
| func TestAutoconfig(t *testing.T) { | ||
| RegisterFailHandler(Fail) | ||
| RunSpecs(t, "Internal/Autoconfig Suite") | ||
| } | ||
|
|
||
| var _ = Describe("DefaultConfiguration", func() { | ||
| It("provides default PasswordEncoder", func() { | ||
| cfg := &DefaultConfiguration{} | ||
| encoder := cfg.PasswordEncoder(struct{ ConditionalOnMissingBeam }{}) | ||
| Expect(encoder).NotTo(BeNil()) | ||
| }) | ||
|
|
||
| It("provides UserDetailsAuthenticationProvider when service is present or nil when absent", func() { | ||
| cfg := &DefaultConfiguration{} | ||
| encoder := NoOpPasswordEncoder{}.New() | ||
|
|
||
| // Absent | ||
| pNil := cfg.UserDetailsAuthenticationProvider(struct { | ||
| ConditionalOnMissingBeam | ||
| encoder PasswordEncoder `@:"Inject"` | ||
| service Optional[UserDetailsService] `@:"Inject"` | ||
| }{ | ||
| encoder: encoder, | ||
| service: OptionalEmpty[UserDetailsService](), | ||
| }) | ||
| Expect(pNil).To(BeNil()) | ||
|
|
||
| // Present | ||
| uds := InMemoryUserDetailsManager{}.New(encoder) | ||
| p := cfg.UserDetailsAuthenticationProvider(struct { | ||
| ConditionalOnMissingBeam | ||
| encoder PasswordEncoder `@:"Inject"` | ||
| service Optional[UserDetailsService] `@:"Inject"` | ||
| }{ | ||
| encoder: encoder, | ||
| service: OptionalOf[UserDetailsService](uds), | ||
| }) | ||
| Expect(p).NotTo(BeNil()) | ||
| }) | ||
|
|
||
| It("provides AuthenticationUserDetailsService wrapping UserDetailsService or defaulting", func() { | ||
| cfg := &DefaultConfiguration{} | ||
|
|
||
| // Default (no UserDetailsService) | ||
| sDefault := cfg.AuthenticationUserDetailsService(struct { | ||
| ConditionalOnMissingBeam | ||
| userDetailsService Optional[UserDetailsService] `@:"Inject"` | ||
| }{ | ||
| userDetailsService: OptionalEmpty[UserDetailsService](), | ||
| }) | ||
| Expect(sDefault).NotTo(BeNil()) | ||
|
|
||
| // Wrapped UserDetailsService | ||
| encoder := NoOpPasswordEncoder{}.New() | ||
| uds := InMemoryUserDetailsManager{}.New(encoder) | ||
| sWrapped := cfg.AuthenticationUserDetailsService(struct { | ||
| ConditionalOnMissingBeam | ||
| userDetailsService Optional[UserDetailsService] `@:"Inject"` | ||
| }{ | ||
| userDetailsService: OptionalOf[UserDetailsService](uds), | ||
| }) | ||
| Expect(sWrapped).NotTo(BeNil()) | ||
| }) | ||
|
|
||
| It("provides PreAuthenticatedAuthenticationProvider when service is present or nil when absent", func() { | ||
| cfg := &DefaultConfiguration{} | ||
|
|
||
| // Absent | ||
| pNil := cfg.PreAuthenticatedAuthenticationProvider(struct { | ||
| ConditionalOnMissingBeam | ||
| service Optional[AuthenticationUserDetailsService] `@:"Inject"` | ||
| }{ | ||
| service: OptionalEmpty[AuthenticationUserDetailsService](), | ||
| }) | ||
| Expect(pNil).To(BeNil()) | ||
|
|
||
| // Present | ||
| service := PreAuthenticatedGrantedAuthoritiesUserDetailsService{}.New() | ||
| p := cfg.PreAuthenticatedAuthenticationProvider(struct { | ||
| ConditionalOnMissingBeam | ||
| service Optional[AuthenticationUserDetailsService] `@:"Inject"` | ||
| }{ | ||
| service: OptionalOf[AuthenticationUserDetailsService](service), | ||
| }) | ||
| Expect(p).NotTo(BeNil()) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.