Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"method": "GET",
"urlPath": "/auth/.well-known/openid-configuration"
},

"response": {
"status": 200,
"bodyFileName": "messages/oidc_jwks.json",
"transformers": ["response-template"]
"bodyFileName": "messages/oidc_config.json",
"transformers": [
"response-template"
]
}
}
13 changes: 13 additions & 0 deletions service/integration/wiremock/mappings/oidc_jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"request": {
"method": "GET",
"urlPath": "/auth/keys"
},
"response": {
"status": 200,
"bodyFileName": "messages/oidc_jwks.json",
"transformers": [
"response-template"
]
}
}
24 changes: 24 additions & 0 deletions service/integration/wiremock/messages/oidc_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"issuer": "{{request.baseUrl}}/mock-server",
"authorization_endpoint": "{{request.baseUrl}}/auth/authorize",
"token_endpoint": "{{request.baseUrl}}/auth/token",
"userinfo_endpoint": "{{request.baseUrl}}/auth/userinfo",
"registration_endpoint": "{{request.baseUrl}}/auth/clients",
"jwks_uri": "{{request.baseUrl}}/auth/keys",
"response_types_supported": ["code", "id_token", "code id_token", "code token", "id_token token", "code id_token token"],
"response_modes_supported": ["query", "fragment", "form_post", "okta_post_message"],
"grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["sms", "openid", "profile", "email", "address", "phone", "offline_access"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"claims_supported": ["iss", "ver", "sub", "aud", "iat", "exp", "jti", "auth_time", "amr", "idp", "nonce", "name", "nickname", "preferred_username", "given_name", "middle_name", "family_name", "email", "email_verified", "profile", "zoneinfo", "locale", "address", "phone_number", "picture", "website", "gender", "birthdate", "updated_at", "at_hash", "c_hash"],
"code_challenge_methods_supported": ["S256"],
"introspection_endpoint": "{{request.baseUrl}}/auth/introspect",
"introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"revocation_endpoint": "{{request.baseUrl}}/auth/revoke",
"revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"end_session_endpoint": "{{request.baseUrl}}/auth/logout",
"request_parameter_supported": true,
"request_object_signing_alg_values_supported": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]
}
52 changes: 29 additions & 23 deletions service/integration/wiremock/messages/oidc_jwks.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
{
"issuer": "{{request.baseUrl}}/mock-server",
"authorization_endpoint": "{{request.baseUrl}}/auth/authorize",
"token_endpoint": "{{request.baseUrl}}/auth/token",
"userinfo_endpoint": "{{request.baseUrl}}/auth/userinfo",
"registration_endpoint": "{{request.baseUrl}}/auth/clients",
"jwks_uri": "{{request.baseUrl}}/auth/keys",
"response_types_supported": ["code", "id_token", "code id_token", "code token", "id_token token", "code id_token token"],
"response_modes_supported": ["query", "fragment", "form_post", "okta_post_message"],
"grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["sms", "openid", "profile", "email", "address", "phone", "offline_access"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"claims_supported": ["iss", "ver", "sub", "aud", "iat", "exp", "jti", "auth_time", "amr", "idp", "nonce", "name", "nickname", "preferred_username", "given_name", "middle_name", "family_name", "email", "email_verified", "profile", "zoneinfo", "locale", "address", "phone_number", "picture", "website", "gender", "birthdate", "updated_at", "at_hash", "c_hash"],
"code_challenge_methods_supported": ["S256"],
"introspection_endpoint": "{{request.baseUrl}}/auth/introspect",
"introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"revocation_endpoint": "{{request.baseUrl}}/auth/revoke",
"revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "none"],
"end_session_endpoint": "{{request.baseUrl}}/auth/logout",
"request_parameter_supported": true,
"request_object_signing_alg_values_supported": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]
}
"keys": [
{
"kid": "STrsCH5CwD6SPwfZrC1gSb8ST91qsWyjleb0QGqSEJA",
"kty": "RSA",
"alg": "RSA-OAEP",
"use": "enc",
"n": "k0e-Xgq-GxG49XJRUP9bzswukbeL1geMlUkD47w7N6a1Rc-cq9kbc3hIZtseGKBRhIN3jKKHdJKNl0iSFu0P5YzjP-13IgYkqhEEodXfIMC6RHgeiJxZqNIonVPvBxWu6iWoqxRY_HgsFtGHhR1OfLP33wXzhZ2Osoc_RKK1D9kz_pbvDHMbaaUvNyCnyqGeqPVw5kpwBb5pi-BPapXvvRTWzbXKxipw59fZ1aSYL-JwjkbpNIdISSZCdmk0lzljKPWq59hVn3lVH2lZz1NZp3eypxFInaQqPwBehPRnwiPVBcZ6yX5zynZ_knUcOHiS65OmDPCoFpWqWJGL1YFHvw",
"e": "AQAB",
"x5c": [
"MIICnTCCAYUCBgGO1+er8TANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdvcGVudGRmMB4XDTI0MDQxMzE0MzkyOFoXDTM0MDQxMzE0NDEwOFowEjEQMA4GA1UEAwwHb3BlbnRkZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJNHvl4KvhsRuPVyUVD/W87MLpG3i9YHjJVJA+O8OzemtUXPnKvZG3N4SGbbHhigUYSDd4yih3SSjZdIkhbtD+WM4z/tdyIGJKoRBKHV3yDAukR4HoicWajSKJ1T7wcVruolqKsUWPx4LBbRh4UdTnyz998F84WdjrKHP0SitQ/ZM/6W7wxzG2mlLzcgp8qhnqj1cOZKcAW+aYvgT2qV770U1s21ysYqcOfX2dWkmC/icI5G6TSHSEkmQnZpNJc5Yyj1qufYVZ95VR9pWc9TWad3sqcRSJ2kKj8AXoT0Z8Ij1QXGesl+c8p2f5J1HDh4kuuTpgzwqBaVqliRi9WBR78CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAa5gfdx6X3IwndMH/x9xsjqUL1EpmLLuAXVh4NFCLs6roA1Xien4A+2y+trTjfImC9hIyEM+iEy9kQdB1ddMZC9PkzxQy94p2QZeIY5lBicxRLPS05rAeQXoHxP46TD7Mzat7txiNCtJZn3Y1bOdEX/SpGosWZ+vVrQMshAwOhBGcApZxW5rhnMBamzO5bQiX53gNdexc0vhZ+mzNURvvCt/XSbyQmyp8537l96AhHDiBScriKcF+J/o42+IV6b2NuFttzf3/Fj9wi2DIAw1DStChos/1XsCtGgLkVJSFxR+JhZfGS4wcEfqfhWhWppgrWPk9UdpjYr6bcl+BSPXvQw=="
],
"x5t": "K6PhEHOchyu2P--3km64mwIct1A",
"x5t#S256": "xkRwZKknmRIiS3nQKoZk_Cw9KH01Wb-o2Qmd8Niy_FA"
},
{
"kid": "CKB_TehGsxMsfhaFyKRAFHV9PveHT3UN0QmomB7GJgM",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "xdGdCYsc3gZtGnLZkWccFlznMVrW_tzEeiflBiCHg8lhH4Tgjw8tZA4DMrueXRGJXqTbpSIjMhpapkSkYIOQV8Zg5zxSo-aahaD6FMzxFKIA7m1TGtwTpOik536Njrii_iRBiekw_o1Ua5KShN_4cptw9a8HN8aP826H_pPLQlOFCWMsMHw8taS2vt59OzRfAOf9BG4gIj56Od4XzQxI2ICNGVoZKADKNbWd0FDMEZqEoe5cDo8mGmO4ouYJpvch7InnC6VNzbJMpTt3sAyawmpAKAI9R4eW96HsUK54foLMBNjGlzKQnJIkisp6YipMqc_43EWYEhQuDa3cdpULJQ",
"e": "AQAB",
"x5c": [
"MIICnTCCAYUCBgGO1+erTjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdvcGVudGRmMB4XDTI0MDQxMzE0MzkyN1oXDTM0MDQxMzE0NDEwN1owEjEQMA4GA1UEAwwHb3BlbnRkZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXRnQmLHN4GbRpy2ZFnHBZc5zFa1v7cxHon5QYgh4PJYR+E4I8PLWQOAzK7nl0RiV6k26UiIzIaWqZEpGCDkFfGYOc8UqPmmoWg+hTM8RSiAO5tUxrcE6TopOd+jY64ov4kQYnpMP6NVGuSkoTf+HKbcPWvBzfGj/Nuh/6Ty0JThQljLDB8PLWktr7efTs0XwDn/QRuICI+ejneF80MSNiAjRlaGSgAyjW1ndBQzBGahKHuXA6PJhpjuKLmCab3IeyJ5wulTc2yTKU7d7AMmsJqQCgCPUeHlveh7FCueH6CzATYxpcykJySJIrKemIqTKnP+NxFmBIULg2t3HaVCyUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAk7RMcdoS500NL2seTQPSN0dIb1jSSskN0IPo2rmTbh2VupWuABMv3pKUzzp0LcyDdwJhdoF44g0UAzlhnqkPhBGG9H7QfGblnCSwcGX96tRsAu8f1dxlGoE1DJWay4OqNAZuSgd8/7kxaCVdK4rURhQdxW+nuO7HfeuwKpi7zFp3ubm2WVAZUAirbQ8CIS02Z5ki6q+l0rDe10RXVUUg6v0uLIF/WpsMEoLjWv7EV4E4qkRk0YYXN74o3BJgJknvyMXnUO1wb69sOFaY627jVxlksCpJ4780lOYnoqQmDXCGceggzTMrXCNOJBiSKwjndaUEseym2CNhjtNfESpdIg=="
],
"x5t": "2poAImSunrm90ff8mrVWeCM8Jh8",
"x5t#S256": "aHHEYJnhNPkbS9DgOL6RrtMVEZLuRyefFBFZRBMJHnU"
}
]
}
36 changes: 27 additions & 9 deletions service/internal/auth/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"log/slog"
"net/http"
"regexp"
"slices"
"strings"
"time"
Expand All @@ -30,14 +31,11 @@ const (
type authContextKey string

var (
// Set of allowed gRPC endpoints that do not require authentication
allowedGRPCEndpoints = [...]string{
// Set of allowed public endpoints that do not require authentication
allowedPublicEndpoints = [...]string{
"/grpc.health.v1.Health/Check",
"/wellknownconfiguration.WellKnownService/GetWellKnownConfiguration",
"/kas.AccessService/PublicKey",
}
// Set of allowed HTTP endpoints that do not require authentication
allowedHTTPEndpoints = [...]string{
"/healthz",
"/.well-known/opentdf-configuration",
"/kas/v2/kas_public_key",
Expand Down Expand Up @@ -66,10 +64,12 @@ type Authentication struct {
oidcConfigurations map[string]AuthNConfig
// Casbin enforcer
enforcer *Enforcer
// Public Routes HTTP & gRPC
publicRoutes []string
}

// 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) {
func NewAuthenticator(ctx context.Context, cfg Config, d *db.Client) (*Authentication, error) {
a := &Authentication{}
a.oidcConfigurations = make(map[string]AuthNConfig)

Expand Down Expand Up @@ -117,7 +117,11 @@ func NewAuthenticator(ctx context.Context, cfg AuthNConfig, d *db.Client) (*Auth
return nil, err
}

a.oidcConfigurations[cfg.Issuer] = cfg
// Combine public routes
a.publicRoutes = append(a.publicRoutes, cfg.PublicRoutes...)
a.publicRoutes = append(a.publicRoutes, allowedPublicEndpoints[:]...)

a.oidcConfigurations[cfg.Issuer] = cfg.AuthNConfig

return a, nil
}
Expand All @@ -131,7 +135,7 @@ type dpopInfo struct {
// verifyTokenHandler is a http handler that verifies the token
func (a Authentication) MuxHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if slices.Contains(allowedHTTPEndpoints[:], r.URL.Path) {
if slices.ContainsFunc(a.publicRoutes, a.isPublicRoute(r.URL.Path)) {
handler.ServeHTTP(w, r)
return
}
Expand Down Expand Up @@ -187,7 +191,7 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler {
// UnaryServerInterceptor is a grpc interceptor that verifies the token in the metadata
func (a Authentication) UnaryServerInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// Allow health checks to pass through
if slices.Contains(allowedGRPCEndpoints[:], info.FullMethod) {
if slices.ContainsFunc(a.publicRoutes, a.isPublicRoute(info.FullMethod)) {
return handler(ctx, req)
}

Expand Down Expand Up @@ -452,3 +456,17 @@ func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo
}
return &dpopKey, nil
}

func (a Authentication) isPublicRoute(path string) func(string) bool {
return func(route string) bool {
pattern := fmt.Sprintf("^%s$", route)
pattern = strings.ReplaceAll(pattern, "*", ".*")
matched, err := regexp.MatchString(pattern, path)
if err != nil {
slog.Warn("error matching route", slog.String("route", route), slog.String("path", path), slog.String("error", err.Error()))
return false
}
slog.Debug("matching route", slog.String("route", route), slog.String("path", path), slog.String("pattern", pattern), slog.Bool("matched", matched))
return matched
}
}
28 changes: 24 additions & 4 deletions service/internal/auth/authn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -141,10 +142,14 @@ func (s *AuthSuite) SetupTest() {

auth, err := NewAuthenticator(
context.Background(),
AuthNConfig{
Issuer: s.server.URL,
Audience: "test",
}, nil)
Config{
AuthNConfig: AuthNConfig{
Issuer: s.server.URL,
Audience: "test",
},
PublicRoutes: []string{"/public", "/public2/*", "/public3/static", "/static*"},
},
nil)

s.Require().NoError(err)

Expand Down Expand Up @@ -539,3 +544,18 @@ func makeDPoPToken(t *testing.T, tc dpopTestCase) string {
}
return string(signedToken)
}

func (s *AuthSuite) Test_PublicPath_Matches() {
// Passing routes
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public")))
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public2/test")))
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public3/static")))
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public2/")))
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/static/test")))
s.Require().True(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/static/test/next")))

// Failing routes
s.Require().False(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public3/")))
s.Require().False(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/public2")))
s.Require().False(slices.ContainsFunc(s.auth.publicRoutes, s.auth.isPublicRoute("/private")))
}
5 changes: 3 additions & 2 deletions service/internal/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import "fmt"

// AuthConfig pulls AuthN and AuthZ together
type Config struct {
Enabled bool `yaml:"enabled" default:"true" `
AuthNConfig `mapstructure:",squash"`
Enabled bool `yaml:"enabled" default:"true" `
PublicRoutes []string `mapstructure:"-"`
AuthNConfig `mapstructure:",squash"`
}

// AuthNConfig is the configuration need for the platform to validate tokens
Expand Down
2 changes: 2 additions & 0 deletions service/internal/auth/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
)

Expand All @@ -27,6 +28,7 @@ type OIDCConfiguration struct {

// DiscoverOPENIDConfiguration discovers the openid configuration for the issuer provided
func DiscoverOIDCConfiguration(ctx context.Context, issuer string) (*OIDCConfiguration, error) {
slog.DebugContext(ctx, "discovering openid configuration", slog.String("issuer", issuer))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s%s", issuer, DiscoveryPath), nil)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion service/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func NewOpenTDFServer(config Config, d *db.Client) (*OpenTDFServer, error) {
slog.Info("authentication enabled")
authN, err = auth.NewAuthenticator(
context.Background(),
config.Auth.AuthNConfig,
config.Auth,
d,
)
if err != nil {
Expand Down
46 changes: 46 additions & 0 deletions service/pkg/server/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package server

type StartOptions func(StartConfig) StartConfig

type StartConfig struct {
ConfigKey string
ConfigFile string
WaitForShutdownSignal bool
PublicRoutes []string
}

// Deprecated: Use WithConfigKey
func WithConfigName(name string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigKey = name
return c
}
}

func WithConfigFile(file string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigFile = file
return c
}
}

func WithConfigKey(key string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigKey = key
return c
}
}

func WithWaitForShutdownSignal() StartOptions {
return func(c StartConfig) StartConfig {
c.WaitForShutdownSignal = true
return c
}
}

func WithPublicRoutes(routes []string) StartOptions {
return func(c StartConfig) StartConfig {
c.PublicRoutes = routes
return c
}
}
42 changes: 5 additions & 37 deletions service/pkg/server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,6 @@ import (
wellknown "github.com/opentdf/platform/service/wellknownconfiguration"
)

type StartOptions func(StartConfig) StartConfig

// Deprecated: Use WithConfigKey
func WithConfigName(name string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigKey = name
return c
}
}

func WithConfigFile(file string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigFile = file
return c
}
}

func WithConfigKey(key string) StartOptions {
return func(c StartConfig) StartConfig {
c.ConfigKey = key
return c
}
}

func WithWaitForShutdownSignal() StartOptions {
return func(c StartConfig) StartConfig {
c.WaitForShutdownSignal = true
return c
}
}

type StartConfig struct {
ConfigKey string
ConfigFile string
WaitForShutdownSignal bool
}

func Start(f ...StartOptions) error {
startConfig := StartConfig{}
for _, fn := range f {
Expand All @@ -71,6 +34,11 @@ func Start(f ...StartOptions) error {
return fmt.Errorf("could not load config: %w", err)
}

// Set allowed public routes when platform is being extended
if len(startConfig.PublicRoutes) > 0 {
conf.Server.Auth.PublicRoutes = startConfig.PublicRoutes
}

slog.Info("starting logger")
logger, err := logger.NewLogger(conf.Logger)
if err != nil {
Expand Down
Loading