Skip to content

Commit f95f219

Browse files
committed
Address #243
Adds `AuthenticationFunc` and `config.WithAuthenticationFunc` to the library. default behavior persists, add this and you get the same behavior as kin.
1 parent 1588f3f commit f95f219

4 files changed

Lines changed: 363 additions & 1 deletion

File tree

config/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package config
22

33
import (
4+
"context"
45
"log/slog"
6+
"net/http"
57

8+
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
69
"github.com/santhosh-tekuri/jsonschema/v6"
710

811
"github.com/pb33f/libopenapi-validator/cache"
@@ -18,6 +21,18 @@ type RegexCache interface {
1821
Store(key, value any) // Set a compiled regex to the cache
1922
}
2023

24+
// AuthenticationFunc validates a security scheme for an HTTP request.
25+
// Return nil when the scheme is satisfied; return an error to fail the current security requirement.
26+
type AuthenticationFunc func(context.Context, *AuthenticationInput) error
27+
28+
// AuthenticationInput contains the request and OpenAPI security scheme details passed to an AuthenticationFunc.
29+
type AuthenticationInput struct {
30+
Request *http.Request
31+
SecuritySchemeName string
32+
SecurityScheme *v3.SecurityScheme
33+
Scopes []string
34+
}
35+
2136
// ValidationOptions A container for validation configuration.
2237
//
2338
// Generally fluent With... style functions are used to establish the desired behavior.
@@ -27,6 +42,7 @@ type ValidationOptions struct {
2742
FormatAssertions bool
2843
ContentAssertions bool
2944
SecurityValidation bool
45+
AuthenticationFunc AuthenticationFunc
3046
OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation
3147
AllowScalarCoercion bool // Enable string->boolean/number coercion
3248
Formats map[string]func(v any) error
@@ -77,6 +93,7 @@ func WithExistingOpts(options *ValidationOptions) Option {
7793
o.FormatAssertions = options.FormatAssertions
7894
o.ContentAssertions = options.ContentAssertions
7995
o.SecurityValidation = options.SecurityValidation
96+
o.AuthenticationFunc = options.AuthenticationFunc
8097
o.OpenAPIMode = options.OpenAPIMode
8198
o.AllowScalarCoercion = options.AllowScalarCoercion
8299
o.Formats = options.Formats
@@ -140,6 +157,14 @@ func WithoutSecurityValidation() Option {
140157
}
141158
}
142159

160+
// WithAuthenticationFunc sets a custom function for validating security requirements.
161+
// When set, the function is authoritative for all security scheme types, including oauth2 and openIdConnect.
162+
func WithAuthenticationFunc(fn AuthenticationFunc) Option {
163+
return func(o *ValidationOptions) {
164+
o.AuthenticationFunc = fn
165+
}
166+
}
167+
143168
// WithCustomFormat adds custom formats and their validators that checks for custom 'format' assertions
144169
// When you add different validators with the same name, they will be overridden,
145170
// and only the last registration will take effect.

config/config_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package config
55

66
import (
7+
"context"
78
"log/slog"
89
"sync"
910
"testing"
@@ -79,6 +80,25 @@ func TestWithoutSecurityValidation(t *testing.T) {
7980
assert.Nil(t, opts.RegexCache)
8081
}
8182

83+
func TestWithAuthenticationFunc(t *testing.T) {
84+
called := false
85+
authFn := func(ctx context.Context, input *AuthenticationInput) error {
86+
called = true
87+
assert.NotNil(t, ctx)
88+
assert.Equal(t, "ApiKeyAuth", input.SecuritySchemeName)
89+
return nil
90+
}
91+
92+
opts := NewValidationOptions(WithAuthenticationFunc(authFn))
93+
94+
assert.True(t, opts.SecurityValidation)
95+
assert.NotNil(t, opts.AuthenticationFunc)
96+
assert.NoError(t, opts.AuthenticationFunc(context.Background(), &AuthenticationInput{
97+
SecuritySchemeName: "ApiKeyAuth",
98+
}))
99+
assert.True(t, called)
100+
}
101+
82102
func TestWithRegexEngine(t *testing.T) {
83103
// Test with nil regex engine (valid)
84104
var mockEngine jsonschema.RegexpEngine = nil
@@ -260,6 +280,24 @@ func TestWithExistingOpts_SecurityValidationCopied(t *testing.T) {
260280
assert.True(t, opts2.SecurityValidation)
261281
}
262282

283+
func TestWithExistingOpts_AuthenticationFuncCopied(t *testing.T) {
284+
called := false
285+
authFn := func(context.Context, *AuthenticationInput) error {
286+
called = true
287+
return nil
288+
}
289+
290+
original := &ValidationOptions{
291+
AuthenticationFunc: authFn,
292+
}
293+
294+
opts := NewValidationOptions(WithExistingOpts(original))
295+
296+
assert.NotNil(t, opts.AuthenticationFunc)
297+
assert.NoError(t, opts.AuthenticationFunc(context.Background(), &AuthenticationInput{}))
298+
assert.True(t, called)
299+
}
300+
263301
// Tests for new OpenAPI and scalar coercion configuration options
264302

265303
func TestWithOpenAPIMode(t *testing.T) {

parameters/validate_security.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
1313
"github.com/pb33f/libopenapi/orderedmap"
1414

15+
"github.com/pb33f/libopenapi-validator/config"
1516
"github.com/pb33f/libopenapi-validator/errors"
1617
"github.com/pb33f/libopenapi-validator/helpers"
1718
"github.com/pb33f/libopenapi-validator/paths"
@@ -84,7 +85,7 @@ func (v *paramValidator) ValidateSecurityWithPathItem(request *http.Request, pat
8485
}
8586

8687
secScheme := v.document.Components.SecuritySchemes.GetOrZero(secName)
87-
schemeValid, schemeErrors := v.validateSecurityScheme(secScheme, sec, request, pathValue)
88+
schemeValid, schemeErrors := v.validateSecurityScheme(secName, secScheme, pair.Value(), sec, request, pathValue)
8889
if !schemeValid {
8990
requirementSatisfied = false
9091
requirementErrors = append(requirementErrors, schemeErrors...)
@@ -103,11 +104,17 @@ func (v *paramValidator) ValidateSecurityWithPathItem(request *http.Request, pat
103104

104105
// validateSecurityScheme checks if a single security scheme is satisfied by the request.
105106
func (v *paramValidator) validateSecurityScheme(
107+
secName string,
106108
secScheme *v3.SecurityScheme,
109+
scopes []string,
107110
sec *base.SecurityRequirement,
108111
request *http.Request,
109112
pathValue string,
110113
) (bool, []*errors.ValidationError) {
114+
if v.options.AuthenticationFunc != nil {
115+
return v.validateAuthenticationFunc(secName, secScheme, scopes, sec, request, pathValue)
116+
}
117+
111118
switch strings.ToLower(secScheme.Type) {
112119
case "http":
113120
return v.validateHTTPSecurityScheme(secScheme, sec, request, pathValue)
@@ -118,6 +125,39 @@ func (v *paramValidator) validateSecurityScheme(
118125
return true, nil
119126
}
120127

128+
func (v *paramValidator) validateAuthenticationFunc(
129+
secName string,
130+
secScheme *v3.SecurityScheme,
131+
scopes []string,
132+
sec *base.SecurityRequirement,
133+
request *http.Request,
134+
pathValue string,
135+
) (bool, []*errors.ValidationError) {
136+
authErr := v.options.AuthenticationFunc(request.Context(), &config.AuthenticationInput{
137+
Request: request,
138+
SecuritySchemeName: secName,
139+
SecurityScheme: secScheme,
140+
Scopes: scopes,
141+
})
142+
if authErr == nil {
143+
return true, nil
144+
}
145+
146+
validationErrors := []*errors.ValidationError{
147+
{
148+
Message: fmt.Sprintf("Authentication failed for security scheme '%s'", secName),
149+
Reason: authErr.Error(),
150+
ValidationType: helpers.SecurityValidation,
151+
ValidationSubType: secScheme.Type,
152+
SpecLine: sec.GoLow().Requirements.ValueNode.Line,
153+
SpecCol: sec.GoLow().Requirements.ValueNode.Column,
154+
HowToFix: fmt.Sprintf("Provide valid credentials for security scheme '%s'", secName),
155+
},
156+
}
157+
errors.PopulateValidationErrors(validationErrors, request, pathValue)
158+
return false, validationErrors
159+
}
160+
121161
func (v *paramValidator) validateHTTPSecurityScheme(
122162
secScheme *v3.SecurityScheme,
123163
sec *base.SecurityRequirement,

0 commit comments

Comments
 (0)