Skip to content

added securityScheme #226

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 12 commits into from
Dec 6, 2024
25 changes: 25 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fuego

import (
"fmt"
"net/http"

"github.com/getkin/kin-openapi/openapi3"
Expand Down Expand Up @@ -267,3 +268,27 @@ func OptionHide() func(*BaseRoute) {
r.Hidden = true
}
}

func OptionSecurity(securityRequirements ...openapi3.SecurityRequirement) func(*BaseRoute) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe the comment could be added here

return func(r *BaseRoute) {
if r.mainRouter.OpenApiSpec.Components == nil {
panic("zero security schemes have been registered with the server")
}

// Validate the security scheme exists in components
for _, req := range securityRequirements {
for schemeName := range req {
if _, exists := r.mainRouter.OpenApiSpec.Components.SecuritySchemes[schemeName]; !exists {
panic(fmt.Sprintf("security scheme '%s' not defined in components", schemeName))
}
}
}

if r.Operation.Security == nil {
r.Operation.Security = &openapi3.SecurityRequirements{}
}

// Append all provided security requirements
*r.Operation.Security = append(*r.Operation.Security, securityRequirements...)
}
}
32 changes: 32 additions & 0 deletions option/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,38 @@ var Summary = fuego.OptionSummary
// Description adds a description to the route.
var Description = fuego.OptionDescription

// OptionSecurity configures security requirements to the route.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// OptionSecurity configures security requirements to the route.
// Security configures security requirements to the route.

You renamed it

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please update this. And this should be good to go. 😄

//
// Single Scheme (AND Logic):
//
// Add a single security requirement with multiple schemes.
// All schemes must be satisfied:
// OptionSecurity(openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read"] // AND requires oauth with read scope
// })
//
// Multiple Schemes (OR Logic):
//
// Add multiple security requirements.
// At least one requirement must be satisfied:
// OptionSecurity(
// openapi3.SecurityRequirement{"basic": []}, // First option
// openapi3.SecurityRequirement{"oauth2": ["read"]} // Alternative option
// })
//
// Mixing Approaches:
//
// Combine AND logic within requirements and OR logic between requirements:
// OptionSecurity(
// openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read:user"] // AND oauth with read:user scope
// },
// openapi3.SecurityRequirement{"apiKey": []} // OR alternative with API key
// })
Comment on lines +96 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same

Suggested change
// OptionSecurity(openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read"] // AND requires oauth with read scope
// })
//
// Multiple Schemes (OR Logic):
//
// Add multiple security requirements.
// At least one requirement must be satisfied:
// OptionSecurity(
// openapi3.SecurityRequirement{"basic": []}, // First option
// openapi3.SecurityRequirement{"oauth2": ["read"]} // Alternative option
// })
//
// Mixing Approaches:
//
// Combine AND logic within requirements and OR logic between requirements:
// OptionSecurity(
// openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read:user"] // AND oauth with read:user scope
// },
// openapi3.SecurityRequirement{"apiKey": []} // OR alternative with API key
// })
// Security(openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read"] // AND requires oauth with read scope
// })
//
// Multiple Schemes (OR Logic):
//
// Add multiple security requirements.
// At least one requirement must be satisfied:
// Security(
// openapi3.SecurityRequirement{"basic": []}, // First option
// openapi3.SecurityRequirement{"oauth2": ["read"]} // Alternative option
// })
//
// Mixing Approaches:
//
// Combine AND logic within requirements and OR logic between requirements:
// Security(
// openapi3.SecurityRequirement{
// "basic": [], // Requires basic auth
// "oauth2": ["read:user"] // AND oauth with read:user scope
// },
// openapi3.SecurityRequirement{"apiKey": []} // OR alternative with API key
// })

var Security = fuego.OptionSecurity

// OperationID adds an operation ID to the route.
var OperationID = fuego.OptionOperationID

Expand Down
239 changes: 239 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http/httptest"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
"github.com/thejerf/slogassert"

Expand Down Expand Up @@ -443,3 +444,241 @@ func TestHide(t *testing.T) {
require.Equal(t, "hello world", w.Body.String())
})
}

func TestSecurity(t *testing.T) {
t.Run("single security requirement with defined scheme", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"basic": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("http").
WithScheme("basic"),
},
}),
)

basic := openapi3.SecurityRequirement{
"basic": []string{},
}
route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(basic),
)

require.NotNil(t, route.Operation.Security)
require.Len(t, *route.Operation.Security, 1)
require.Contains(t, (*route.Operation.Security)[0], "basic")
require.Empty(t, (*route.Operation.Security)[0]["basic"])

r := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
s.Mux.ServeHTTP(w, r)
require.Equal(t, "hello world", w.Body.String())
})

t.Run("security with scopes and defined scheme", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"oauth2": &openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{
Type: "oauth2",
Flows: &openapi3.OAuthFlows{
AuthorizationCode: &openapi3.OAuthFlow{
AuthorizationURL: "https://example.com/oauth/authorize",
TokenURL: "https://example.com/oauth/token",
Scopes: map[string]string{
"read:users": "Read user information",
},
},
},
},
},
}),
)

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"oauth2": []string{"read:users", "write:users"},
},
),
)

require.NotNil(t, route.Operation.Security)
require.Len(t, *route.Operation.Security, 1)
require.Contains(t, (*route.Operation.Security)[0], "oauth2")
require.Equal(t,
[]string{"read:users", "write:users"},
(*route.Operation.Security)[0]["oauth2"],
)
})

t.Run("AND combination with defined schemes", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"basic": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("http").
WithScheme("basic"),
},
"oauth2": &openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{
Type: "oauth2",
Flows: &openapi3.OAuthFlows{
AuthorizationCode: &openapi3.OAuthFlow{
AuthorizationURL: "https://example.com/oauth/authorize",
TokenURL: "https://example.com/oauth/token",
Scopes: map[string]string{
"read:users": "Read user information",
},
},
},
},
},
}),
)

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"basic": []string{},
"oauth2": []string{"read:users"},
},
),
)

require.NotNil(t, route.Operation.Security)
require.Len(t, *route.Operation.Security, 1)
require.Contains(t, (*route.Operation.Security)[0], "basic")
require.Empty(t, (*route.Operation.Security)[0]["basic"])
require.Contains(t, (*route.Operation.Security)[0], "oauth2")
require.Equal(t, []string{"read:users"}, (*route.Operation.Security)[0]["oauth2"])
})

t.Run("OR combination with defined schemes", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"basic": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("http").
WithScheme("basic"),
},
"oauth2": &openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{
Type: "oauth2",
Flows: &openapi3.OAuthFlows{
AuthorizationCode: &openapi3.OAuthFlow{
AuthorizationURL: "https://example.com/oauth/authorize",
TokenURL: "https://example.com/oauth/token",
Scopes: map[string]string{
"read:users": "Read user information",
},
},
},
},
},
}),
)

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"basic": []string{},
},
openapi3.SecurityRequirement{
"oauth2": []string{"read:users"},
},
),
)

require.NotNil(t, route.Operation.Security)
require.Len(t, *route.Operation.Security, 2)
require.Contains(t, (*route.Operation.Security)[0], "basic")
require.Empty(t, (*route.Operation.Security)[0]["basic"])
require.Contains(t, (*route.Operation.Security)[1], "oauth2")
require.Equal(t, []string{"read:users"}, (*route.Operation.Security)[1]["oauth2"])
})

t.Run("panic on undefined security scheme", func(t *testing.T) {
s := fuego.NewServer()

require.Panics(t, func() {
fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"undefined": []string{},
},
),
)
})
})

t.Run("panic on partially undefined schemes", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"basic": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("http").
WithScheme("basic"),
},
}),
)

require.Panics(t, func() {
fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"basic": []string{},
"undefined": []string{},
},
),
)
})
})

t.Run("empty security options", func(t *testing.T) {
s := fuego.NewServer()

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(),
)

require.NotNil(t, route.Operation.Security)
require.Empty(t, (*route.Operation.Security))
})

t.Run("multiple security options with different scopes", func(t *testing.T) {
s := fuego.NewServer(
fuego.WithSecurity(openapi3.SecuritySchemes{
"Bearer": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("http").
WithScheme("bearer"),
},
"ApiKey": &openapi3.SecuritySchemeRef{
Value: openapi3.NewSecurityScheme().
WithType("apiKey").
WithIn("header").
WithName("X-API-Key"),
},
}),
)

route := fuego.Get(s, "/test", helloWorld,
fuego.OptionSecurity(
openapi3.SecurityRequirement{
"Bearer": []string{"read"},
"ApiKey": []string{"basic"},
},
),
)

require.NotNil(t, route.Operation.Security)
require.Len(t, *route.Operation.Security, 1)

security := (*route.Operation.Security)[0]
require.Contains(t, security, "Bearer")
require.Equal(t, []string{"read"}, security["Bearer"])
require.Contains(t, security, "ApiKey")
require.Equal(t, []string{"basic"}, security["ApiKey"])
})
}
26 changes: 26 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,32 @@ func WithGlobalResponseTypes(code int, description string, errorType ...any) fun
}
}

// WithSecurity configures security schemes in the OpenAPI specification.
// It allows setting up authentication methods like JWT Bearer tokens, API keys, OAuth2, etc.
// For example:
//
// app := fuego.NewServer(
// fuego.WithSecurity(map[string]*openapi3.SecuritySchemeRef{
// "bearerAuth": &openapi3.SecuritySchemeRef{
// Value: openapi3.NewSecurityScheme().
// WithType("http").
// WithScheme("bearer").
// WithBearerFormat("JWT").
// WithDescription("Enter your JWT token in the format: Bearer <token>"),
// },
// }),
// )
func WithSecurity(schemes openapi3.SecuritySchemes) func(*Server) {
return func(s *Server) {
if s.OpenApiSpec.Components.SecuritySchemes == nil {
s.OpenApiSpec.Components.SecuritySchemes = openapi3.SecuritySchemes{}
}
for name, scheme := range schemes {
s.OpenApiSpec.Components.SecuritySchemes[name] = scheme
}
}
}

// WithoutAutoGroupTags disables the automatic grouping of routes by tags.
// By default, routes are tagged by group.
// For example:
Expand Down
Loading