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

SAML and OAuth2/OIDC AC with relative callback URLs #265

Merged
merged 4 commits into from
Aug 3, 2021
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Unreleased changes are available as `avenga/couper:edge` container.
* Default values for environment variable by means of `environment_variables` within `defaults` block. ([#271](https://github.com/avenga/couper/pull/271))
* `protocol`, `host`, `port`, `origin`, `body`, `json_body` to [`backend_requests`](./docs/REFERENCE.md#backend_requests) ([#278](https://github.com/avenga/couper/pull/278))

* **Changed**
* The `sp_acs_url` in the [SAML Block](./docs/REFERENCE.md#saml-block) may now be relative ([#265](https://github.com/avenga/couper/pull/265))

* **Fixed**
* No GZIP compression for small response bodies ([#186](https://github.com/avenga/couper/issues/186))
* Missing error type for [request](docs/REFERENCE.md#request-block)/[response](docs/REFERENCE.md#response-block) body, json_body or form_body related HCL evaluation errors ([#276](https://github.com/avenga/couper/pull/276))
Expand Down
9 changes: 9 additions & 0 deletions accesscontrol/saml2.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (

"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/eval/lib"
)

type Saml2 struct {
Expand Down Expand Up @@ -80,6 +82,13 @@ func (s *Saml2) Validate(req *http.Request) error {
return err
}

origin := eval.NewRawOrigin(req.URL)
absAcsUrl, err := lib.AbsoluteURL(s.sp.AssertionConsumerServiceURL, origin)
if err != nil {
return err
}
s.sp.AssertionConsumerServiceURL = absAcsUrl

encodedResponse := req.FormValue("SAMLResponse")
req.ContentLength = 0

Expand Down
6 changes: 3 additions & 3 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ Like all [Access Control](#access-control) types, the `beta_oauth2` block is def
| `authorization_endpoint` | string |-| The authorization server endpoint URL used for authorization. |⚠ required|-|
| `token_endpoint` | string |-| The authorization server endpoint URL used for requesting the token. |⚠ required|-|
| `token_endpoint_auth_method` |string|`client_secret_basic`|Defines the method to authenticate the client at the token endpoint.|If set to `client_secret_post`, the client credentials are transported in the request body. If set to `client_secret_basic`, the client credentials are transported via Basic Authentication.|-|
| `redirect_uri` | string |-| The Couper endpoint for receiving the authorization code. |⚠ required|-|
| `redirect_uri` | string |-| The Couper endpoint for receiving the authorization code. |⚠ required. Relative URL references are resolved against the origin of the current request URL.|-|
| `grant_type` |string|-| The grant type. |⚠ required, to be set to: `authorization_code`|`grant_type = "authorization_code"`|
| `client_id`| string|-|The client identifier.|⚠ required|-|
| `client_secret` |string|-|The client password.|⚠ required.|-|
Expand All @@ -397,7 +397,7 @@ Like all [Access Control](#access-control) types, the `beta_oidc` block is defin
| `configuration_url` | string |-| The OpenID configuration URL. |⚠ required|-|
| `ttl` | duration |-| The duration to cache the OpenID configuration located at `configuration_url`. |⚠ required| `ttl = "1d"` |
| `token_endpoint_auth_method` |string|`client_secret_basic`|Defines the method to authenticate the client at the token endpoint.|If set to `client_secret_post`, the client credentials are transported in the request body. If set to `client_secret_basic`, the client credentials are transported via Basic Authentication.|-|
| `redirect_uri` | string |-| The Couper endpoint for receiving the authorization code. |⚠ required|-|
| `redirect_uri` | string |-| The Couper endpoint for receiving the authorization code. |⚠ required. Relative URL references are resolved against the origin of the current request URL.|-|
| `client_id`| string|-|The client identifier.|⚠ required|-|
| `client_secret` |string|-|The client password.|⚠ required.|-|
| `scope` |string|-| A space separated list of requested scopes for the access token.|`openid` is automatically added.| `scope = "profile read"` |
Expand All @@ -421,7 +421,7 @@ required _label_.
| Attribute(s) | Type |Default|Description|Characteristic(s)| Example|
| :------------------------------ | :--------------- | :--------------- | :--------------- | :--------------- | :--------------- |
|`idp_metadata_file`|string|-|File reference to the Identity Provider metadata XML file.|⚠ required|-|
|`sp_acs_url` |string|-|The URL of the Service Provider's ACS endpoint.|⚠ required|-|
|`sp_acs_url` |string|-|The URL of the Service Provider's ACS endpoint.|⚠ required. Relative URL references are resolved against the origin of the current request URL.|-|
| `sp_entity_id` |string|-|The Service Provider's entity ID.|⚠ required|-|
| `array_attributes`|string|-|A list of assertion attributes that may have several values.|-|-|

Expand Down
27 changes: 16 additions & 11 deletions eval/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
saml: c.saml[:],
}

ctx.createOAuth2Functions()

if rc := req.Context(); rc != nil {
ctx.inner = context.WithValue(rc, ContextType, ctx)
}
Expand Down Expand Up @@ -138,12 +136,14 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
}
port, _ := strconv.ParseInt(p, 10, 64)
body, jsonBody := parseReqBody(req)

origin := NewRawOrigin(req.URL)
ctx.eval.Variables[ClientRequest] = cty.ObjectVal(ctxMap.Merge(ContextMap{
ID: cty.StringVal(id),
Method: cty.StringVal(req.Method),
PathParam: seetie.MapToValue(pathParams),
URL: cty.StringVal(req.URL.String()),
Origin: cty.StringVal(newRawOrigin(req.URL).String()),
Origin: cty.StringVal(origin.String()),
Protocol: cty.StringVal(req.URL.Scheme),
Host: cty.StringVal(req.URL.Hostname()),
Port: cty.NumberIntVal(port),
Expand All @@ -154,6 +154,8 @@ func (c *Context) WithClientRequest(req *http.Request) *Context {
FormBody: seetie.ValuesMapToValue(parseForm(req).PostForm),
}.Merge(newVariable(ctx.inner, req.Cookies(), req.Header))))

ctx.createClientRequestRelatedFunctions(origin)

updateFunctions(ctx)

return ctx
Expand Down Expand Up @@ -196,7 +198,7 @@ func (c *Context) WithBeresps(beresps ...*http.Response) *Context {
bereqs[name] = cty.ObjectVal(ContextMap{
Method: cty.StringVal(bereq.Method),
URL: cty.StringVal(bereq.URL.String()),
Origin: cty.StringVal(newRawOrigin(bereq.URL).String()),
Origin: cty.StringVal(NewRawOrigin(bereq.URL).String()),
Protocol: cty.StringVal(bereq.URL.Scheme),
Host: cty.StringVal(bereq.URL.Hostname()),
Port: cty.NumberIntVal(port),
Expand Down Expand Up @@ -258,29 +260,32 @@ func (c *Context) WithOidcConfig(os map[string]*oidc.OidcConfig) *Context {
return c
}

// WithSAML initially setup the lib.FnSamlSsoUrl function.
// WithSAML initially setup the saml configuration.
johakoch marked this conversation as resolved.
Show resolved Hide resolved
func (c *Context) WithSAML(s []*config.SAML) *Context {
c.saml = s
if c.saml == nil {
c.saml = make([]*config.SAML, 0)
}
samlfn := lib.NewSamlSsoUrlFunction(c.saml)
c.eval.Functions[lib.FnSamlSsoUrl] = samlfn
return c
}

func (c *Context) HCLContext() *hcl.EvalContext {
return c.eval
}

// createOAuth2Functions creates the listed OAuth2 functions for the client request context.
func (c *Context) createOAuth2Functions() {
// createClientRequestRelatedFunctions creates the listed functions for the client request context.
func (c *Context) createClientRequestRelatedFunctions(origin *url.URL) {
if c.oauth2 != nil {
oauth2fn := lib.NewOAuthAuthorizationUrlFunction(c.oauth2, c.getCodeVerifier)
oauth2fn := lib.NewOAuthAuthorizationUrlFunction(c.oauth2, c.getCodeVerifier, origin)
c.eval.Functions[lib.FnOAuthAuthorizationUrl] = oauth2fn
}
c.eval.Functions[lib.FnOAuthVerifier] = lib.NewOAuthCodeVerifierFunction(c.getCodeVerifier)
c.eval.Functions[lib.InternalFnOAuthHashedVerifier] = lib.NewOAuthCodeChallengeFunction(c.getCodeVerifier)

if c.saml != nil {
samlfn := lib.NewSamlSsoUrlFunction(c.saml, origin)
c.eval.Functions[lib.FnSamlSsoUrl] = samlfn
}
}

func (c *Context) getCodeVerifier() (*pkce.CodeVerifier, error) {
Expand Down Expand Up @@ -380,7 +385,7 @@ func parseJSONBytes(b []byte) cty.Value {
return val
}

func newRawOrigin(u *url.URL) *url.URL {
func NewRawOrigin(u *url.URL) *url.URL {
rawOrigin := *u
rawOrigin.Path = ""
rawOrigin.RawQuery = ""
Expand Down
8 changes: 6 additions & 2 deletions eval/lib/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
CodeVerifier = "code_verifier"
)

func NewOAuthAuthorizationUrlFunction(oauth2Configs []config.OAuth2Authorization, verifier func() (*pkce.CodeVerifier, error)) function.Function {
func NewOAuthAuthorizationUrlFunction(oauth2Configs []config.OAuth2Authorization, verifier func() (*pkce.CodeVerifier, error), origin *url.URL) function.Function {
oauth2s := make(map[string]config.OAuth2Authorization)
for _, o := range oauth2Configs {
oauth2s[o.GetName()] = o
Expand Down Expand Up @@ -47,7 +47,11 @@ func NewOAuthAuthorizationUrlFunction(oauth2Configs []config.OAuth2Authorization
query := oauthAuthorizationUrl.Query()
query.Set("response_type", "code")
query.Set("client_id", oauth2.GetClientID())
query.Set("redirect_uri", oauth2.GetRedirectURI())
absRedirectUri, err := AbsoluteURL(oauth2.GetRedirectURI(), origin)
if err != nil {
return cty.StringVal(""), err
}
query.Set("redirect_uri", absRedirectUri)
if scope := oauth2.GetScope(); scope != "" {
query.Set("scope", scope)
}
Expand Down
10 changes: 8 additions & 2 deletions eval/lib/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lib
import (
"encoding/xml"
"fmt"
"net/url"

saml2 "github.com/russellhaering/gosaml2"
"github.com/russellhaering/gosaml2/types"
Expand All @@ -17,7 +18,7 @@ const (
NameIdFormatUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
)

func NewSamlSsoUrlFunction(configs []*config.SAML) function.Function {
func NewSamlSsoUrlFunction(configs []*config.SAML, origin *url.URL) function.Function {
type entity struct {
config *config.SAML
descriptor *types.EntityDescriptor
Expand Down Expand Up @@ -68,8 +69,13 @@ func NewSamlSsoUrlFunction(configs []*config.SAML) function.Function {

nameIDFormat := getNameIDFormat(metadata.IDPSSODescriptor.NameIDFormats)

absAcsUrl, err := AbsoluteURL(ent.config.SpAcsUrl, origin)
if err != nil {
return cty.StringVal(""), err
}

sp := &saml2.SAMLServiceProvider{
AssertionConsumerServiceURL: ent.config.SpAcsUrl,
AssertionConsumerServiceURL: absAcsUrl,
IdentityProviderSSOURL: ssoUrl,
ServiceProviderIssuer: ent.config.SpEntityId,
SignAuthnRequests: false,
Expand Down
8 changes: 6 additions & 2 deletions eval/lib/saml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/xml"
"io"
"net/http"
"net/url"
"strings"
"testing"
Expand Down Expand Up @@ -92,9 +93,12 @@ func Test_SamlSsoUrl(t *testing.T) {
h.Must(err)
}

hclContext := cf.Context.Value(eval.ContextType).(*eval.Context).HCLContext()
evalContext := cf.Context.Value(eval.ContextType).(*eval.Context)
req, err := http.NewRequest(http.MethodGet, "https://www.example.com/foo", nil)
h.Must(err)
evalContext = evalContext.WithClientRequest(req)

ssoUrl, err := hclContext.Functions[lib.FnSamlSsoUrl].Call([]cty.Value{cty.StringVal(tt.samlLabel)})
ssoUrl, err := evalContext.HCLContext().Functions[lib.FnSamlSsoUrl].Call([]cty.Value{cty.StringVal(tt.samlLabel)})
if err == nil && tt.wantErr {
st.Fatal("Error expected")
}
Expand Down
12 changes: 12 additions & 0 deletions eval/lib/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ func newUrlEncodeFunction() function.Function {
},
})
}

func AbsoluteURL(urlRef string, origin *url.URL) (string, error) {
u, err := url.Parse(urlRef)
if err != nil {
return "", err
}

if !u.IsAbs() {
return origin.ResolveReference(u).String(), nil
}
return urlRef, nil
}
10 changes: 7 additions & 3 deletions oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/avenga/couper/config/request"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/eval/lib"
"github.com/avenga/couper/internal/seetie"
)

Expand Down Expand Up @@ -71,9 +72,6 @@ func (c *Client) newTokenRequest(ctx context.Context, requestParams map[string]s
if scope := c.clientConfig.GetScope(); scope != "" && grantType != "authorization_code" {
post.Set("scope", scope)
}
if acClientConfig, ok := c.clientConfig.(config.OAuth2AcClient); ok && grantType == "authorization_code" {
post.Set("redirect_uri", acClientConfig.GetRedirectURI())
}
if requestParams != nil {
for key, value := range requestParams {
post.Set(key, value)
Expand Down Expand Up @@ -163,6 +161,12 @@ func (a AbstractAcClient) GetTokenResponse(ctx context.Context, callbackURL *url
}

requestParams := map[string]string{"code": code}
origin := eval.NewRawOrigin(callbackURL)
absRedirectUri, err := lib.AbsoluteURL(a.getAcClientConfig().GetRedirectURI(), origin)
if err != nil {
return nil, nil, "", err
}
requestParams["redirect_uri"] = absRedirectUri

evalContext, _ := ctx.Value(eval.ContextType).(*eval.Context)

Expand Down
12 changes: 12 additions & 0 deletions server/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3265,6 +3265,12 @@ func TestOAuthPKCEFunctions(t *testing.T) {
if auq.Get("client_id") != "foo" {
t.Errorf("beta_oauth_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
}
au, err = url.Parse(res.Header.Get("x-au-pkce-rel"))
helper.Must(err)
auq = au.Query()
if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" {
t.Errorf("oauth_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback")
}

req, err = http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil)
helper.Must(err)
Expand Down Expand Up @@ -3390,6 +3396,12 @@ func TestOIDCPKCEFunctions(t *testing.T) {
if auq.Get("client_id") != "foo" {
t.Errorf("beta_oauth_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo")
}
au, err = url.Parse(res.Header.Get("x-au-pkce-rel"))
helper.Must(err)
auq = au.Query()
if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" {
t.Errorf("oauth_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback")
}
}

func TestOIDCNonceFunctions(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions server/http_oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ func TestOAuth2AccessControl(t *testing.T) {
{"code; client_secret_post", "05_couper.hcl", "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}}, http.StatusOK, "client_id=foo&client_secret=etbinbp4in&code=qeuboub&code_verifier=qerbnr&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "", ""},
{"code, state param", "06_couper.hcl", "/cb?code=qeuboub&state=" + state, http.Header{"Cookie": []string{"st=" + st}}, http.StatusOK, "code=qeuboub&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""},
{"code, nonce param", "07_couper.hcl", "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st}}, http.StatusOK, "code=qeuboub-id&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""},
{"code; client_secret_basic; PKCE; relative redirect_uri", "08_couper.hcl", "/cb?code=qeuboub", http.Header{"Cookie": []string{"pkcecv=qerbnr"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"www.example.com"}}, http.StatusOK, "code=qeuboub&code_verifier=qerbnr&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fwww.example.com%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""},
{"code; nonce param; relative redirect_uri", "09_couper.hcl", "/cb?code=qeuboub-id", http.Header{"Cookie": []string{"nnc=" + st}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"www.example.com"}}, http.StatusOK, "code=qeuboub-id&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fwww.example.com%2Fcb", "Basic Zm9vOmV0YmluYnA0aW4=", ""},
} {
t.Run(tc.path[1:], func(subT *testing.T) {
shutdown, hook := newCouperWithTemplate("testdata/oauth2/"+tc.filename, test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL})
Expand Down
16 changes: 16 additions & 0 deletions server/testdata/integration/functions/02_couper.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ server "oauth-functions" {
x-v-2 = beta_oauth_verifier()
x-hv = internal_oauth_hashed_verifier()
x-au-pkce = beta_oauth_authorization_url("ac-pkce")
x-au-pkce-rel = beta_oauth_authorization_url("ac-pkce-relative")
}
}
}

endpoint "/csrf" {
response {
headers = {
Expand All @@ -18,6 +20,7 @@ server "oauth-functions" {
}
}
}

definitions {
beta_oauth2 "ac-pkce" {
grant_type = "authorization_code"
Expand All @@ -30,6 +33,19 @@ definitions {
verifier_method = "ccm_s256"
verifier_value = "not_used_here"
}

beta_oauth2 "ac-pkce-relative" {
grant_type = "authorization_code"
authorization_endpoint = "https://authorization.server/oauth/authorize"
scope = "openid profile email"
token_endpoint = "https://authorization.server/oauth/token"
redirect_uri = "/oidc/callback"
client_id = "foo"
client_secret = "5eCr3t"
verifier_method = "ccm_s256"
verifier_value = "not_used_here"
}

beta_oauth2 "ac-state" {
grant_type = "authorization_code"
authorization_endpoint = "https://authorization.server/oauth/authorize"
Expand Down
Loading