Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
182eda2
just saving
mkleene Apr 17, 2024
7d20a6c
almost fix test
mkleene Apr 18, 2024
e122691
fix up tests
mkleene Apr 18, 2024
6e109cc
Merge branch 'main' into make-dpop-optional
mkleene Apr 18, 2024
361ee20
revert this
mkleene Apr 18, 2024
5b5bb7c
add comment
mkleene Apr 18, 2024
cd22af4
Merge remote-tracking branch 'origin/main' into make-dpop-optional
mkleene Apr 18, 2024
a6aaa2f
Merge remote-tracking branch 'origin/make-dpop-optional' into make-dp…
mkleene Apr 18, 2024
c43b154
docs
mkleene Apr 19, 2024
71f2d1d
x
mkleene Apr 19, 2024
d2e1e26
use a URL that we can access
mkleene Apr 19, 2024
6c921e9
Merge branch 'main' into make-dpop-optional
mkleene Apr 19, 2024
cd00a1e
Update start_test.go
mkleene Apr 19, 2024
922e56a
Merge branch 'main' into make-dpop-optional
mkleene Apr 19, 2024
22dc55a
Update server.go
mkleene Apr 19, 2024
65f116a
Update service/internal/auth/config.go
mkleene Apr 19, 2024
661fcc7
allow disabling authentication
mkleene Apr 19, 2024
1eb9062
Merge remote-tracking branch 'origin/make-dpop-optional' into make-dp…
mkleene Apr 19, 2024
fec65c5
switch enabled/disabled
mkleene Apr 19, 2024
5fbbd9e
Merge remote-tracking branch 'origin/main' into make-dpop-optional
mkleene Apr 22, 2024
1bda433
do not get rid of `enabled`
mkleene Apr 22, 2024
93fec4e
oops
mkleene Apr 22, 2024
dbc1804
Update opentdf-example-no-kas.yaml
mkleene Apr 22, 2024
18f7fa3
Update opentdf-example-no-kas.yaml
mkleene Apr 22, 2024
a4e0934
Update opentdf-with-hsm.yaml
mkleene Apr 22, 2024
d3121f8
allow rewrap with missing key
mkleene Apr 22, 2024
8ebd551
Merge remote-tracking branch 'origin/make-dpop-optional' into make-dp…
mkleene Apr 22, 2024
c520b11
use the previous name
mkleene Apr 22, 2024
40444bf
oops
mkleene Apr 22, 2024
88846cc
Update rewrap.go
mkleene Apr 22, 2024
bbbf7e5
Merge remote-tracking branch 'origin/main' into make-dpop-optional
mkleene Apr 22, 2024
735992c
Update authn_test.go
mkleene Apr 22, 2024
3fd0088
Update authn_test.go
mkleene Apr 22, 2024
ce353d7
Update authn_test.go
mkleene Apr 22, 2024
6f5a63d
oops
mkleene Apr 22, 2024
ec3c36d
Merge remote-tracking branch 'origin/make-dpop-optional' into make-dp…
mkleene Apr 22, 2024
581c46e
Merge branch 'main' into make-dpop-optional
mkleene Apr 22, 2024
c74428f
Merge branch 'main' into make-dpop-optional
mkleene Apr 22, 2024
d8c0a86
Merge branch 'main' into make-dpop-optional
mkleene Apr 23, 2024
7398d73
Merge branch 'main' into make-dpop-optional
mkleene Apr 23, 2024
33b6398
switch setting name
mkleene Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This guide provides details about the configuration setup for our application, i
- [Server Configuration](#server-configuration)
- [Database Configuration](#database-configuration)
- [OPA Configuration](#opa-configuration)
-[Services Configuration](#services-configuration)
- [Services Configuration](#services-configuration)

## Logger Configuration

Expand Down Expand Up @@ -41,6 +41,7 @@ The server configuration is used to define how the application runs its server.
| `tls.key` | The path to the tls key. | |
| `auth.audience` | The audience for the IDP. | |
| `auth.issuer` | The issuer for the IDP. | |
| `auth.allowNoDPoP` | If true, we allow access tokens that do not have DPoP bindings. | `false` |

Example:

Expand All @@ -54,7 +55,7 @@ server:
cert: /path/to/cert
key: /path/to/key
auth:
enabled: true
allowNoDPoP: false
audience: https://example.com
issuer: https://example.com
```
Expand Down
1 change: 0 additions & 1 deletion opentdf-example-no-kas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ services:
enabled: true
server:
auth:
enabled: false
audience: "http://localhost:8080"
issuer: http://localhost:8888/auth/realms/tdf
grpc:
Expand Down
1 change: 0 additions & 1 deletion opentdf-with-hsm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ services:
legacy: true
server:
auth:
enabled: true
audience: "http://localhost:8080"
issuer: http://localhost:8888/auth/realms/opentdf
clients:
Expand Down
39 changes: 20 additions & 19 deletions service/internal/auth/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,15 @@ type Authentication struct {
// openidConfigurations holds the openid configuration for each issuer
oidcConfigurations map[string]AuthNConfig
// Casbin enforcer
enforcer *Enforcer
enforcer *Enforcer
allowNoDPoP bool
}

// Creates new authN which is used to verify tokens for a set of given issuers
func NewAuthenticator(ctx context.Context, cfg AuthNConfig, d *db.Client) (*Authentication, error) {
a := &Authentication{}
a := &Authentication{
allowNoDPoP: cfg.AllowNoDPoP,
}
a.oidcConfigurations = make(map[string]AuthNConfig)

// validate the configuration
Expand Down Expand Up @@ -142,7 +145,7 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler {
http.Error(w, "missing authorization header", http.StatusUnauthorized)
return
}
tok, dpopKey, err := a.checkToken(r.Context(), header, dpopInfo{
tok, newCtx, err := a.checkToken(r.Context(), header, dpopInfo{
headers: r.Header["Dpop"],
path: r.URL.Path,
method: r.Method,
Expand Down Expand Up @@ -180,7 +183,7 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler {
return
}

handler.ServeHTTP(w, r.WithContext(ContextWithJWK(r.Context(), dpopKey)))
handler.ServeHTTP(w, r.WithContext(newCtx))
})
}

Expand Down Expand Up @@ -221,7 +224,7 @@ func (a Authentication) UnaryServerInterceptor(ctx context.Context, req any, inf
action = "other"
}

token, dpopJWK, err := a.checkToken(
token, newCtx, err := a.checkToken(
ctx,
header,
dpopInfo{
Expand All @@ -247,23 +250,20 @@ func (a Authentication) UnaryServerInterceptor(ctx context.Context, req any, inf
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}

return handler(ContextWithJWK(ctx, dpopJWK), req)
return handler(newCtx, req)
}

// checkToken is a helper function to verify the token.
func (a Authentication) checkToken(ctx context.Context, authHeader []string, dpopInfo dpopInfo) (jwt.Token, jwk.Key, error) {
func (a Authentication) checkToken(ctx context.Context, authHeader []string, dpopInfo dpopInfo) (jwt.Token, context.Context, error) {
var (
tokenRaw string
tokenType string
tokenRaw string
)

// If we don't get a DPoP/Bearer token type, we can't proceed
switch {
case strings.HasPrefix(authHeader[0], "DPoP "):
tokenType = "DPoP"
tokenRaw = strings.TrimPrefix(authHeader[0], "DPoP ")
case strings.HasPrefix(authHeader[0], "Bearer "):
tokenType = "Bearer"
tokenRaw = strings.TrimPrefix(authHeader[0], "Bearer ")
default:
return nil, nil, fmt.Errorf("not of type bearer or dpop")
Expand Down Expand Up @@ -307,16 +307,17 @@ func (a Authentication) checkToken(ctx context.Context, authHeader []string, dpo
return nil, nil, err
}

if tokenType == "Bearer" {
slog.Warn("Presented bearer token. validating as DPoP")
_, tokenHasCNF := accessToken.Get("cnf")
if !tokenHasCNF && a.allowNoDPoP {
// this condition is not quite tight because it's possible that the `cnf` claim may
// come from token introspection
return accessToken, ctx, nil
}

key, err := validateDPoP(accessToken, tokenRaw, dpopInfo)
if err != nil {
return nil, nil, err
}

return accessToken, *key, nil
return accessToken, ContextWithJWK(ctx, key), nil
}

func ContextWithJWK(ctx context.Context, key jwk.Key) context.Context {
Expand All @@ -332,10 +333,10 @@ func GetJWKFromContext(ctx context.Context) jwk.Key {
return jwk
}

return nil
panic("got something that is not a jwk.Key from the JWK context")
}

func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo) (*jwk.Key, error) {
func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo) (jwk.Key, error) {
if len(dpopInfo.headers) != 1 {
return nil, fmt.Errorf("got %d dpop headers, should have 1", len(dpopInfo.headers))
}
Expand Down Expand Up @@ -450,5 +451,5 @@ func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo
if ath != base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(h.Sum(nil)) {
return nil, fmt.Errorf("incorrect `ath` claim in DPoP JWT")
}
return &dpopKey, nil
return dpopKey, nil
}
31 changes: 29 additions & 2 deletions service/internal/auth/authn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ func (s *AuthSuite) SetupTest() {
auth, err := NewAuthenticator(
context.Background(),
AuthNConfig{
Issuer: s.server.URL,
Audience: "test",
AllowNoDPoP: false,
Issuer: s.server.URL,
Audience: "test",
}, nil)

s.Require().NoError(err)
Expand Down Expand Up @@ -539,3 +540,29 @@ func makeDPoPToken(t *testing.T, tc dpopTestCase) string {
}
return string(signedToken)
}

func (s *AuthSuite) Test_Allowing_Auth_With_No_DPoP() {
auth, err := NewAuthenticator(
context.Background(),
AuthNConfig{
AllowNoDPoP: true,
Issuer: s.server.URL,
Audience: "test",
}, nil)

s.Require().NoError(err)

tok := jwt.New()
s.Require().NoError(tok.Set(jwt.ExpirationKey, time.Now().Add(time.Hour)))
s.Require().NoError(tok.Set("iss", s.server.URL))
s.Require().NoError(tok.Set("aud", "test"))
s.Require().NoError(tok.Set("client_id", "client1"))
signedTok, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256, s.key))

s.NotNil(signedTok)
s.Require().NoError(err)

_, ctx, err := auth.checkToken(context.Background(), []string{fmt.Sprintf("Bearer %s", string(signedTok))}, dpopInfo{})
s.Require().NoError(err)
s.Require().Nil(GetJWKFromContext(ctx))
}
5 changes: 3 additions & 2 deletions service/internal/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import "fmt"

// AuthConfig pulls AuthN and AuthZ together
type Config struct {
Enabled bool `yaml:"enabled" default:"true" `
AuthNConfig `mapstructure:",squash"`
DeprecatedEnabled bool `yaml:"deprecatedEnabled" default:"true"`
AuthNConfig `mapstructure:",squash"`
}

// AuthNConfig is the configuration need for the platform to validate tokens
type AuthNConfig struct {
AllowNoDPoP bool `yaml:"allowNoDPoP" json:"allowNoDPoP"`
Issuer string `yaml:"issuer" json:"issuer"`
Audience string `yaml:"audience" json:"audience"`
OIDCConfiguration `yaml:"-" json:"-"`
Expand Down
13 changes: 7 additions & 6 deletions service/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func NewOpenTDFServer(config Config, d *db.Client) (*OpenTDFServer, error) {

// Add authN interceptor
// TODO Remove this conditional once we move to the hardening phase (https://github.com/opentdf/platform/issues/381)
if config.Auth.Enabled {
if config.Auth.DeprecatedEnabled {
slog.Info("authentication enabled")
authN, err = auth.NewAuthenticator(
context.Background(),
Expand All @@ -99,6 +99,8 @@ func NewOpenTDFServer(config Config, d *db.Client) (*OpenTDFServer, error) {
if err != nil {
return nil, fmt.Errorf("failed to create authentication interceptor: %w", err)
}
} else {
slog.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP you can use `allowNoDPoP`")
}

// Try an register oidc issuer to wellknown service but don't return an error if it fails
Expand Down Expand Up @@ -160,8 +162,10 @@ func newHttpServer(c Config, h http.Handler, a *auth.Authentication, g *grpc.Ser

// Add authN interceptor
// TODO check if this is needed or if it is handled by gRPC
if c.Auth.Enabled {
if c.Auth.DeprecatedEnabled {
h = a.MuxHandler(h)
} else {
slog.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP you can use `allowNoDPoP`")
}

// Add CORS // TODO We need to make cors configurable (https://github.com/opentdf/platform/issues/305)
Expand Down Expand Up @@ -218,10 +222,7 @@ func newGrpcServer(c Config, a *auth.Authentication) (*grpc.Server, error) {
slog.Warn("failed to create proto validator", slog.String("error", err.Error()))
}

// Add authN interceptor
if c.Auth.Enabled {
i = append(i, a.UnaryServerInterceptor)
}
i = append(i, a.UnaryServerInterceptor)

// Add tls creds if tls is not nil
if c.TLS.Enabled {
Expand Down
42 changes: 36 additions & 6 deletions service/pkg/server/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"

Expand All @@ -24,7 +25,7 @@ type TestServiceService interface{}
type TestService struct{}

func (t TestService) TestHandler(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
_, err := w.Write([]byte("hello " + pathParams["name"] + " from test service!"))
_, err := w.Write([]byte("hello from test service!"))
if err != nil {
panic(err)
}
Expand All @@ -43,23 +44,52 @@ func ServiceRegistrationTest() serviceregistry.Registration {
if !ok {
return fmt.Errorf("Surprise! Not a TestService")
}
return mux.HandlePath(http.MethodGet, "/testpath/{name}", t.TestHandler)
return mux.HandlePath(http.MethodGet, "/healthz", t.TestHandler)
}
},
}
}

func Test_Start_When_Extra_Service_Registered_Expect_Response(t *testing.T) {
discoveryURL := "not set yet"

discoveryEndpoint := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var resp string
switch req.URL.Path {
case "/.well-known/openid-configuration":
resp = `{
"issuer": "https://example.com",
"authorization_endpoint": "https://example.com/oauth2/v1/authorize",
"token_endpoint": "https://example.com/oauth2/v1/token",
"userinfo_endpoint": "https://example.com/oauth2/v1/userinfo",
"registration_endpoint": "https://example.com/oauth2/v1/clients",
"jwks_uri": "` + discoveryURL + `/oauth2/v1/keys"
}`
case "/oauth2/v1/keys":
resp = `{
"keys":[{"kty":"RSA","alg":"RS256","kid":"saqvCEEc1QX1kjGRh3sf0o4bdPMiiQBVj9xYz95M-X0","use":"sig","e":"AQAB","n":"yXgJvKqNfKoOoc1KiTg8QYfAO2AA47PjHtqZFsPSh93FI3tobD52t1I9cbD7ZotIYfYmZ6KwDvtrAIMVAPKvqvVUji3xSsNQ_Vv4XRmoWwP1vgJNJxoHOyj7pfDdhjplZZaQEcEEpm_J9rXN6V2lLyL6zYLJr_SlI5JeMc8i0tigFW_yLTUpSQ_85r5fAvkr0VDeUHfonaueaFhF5r-fne-F9EZzAVZvG3P8IG8_K6NEoM6muzsplPWJ-95hheRa3Zh58vYTVHcX8DXd8rpS3laUlLuEmIVs-FlqYrIBKpP2spQYGRvf-P1wpNftMH7OTB4j6ULQjwlNRmiQ34TOhw"}]
}`
default:
w.WriteHeader(http.StatusNotFound)
return
}
_, _ = w.Write([]byte(resp))
}),
)

discoveryURL = discoveryEndpoint.URL

// Create new opentdf server
d, _ := db.NewClient(db.Config{})
s, err := server.NewOpenTDFServer(server.Config{
WellKnownConfigRegister: func(namespace string, config any) error {
return nil
},
Auth: auth.Config{
Enabled: false,
AuthNConfig: auth.AuthNConfig{
Issuer: "test",
Issuer: discoveryEndpoint.URL,
Audience: "test",
},
},
Port: 43481,
Expand Down Expand Up @@ -87,7 +117,7 @@ func Test_Start_When_Extra_Service_Registered_Expect_Response(t *testing.T) {
var resp *http.Response
// Make request to test service and ensure it registered
for i := 3; i > 0; i-- {
resp, err = http.Get("http://localhost:43481/testpath/world")
resp, err = http.Get("http://localhost:43481/healthz")
if err == nil {
break
}
Expand All @@ -102,5 +132,5 @@ func Test_Start_When_Extra_Service_Registered_Expect_Response(t *testing.T) {
respBody, err := io.ReadAll(resp.Body)

require.NoError(t, err)
assert.Equal(t, "hello world from test service!", string(respBody))
assert.Equal(t, "hello from test service!", string(respBody))
}