Skip to content

Commit

Permalink
feat: add api key authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Dec 1, 2024
1 parent 710fdf1 commit ac99c3e
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 38 deletions.
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

0 comments on commit ac99c3e

Please sign in to comment.