Skip to content
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

Add api key authentication support #228

Merged
merged 1 commit into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 70 additions & 7 deletions .github/workflows/scans.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,75 @@ jobs:
if: ${{ always() }}
run: docker stop $(docker ps -q --filter ancestor=ghcr.io/cerberauth/api-vulns-challenges/${{ matrix.challenge }}:latest)

run-api-key-scans:
name: JWT Scans
run-header-strong-api-key-scan:
name: Strong API Key Scan
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run Server
run: docker run -d -p 8080:8080 ghcr.io/cerberauth/api-vulns-challenges/strong-api-key:latest

- name: Setup Go environment
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: VulnAPI
id: vulnapi
run: |
go run main.go scan curl http://localhost:8080 -H "X-API-Key: abcdef1234" --sqa-opt-out

- name: Stop Server
if: ${{ always() }}
run: docker stop $(docker ps -q --filter ancestor=ghcr.io/cerberauth/api-vulns-challenges/strong-api-key:latest)

run-header-api-key-scan:
name: API Key in header Scan
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run Server
run: docker run -d -p 8080:8080 ghcr.io/cerberauth/api-vulns-challenges/auth-not-verified:latest

- name: Setup Go environment
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: VulnAPI
id: vulnapi
continue-on-error: true
run: |
go run main.go scan curl http://localhost:8080 -H "X-API-Key: abcdef1234" --sqa-opt-out

- name: Check for vulnerabilities
if: ${{ steps.vulnapi.outputs.conclusion == 'failure' }}
run: echo "Vulnerabilities found"

- name: Stop Server
if: ${{ always() }}
run: docker stop $(docker ps -q --filter ancestor=ghcr.io/cerberauth/api-vulns-challenges/auth-not-verified:latest)

run-bearer-api-key-scan:
name: Bearer API Key Scan
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -209,7 +276,7 @@ jobs:
run: docker stop $(docker ps -q --filter ancestor=ghcr.io/cerberauth/api-vulns-challenges/apollo:latest)

run-openapi-scans:
name: JWT Scans
name: OpenAPI Scans
runs-on: ubuntu-latest

strategy:
Expand All @@ -235,10 +302,6 @@ jobs:
- name: Run Server
run: docker run -d -p 8080:8080 ghcr.io/cerberauth/api-vulns-challenges/auth-not-verified:latest

- name: Get JWT
id: get-jwt
run: echo "jwt=$(docker run --rm ghcr.io/cerberauth/api-vulns-challenges/jwt-strong-eddsa-key:latest jwt)" >> $GITHUB_OUTPUT

- name: Setup Go environment
uses: actions/setup-go@v5
with:
Expand Down
4 changes: 2 additions & 2 deletions internal/operation/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package operation

import (
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -109,7 +109,7 @@ func (operation *Operation) IsReachable() error {
case "https":
host += ":443"
default:
return errors.New("unsupported scheme")
return fmt.Errorf("unsupported scheme: %s", operation.URL.Scheme)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/operation/operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func TestOperation_IsReachableWhenUnsupportedScheme(t *testing.T) {
err := operation.IsReachable()

assert.Error(t, err)
assert.Equal(t, "unsupported scheme", err.Error())
assert.Equal(t, "unsupported scheme: ftp", err.Error())
}

func TestNewOperationFromRequest(t *testing.T) {
Expand Down
16 changes: 9 additions & 7 deletions openapi/security_scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,14 @@ func NewErrUnsupportedSecuritySchemeType(schemeType string) error {
}

func mapHTTPSchemeType(name string, scheme *openapi3.SecuritySchemeRef, securitySchemeValue *string) (*auth.SecurityScheme, error) {
schemeScheme := strings.ToLower(scheme.Value.Scheme)

switch schemeScheme {
switch schemeScheme := strings.ToLower(scheme.Value.Scheme); schemeScheme {
case BearerScheme:
securityScheme, err := auth.NewAuthorizationBearerSecurityScheme(name, securitySchemeValue)
if err != nil {
return nil, err
}

bearerFormat := strings.ToLower(scheme.Value.BearerFormat)
switch bearerFormat {
switch bearerFormat := strings.ToLower(scheme.Value.BearerFormat); bearerFormat {
case "":
return securityScheme, nil
case "jwt":
Expand All @@ -66,6 +63,10 @@ func mapHTTPSchemeType(name string, scheme *openapi3.SecuritySchemeRef, security
}
}

func mapAPIKeySchemeType(name string, scheme *openapi3.SecuritySchemeRef, securitySchemeValue *string) (*auth.SecurityScheme, error) {
return auth.NewAPIKeySecurityScheme(name, auth.SchemeIn(scheme.Value.In), securitySchemeValue)
}

func mapOAuth2SchemeType(name string, scheme *openapi3.SecuritySchemeRef, securitySchemeValue *auth.OAuthValue) (*auth.SecurityScheme, error) {
if scheme.Value.Flows == nil {
return auth.NewOAuthSecurityScheme(name, nil, securitySchemeValue, nil)
Expand Down Expand Up @@ -113,8 +114,7 @@ func (openapi *OpenAPI) SecuritySchemeMap(values *SecuritySchemeValues) (auth.Se
value, _ = securitySchemeValue.(*string)
}

schemeType := strings.ToLower(scheme.Value.Type)
switch schemeType {
switch schemeType := strings.ToLower(scheme.Value.Type); schemeType {
case HttpSchemeType:
securitySchemes[name], err = mapHTTPSchemeType(name, scheme, value)
case OAuth2SchemeType, OpenIdConnectSchemeType:
Expand All @@ -123,6 +123,8 @@ func (openapi *OpenAPI) SecuritySchemeMap(values *SecuritySchemeValues) (auth.Se
oauthValue = auth.NewOAuthValue(*value, nil, nil, nil)
}
securitySchemes[name], err = mapOAuth2SchemeType(name, scheme, oauthValue)
case ApiKeySchemeType:
securitySchemes[name], err = mapAPIKeySchemeType(name, scheme, value)
default:
err = NewErrUnsupportedSecuritySchemeType(schemeType)
}
Expand Down
40 changes: 27 additions & 13 deletions openapi/security_scheme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func TestSecuritySchemeMap_WithoutSecurityComponents(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}}}}}`),
)

Expand All @@ -25,7 +25,7 @@ func TestSecuritySchemeMap_WithoutSecurityComponents(t *testing.T) {
func TestSecuritySchemeMap_WithUnknownSchemeType(t *testing.T) {
expectedErr := openapi.NewErrUnsupportedSecuritySchemeType("other")
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: other}}}}`),
)

Expand All @@ -39,7 +39,7 @@ func TestSecuritySchemeMap_WithUnknownSchemeType(t *testing.T) {
func TestSecuritySchemeMap_WithUnknownScheme(t *testing.T) {
expectedErr := openapi.NewErrUnsupportedScheme("other")
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: other}}}}`),
)

Expand All @@ -53,7 +53,7 @@ func TestSecuritySchemeMap_WithUnknownScheme(t *testing.T) {
func TestSecuritySchemeMap_WithUnknownBearerFormat(t *testing.T) {
expectedErr := openapi.NewErrUnsupportedBearerFormat("other")
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: bearer, bearerFormat: other}}}}`),
)

Expand All @@ -66,7 +66,7 @@ func TestSecuritySchemeMap_WithUnknownBearerFormat(t *testing.T) {

func TestSecuritySchemeMap_WithHTTPJWTBearer(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: bearer, bearerFormat: JWT}}}}`),
)

Expand All @@ -81,7 +81,7 @@ func TestSecuritySchemeMap_WithHTTPJWTBearer(t *testing.T) {

func TestSecuritySchemeMap_WithHTTPBearer(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: bearer}}}}`),
)

Expand All @@ -95,7 +95,7 @@ func TestSecuritySchemeMap_WithHTTPBearer(t *testing.T) {

func TestSecuritySchemeMap_WithoutHTTPJWTBearerAndDefaultValue(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: bearer, bearerFormat: JWT}}}}`),
)

Expand All @@ -109,9 +109,23 @@ func TestSecuritySchemeMap_WithoutHTTPJWTBearerAndDefaultValue(t *testing.T) {
assert.Equal(t, auth.JWTTokenFormat, *result["bearer_auth"].GetTokenFormat())
}

func TestSecuritySchemeMap_WithAPIKeyInHeader(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [{name: 'Authorization', in: header, required: true, schema: {type: string}}], responses: {'204': {description: successful operation}}, security: [{api_key_auth: []}]}}}, components: {securitySchemes: {api_key_auth: {type: apiKey, in: header, name: X-API-KEY}}}}`),
)

result, err := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())

assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, auth.ApiKey, result["api_key_auth"].GetType())
assert.Equal(t, auth.InHeader, *result["api_key_auth"].In)
}

func TestSecuritySchemeMap_WithInvalidValueType(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{bearer_auth: []}]}}}, components: {securitySchemes: {bearer_auth: {type: http, scheme: bearer, bearerFormat: JWT}}}}`),
)

Expand All @@ -126,7 +140,7 @@ func TestSecuritySchemeMap_WithInvalidValueType(t *testing.T) {

func TestSecuritySchemeMap_WithOAuth(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{oauth_auth: []}]}}}, components: {securitySchemes: {oauth_auth: {type: oauth2}}}}`),
)

Expand All @@ -140,7 +154,7 @@ func TestSecuritySchemeMap_WithOAuth(t *testing.T) {

func TestSecuritySchemeMap_WithOAuthAndAuthorizationCodeFlow(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{oauth_auth: []}]}}}, components: {securitySchemes: {oauth_auth: {type: oauth2, flows: {authorizationCode: {tokenUrl: 'http://localhost:8080/token', refreshUrl: 'http://localhost:8080/refresh'}}}}}}`),
)

Expand All @@ -158,7 +172,7 @@ func TestSecuritySchemeMap_WithOAuthAndAuthorizationCodeFlow(t *testing.T) {

func TestSecuritySchemeMap_WithOAuthAndImplicitFlow(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{oauth_auth: []}]}}}, components: {securitySchemes: {oauth_auth: {type: oauth2, flows: {implicit: {tokenUrl: 'http://localhost:8080/token', refreshUrl: 'http://localhost:8080/refresh'}}}}}}`),
)

Expand All @@ -176,7 +190,7 @@ func TestSecuritySchemeMap_WithOAuthAndImplicitFlow(t *testing.T) {

func TestSecuritySchemeMap_WithOAuthAndClientCredentialsFlow(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{oauth_auth: []}]}}}, components: {securitySchemes: {oauth_auth: {type: oauth2, flows: {clientCredentials: {tokenUrl: 'http://localhost:8080/token', refreshUrl: 'http://localhost:8080/refresh'}}}}}}`),
)

Expand All @@ -194,7 +208,7 @@ func TestSecuritySchemeMap_WithOAuthAndClientCredentialsFlow(t *testing.T) {

func TestSecuritySchemeMap_WithOpenIDConnect(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
context.TODO(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {get: {parameters: [], responses: {'204': {description: successful operation}}, security: [{oidc_auth: []}]}}}, components: {securitySchemes: {oidc_auth: {type: openIdConnect}}}}`),
)

Expand Down
45 changes: 45 additions & 0 deletions scenario/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,48 @@ func TestNewURLScanWithLowerCaseAuthorizationHeader(t *testing.T) {
assert.Equal(t, http.MethodGet, s.Operations[0].Method)
assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAuthorizationBearerSecurityScheme("default", &token)}, s.Operations[0].SecuritySchemes)
}

func TestNewURLScanWithAPIKeyInHeader(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()

apiKey := "token"
tests := []struct {
name string
}{
{
name: "X-Api-Key",
},
{
name: "Apikey",
},
{
name: "App-Key",
},
{
name: "X-Token",
},
{
name: "Api-Secret",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header := http.Header{}
header.Add(tt.name, apiKey)
client := request.NewClient(request.NewClientOptions{
Header: header,
})

s, err := scenario.NewURLScan(http.MethodGet, server.URL, "", client, nil)

require.NoError(t, err)
assert.Equal(t, server.URL, s.Operations[0].URL.String())
assert.Equal(t, http.MethodGet, s.Operations[0].Method)
assert.Equal(t, []*auth.SecurityScheme{auth.MustNewAPIKeySecurityScheme(tt.name, auth.InHeader, &apiKey)}, s.Operations[0].SecuritySchemes)
})
}
}
Loading