-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Retrieval of an access token from the request body (#115)
- Loading branch information
Showing
12 changed files
with
359 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
internal/pipeline/authenticators/extractors/body_parameter_extract_strategy.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package extractors | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/dadrus/heimdall/internal/heimdall" | ||
"github.com/dadrus/heimdall/internal/pipeline/contenttype" | ||
"github.com/dadrus/heimdall/internal/x/errorchain" | ||
) | ||
|
||
type BodyParameterExtractStrategy struct { | ||
Name string | ||
} | ||
|
||
func (es BodyParameterExtractStrategy) GetAuthData(ctx heimdall.Context) (AuthData, error) { | ||
decoder, err := contenttype.NewDecoder(ctx.RequestHeader("Content-Type")) | ||
if err != nil { | ||
return nil, errorchain.New(heimdall.ErrArgument).CausedBy(err) | ||
} | ||
|
||
data, err := decoder.Decode(ctx.RequestBody()) | ||
if err != nil { | ||
return nil, errorchain.NewWithMessage(heimdall.ErrArgument, | ||
"failed to decode request body").CausedBy(err) | ||
} | ||
|
||
entry, ok := data[es.Name] | ||
if !ok { | ||
return nil, errorchain.NewWithMessagef(heimdall.ErrArgument, | ||
"no %s parameter present in request body", es.Name) | ||
} | ||
|
||
var value string | ||
|
||
switch val := entry.(type) { | ||
case string: | ||
value = val | ||
case []string: | ||
if len(val) != 1 { | ||
return nil, errorchain.NewWithMessagef(heimdall.ErrArgument, | ||
"%s request body parameter is present multiple times", es.Name) | ||
} | ||
|
||
value = val[0] | ||
case []any: | ||
if len(val) != 1 { | ||
return nil, errorchain.NewWithMessagef(heimdall.ErrArgument, | ||
"%s request body parameter is present multiple times", es.Name) | ||
} | ||
|
||
value, ok = val[0].(string) | ||
if !ok { | ||
return nil, errorchain.NewWithMessagef(heimdall.ErrArgument, | ||
"unexpected type for %s request body parameter", es.Name) | ||
} | ||
default: | ||
return nil, errorchain.NewWithMessagef(heimdall.ErrArgument, | ||
"unexpected type for %s request body parameter", es.Name) | ||
} | ||
|
||
return &bodyParameterAuthData{ | ||
name: es.Name, | ||
value: strings.TrimSpace(value), | ||
}, nil | ||
} | ||
|
||
type bodyParameterAuthData struct { | ||
name string | ||
value string | ||
} | ||
|
||
func (c *bodyParameterAuthData) ApplyTo(req *http.Request) { | ||
panic("application of extracted body parameters to a request is not yet supported") | ||
} | ||
|
||
func (c *bodyParameterAuthData) Value() string { | ||
return c.value | ||
} |
230 changes: 230 additions & 0 deletions
230
internal/pipeline/authenticators/extractors/body_parameter_extract_strategy_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
package extractors | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/dadrus/heimdall/internal/heimdall" | ||
"github.com/dadrus/heimdall/internal/heimdall/mocks" | ||
) | ||
|
||
func TestExtractBodyParameter(t *testing.T) { | ||
t.Parallel() | ||
|
||
for _, tc := range []struct { | ||
uc string | ||
parameterName string | ||
configureMocks func(t *testing.T, ctx *mocks.MockContext) | ||
assert func(t *testing.T, err error, authData AuthData) | ||
}{ | ||
{ | ||
uc: "unsupported content type", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type").Return("FooBar") | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "unsupported mime type") | ||
}, | ||
}, | ||
{ | ||
uc: "json body decoding error", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type").Return("application/json") | ||
ctx.On("RequestBody").Return([]byte("foo:?:bar")) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "failed to decode") | ||
}, | ||
}, | ||
{ | ||
uc: "form url encoded body decoding error", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/x-www-form-urlencoded") | ||
ctx.On("RequestBody").Return([]byte("foo;")) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "failed to decode") | ||
}, | ||
}, | ||
{ | ||
uc: "json encoded body does not contain required parameter", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/json") | ||
ctx.On("RequestBody").Return([]byte(`{"bar": "foo"}`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "no foobar parameter present") | ||
}, | ||
}, | ||
{ | ||
uc: "form url encoded body does not contain required parameter", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/x-www-form-urlencoded") | ||
ctx.On("RequestBody").Return([]byte(`foo=bar`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "no foobar parameter present") | ||
}, | ||
}, | ||
{ | ||
uc: "json encoded body contains required parameter multiple times", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/json") | ||
ctx.On("RequestBody").Return([]byte(`{"foobar": ["foo", "bar"]}`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "multiple times") | ||
}, | ||
}, | ||
{ | ||
uc: "form url encoded body contains required parameter multiple times", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/x-www-form-urlencoded") | ||
ctx.On("RequestBody").Return([]byte(`foobar=foo&foobar=bar`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "multiple times") | ||
}, | ||
}, | ||
{ | ||
uc: "json encoded body contains required parameter in wrong format #1", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/json") | ||
ctx.On("RequestBody").Return([]byte(`{"foobar": [1]}`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "unexpected type") | ||
}, | ||
}, | ||
{ | ||
uc: "json encoded body contains required parameter in wrong format #2", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/json") | ||
ctx.On("RequestBody").Return([]byte(`{"foobar": { "foo": "bar" }}`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.Error(t, err) | ||
assert.ErrorIs(t, err, heimdall.ErrArgument) | ||
assert.Contains(t, err.Error(), "unexpected type") | ||
}, | ||
}, | ||
{ | ||
uc: "json encoded body contains required parameter", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/json") | ||
ctx.On("RequestBody").Return([]byte(`{"foobar": "foo"}`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, "foo", authData.Value()) | ||
}, | ||
}, | ||
{ | ||
uc: "form url encoded body contains required parameter", | ||
parameterName: "foobar", | ||
configureMocks: func(t *testing.T, ctx *mocks.MockContext) { | ||
t.Helper() | ||
|
||
ctx.On("RequestHeader", "Content-Type"). | ||
Return("application/x-www-form-urlencoded") | ||
ctx.On("RequestBody").Return([]byte(`foobar=foo`)) | ||
}, | ||
assert: func(t *testing.T, err error, authData AuthData) { | ||
t.Helper() | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, "foo", authData.Value()) | ||
}, | ||
}, | ||
} { | ||
t.Run("case="+tc.uc, func(t *testing.T) { | ||
// GIVEN | ||
ctx := &mocks.MockContext{} | ||
tc.configureMocks(t, ctx) | ||
|
||
strategy := BodyParameterExtractStrategy{Name: tc.parameterName} | ||
|
||
// WHEN | ||
authData, err := strategy.GetAuthData(ctx) | ||
|
||
// THEN | ||
tc.assert(t, err, authData) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.