Skip to content
This repository was archived by the owner on Jul 28, 2021. It is now read-only.

Commit 8764ccf

Browse files
author
Mario Hros
committed
Use state parameter to encode the original visited URL and support fixed OIDC callback URL
Use OIDC state parameter which is later returned during the auth flow to encode the original visited URL instead of using a cookie. Using a cookie is unnecessary in this case and even causes race conditions if multiple requests are made to a different OIDC-protected targets at the same time. Additionally, the previously-used cookie for this auth flow purpose was named the same as the session cookie, which could even effectively overwrite an existing valid session cookie. This commit also implements support for fixed callback URI/URLs as many public OIDC providers require admins to configure a fixed return/callback URLs in advance for every client to prevent abuse (e.g. by uploading a fake OIDC handler script on a different URI using some unprotected uploader and stealing tokens returned by the OIDC provider to this callback URL).
1 parent ffc565b commit 8764ccf

File tree

16 files changed

+415
-198
lines changed

16 files changed

+415
-198
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Istio uses an Envoy proxy sidecar to mediate all inbound and outbound traffic fo
2929

3030
### Protecting frontend apps
3131

32-
If you're using a browser based application, you can use the [Open ID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) / OAuth 2.0 `authorization_grant` flow to authenticate your users. When an unauthenticated user is detected, they are automatically redirected to the authentication page. When the authentication completes, the browser is redirected to an implicit `/oidc/callback` endpoint where the adapter intercepts the request. At this point, the adapter obtains tokens from the identity provider and then redirects the user back to their originally requested URL.
32+
If you're using a browser based application, you can use the [Open ID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) / OAuth 2.0 `authorization_grant` flow to authenticate your users. When an unauthenticated user is detected, they are automatically redirected to the authentication page. When the authentication completes, the browser is redirected to an implicit `/oidc/callback` endpoint where the adapter intercepts the request. At this point, the adapter obtains tokens from the identity provider and then redirects the user back to their originally requested URL. Instead of the implicit `/oidc/callback` appended to the original URI, you can configure a different absolute URI or full URL to the callback in the OidcConfig.
3333

3434
To view the user session information including the session tokens, you can look in the `Authorization` header.
3535

@@ -137,7 +137,11 @@ Depending on whether you're protecting frontend or backend applications, create
137137
| `clientSecretRef` | object | no | A reference secret that is used to authenticate the client. This can be used in place of the `clientSecret`. |
138138
| `clientSecretRef.name` | string |yes | The name of the Kubernetes Secret that contains the `clientSecret`. |
139139
| `clientSecretRef.key` | string | yes | The field within the Kubernetes Secret that contains the `clientSecret`. |
140+
| `callback` | string | no | Callback URL or URI to return to from the IODC provider (default is relative `oidc/callback`) |
140141
142+
**Callback**
143+
144+
The default callback is `oidc/callback` in the relative form, which means that for the original request path `/something`, the redirect URL into which the OIDC provider will redirect will be `scheme://host/something/oidc/callback`. Each URI you are protecting will have its own callback URL generated. Some providers only support a fixed return URL, though. To make a fixed URI, you can specify an absolute callback URI like `/oidc/callback` which will form a URL with original request scheme and host. Or you can set a full URL to the callback in the form `https://host/callback/url`. In any case, you are responsible to ensure the routes and Policy are all set up so that the resulting callback URL is handled by the adapter.
141145
142146
* For backend applications: The OAuth 2.0 Bearer token spec defines a pattern for protecting APIs by using [JSON Web Tokens (JWTs)](https://tools.ietf.org/html/rfc7519.html). Using the following configuration as an example, define a `JwtConfig` CRD that contains the public key resource, which is used to validate token signatures.
143147

adapter/client/callback.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package client
2+
3+
import (
4+
"path"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
var regexpURL = regexp.MustCompile(`(https?)://([^/]+)(/?.*)`)
10+
11+
func callbackDefinitionForClient(c Client) string {
12+
callbackDef := ""
13+
14+
// also support empty clients (for tests without a valid client, using default callbackEndpoint)
15+
if c != nil {
16+
callbackDef = c.Callback()
17+
}
18+
19+
if callbackDef == "" {
20+
// if the callback is empty for the client, use the original behavior:
21+
// /oidc/callback is simply appended to the original path, resulting in many different callback URLs
22+
callbackDef = "oidc/callback"
23+
}
24+
return callbackDef
25+
}
26+
27+
// CallbackURLForTarget returns the absolute URL to which IdP should redirect after authenticating the user.
28+
// This is the speckial URL which is expected to be handled by the adapter under the provided Client configuration.
29+
func CallbackURLForTarget(c Client, requestScheme, requestHost, requestPath string) string {
30+
callbackDef := callbackDefinitionForClient(c)
31+
32+
if requestPath == "" {
33+
requestPath = "/"
34+
}
35+
36+
if callbackDef[0] == '/' {
37+
// callback path absolute on the request host
38+
return requestScheme + "://" + requestHost + callbackDef
39+
}
40+
41+
m := regexpURL.FindStringSubmatch(callbackDef)
42+
if len(m) > 1 {
43+
// callback is full URL
44+
return callbackDef
45+
}
46+
47+
// callback path relative to the target path
48+
if strings.HasSuffix(requestPath, callbackDef) {
49+
// relative callback path already appended, do not append twice
50+
// this can happen if this function is called in the actual callback handler
51+
// (happens after /authorize OIDC provider URL redirects back and we call OIDC provider API again)
52+
callbackDef = ""
53+
}
54+
return requestScheme + "://" + requestHost + path.Join(requestPath, callbackDef)
55+
}
56+
57+
// IsCallbackRequest returns true if the provided request should be handled by the adapter as part of the auth flow
58+
func IsCallbackRequest(c Client, requestScheme, requestHost, requestPath string) bool {
59+
callbackDef := callbackDefinitionForClient(c)
60+
61+
if requestPath == "" {
62+
requestPath = "/"
63+
}
64+
65+
if callbackDef[0] == '/' {
66+
// callback path absolute on the request host
67+
return strings.HasPrefix(requestPath, callbackDef)
68+
}
69+
70+
m := regexpURL.FindStringSubmatch(callbackDef)
71+
if len(m) == 4 {
72+
// callback is full URL, compare parts (case-insensitive just in case)
73+
return strings.EqualFold(m[1], requestScheme) &&
74+
strings.EqualFold(m[2], requestHost) &&
75+
strings.EqualFold(m[3], requestPath)
76+
}
77+
78+
// callback path relative to the target path, thus ending with callbackDef
79+
return strings.HasSuffix(requestPath, callbackDef)
80+
}

adapter/client/callback_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package client
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
"github.com/ibm-cloud-security/app-identity-and-access-adapter/tests/fake"
8+
)
9+
10+
func TestCallbackURLForTarget(t *testing.T) {
11+
tests := []struct {
12+
CallbackDef string
13+
Scheme, Host, Path string
14+
Result string
15+
}{
16+
{ // http scheme test
17+
"",
18+
"http", "test.com", "/admin",
19+
"http://test.com/admin/oidc/callback",
20+
},
21+
{ // default relative callback
22+
"",
23+
"https", "test.com", "/admin",
24+
"https://test.com/admin/oidc/callback",
25+
},
26+
{ // relative URI
27+
"custom/relative/path",
28+
"https", "test.com", "/admin",
29+
"https://test.com/admin/custom/relative/path",
30+
},
31+
{ // relative URI already appended
32+
"custom/relative/path",
33+
"https", "test.com", "/admin/custom/relative/path",
34+
"https://test.com/admin/custom/relative/path",
35+
},
36+
{ // absolute URI
37+
"/absolute/uri/callback",
38+
"https", "test.com", "/admin",
39+
"https://test.com/absolute/uri/callback",
40+
},
41+
{ // full URL (http)
42+
"http://test.com/my/callback",
43+
"http", "test.com", "/admin",
44+
"http://test.com/my/callback",
45+
},
46+
{ // full URL (https)
47+
"https://test.com/my/callback",
48+
"https", "test.com", "/admin",
49+
"https://test.com/my/callback",
50+
},
51+
}
52+
53+
for n, tst := range tests {
54+
cli := fake.NewClientWithCallback(nil, tst.CallbackDef)
55+
56+
res := CallbackURLForTarget(cli, tst.Scheme, tst.Host, tst.Path)
57+
if res != tst.Result {
58+
t.Fatalf("TestCallbackURLForTarget#%d CallbackURLForTarget failed.\n"+
59+
" Expected: %s\n Got: %s", n, tst.Result, res)
60+
}
61+
62+
resURL, err := url.Parse(res)
63+
if err != nil {
64+
t.Fatalf("TestCallbackURLForTarget#%d failed to parse URL returned from CallbackURLForTarget %s: %s",
65+
n, res, err.Error())
66+
}
67+
68+
if !IsCallbackRequest(cli, resURL.Scheme, resURL.Host, resURL.Path) {
69+
t.Fatalf("TestCallbackURLForTarget#%d IsCallbackRequest check not returning true\n"+
70+
" scheme: %s, host: %s, path: %s", n, resURL.Scheme, resURL.Host, resURL.Path)
71+
}
72+
}
73+
}

adapter/client/client.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import (
66
"go.uber.org/zap"
77

88
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/authserver"
9-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/pkg/apis/policies/v1"
9+
v1 "github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/pkg/apis/policies/v1"
1010
)
1111

1212
// Client encapsulates an authn/z client object
1313
type Client interface {
1414
Name() string
1515
ID() string
16+
Callback() string
1617
Secret() string
1718
AuthorizationServer() authserver.AuthorizationServerService
1819
ExchangeGrantCode(code string, redirectURI string) (*authserver.TokenResponse, error)
@@ -32,6 +33,10 @@ func (c *remoteClient) ID() string {
3233
return c.ClientID
3334
}
3435

36+
func (c *remoteClient) Callback() string {
37+
return c.ClientCallback
38+
}
39+
3540
func (c *remoteClient) Secret() string {
3641
return c.ClientSecret
3742
}

adapter/config/config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ type Config struct {
1414
// The blockKey is used to encrypt the cookie value
1515
// Valid lengths are 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
1616
BlockKeySize IntOptions
17+
// Use Secure attribute for session cookies.
18+
// That ensures they are sent over HTTPS and should be enabled for production!
19+
SecureCookies bool
1720
}
1821

19-
// defaultArgs returns the default configuration size
22+
// NewConfig returns the default configuration
2023
func NewConfig() *Config {
2124
return &Config{
2225
AdapterPort: uint16(47304),
@@ -37,5 +40,6 @@ func NewConfig() *Config {
3740
},
3841
Value: 16,
3942
},
43+
SecureCookies: false,
4044
}
4145
}

adapter/pkg/apis/policies/v1/oidc_config.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ type OidcConfig struct {
1717

1818
// OidcConfigSpec is the spec for a OidcConfig resource
1919
type OidcConfigSpec struct {
20-
ClientName string
21-
AuthMethod string `json:"authMethod"`
22-
ClientID string `json:"clientId"`
23-
DiscoveryURL string `json:"discoveryUrl"`
24-
ClientSecret string `json:"clientSecret"`
20+
ClientName string
21+
AuthMethod string `json:"authMethod"`
22+
ClientID string `json:"clientId"`
23+
ClientCallback string `json:"callback"`
24+
DiscoveryURL string `json:"discoveryUrl"`
25+
ClientSecret string `json:"clientSecret"`
2526
ClientSecretRef ClientSecretRef `json:"clientSecretRef"`
2627
}
2728

2829
type ClientSecretRef struct {
29-
Name string `json:"name"`
30-
Key string `json:"key"`
30+
Name string `json:"name"`
31+
Key string `json:"key"`
3132
}
3233

3334
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

adapter/policy/engine/engine.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import (
55
"errors"
66
"strings"
77

8-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/pkg/apis/policies/v1"
8+
v1 "github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/pkg/apis/policies/v1"
99

1010
"go.uber.org/zap"
1111

1212
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/policy"
1313
policy2 "github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/policy/store/policy"
14-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
14+
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
1515
)
1616

1717
const (
@@ -53,6 +53,9 @@ func (m *engine) Evaluate(target *authnz.TargetMsg) (*Action, error) {
5353

5454
// Strip custom path components
5555
if strings.HasSuffix(target.Path, callbackEndpoint) {
56+
// Attempt to strip default /oidc/callback for backward compatibility.
57+
// Now also a custom callbak can be configured in OidcConfig. Custom callbacks
58+
// has to be explicitly supported in the routing so stripping them is not required.
5659
target.Path = strings.Split(target.Path, callbackEndpoint)[0]
5760
} else if strings.HasSuffix(target.Path, logoutEndpoint) {
5861
target.Path = strings.Split(target.Path, logoutEndpoint)[0]
@@ -150,8 +153,8 @@ func createDefaultRules(action Action) []v1.Rule {
150153
case policy.OIDC:
151154
return []v1.Rule{
152155
{
153-
Claim: aud,
154-
Match: "ANY",
156+
Claim: aud,
157+
Match: "ANY",
155158
Values: []string{action.Client.ID()},
156159
},
157160
}

adapter/strategy/strategy.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
policy "istio.io/api/policy/v1beta1"
77

88
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/policy/engine"
9-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
9+
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
1010
)
1111

1212
// Strategy defines the entry point to an authentication handler

adapter/strategy/web/utils.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"go.uber.org/zap"
1212

1313
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/client"
14-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
14+
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
1515
)
1616

1717
const (
@@ -82,7 +82,7 @@ func buildTokenCookieName(base string, c client.Client) string {
8282

8383
// generateSessionIDCookie creates a new sessionId cookie
8484
// if the provided value is empty and new id is randomly generated
85-
func generateSessionIDCookie(c client.Client, value *string) *http.Cookie {
85+
func generateSessionIDCookie(c client.Client, value *string, secure bool) *http.Cookie {
8686
var v = randString(15)
8787
if value != nil {
8888
v = *value
@@ -91,8 +91,12 @@ func generateSessionIDCookie(c client.Client, value *string) *http.Cookie {
9191
Name: buildTokenCookieName(sessionCookie, c),
9292
Value: v,
9393
Path: "/",
94-
Secure: false, // TODO: replace on release
95-
HttpOnly: false,
96-
Expires: time.Now().Add(time.Hour * time.Duration(2160)), // 90 days
94+
Secure: secure,
95+
SameSite: http.SameSiteLaxMode,
96+
HttpOnly: true, // Cookie available to HTTP protocol only (no JS access)
97+
//TODO: possible to use Expires instead of Max-Age once Istio supports it,
98+
// see https://github.com/istio/istio/pull/21270
99+
//Expires: time.Now().Add(time.Hour * time.Duration(2160)), // 90 days
100+
MaxAge: 90 * 24 * 60 * 60, // 90 days
97101
}
98102
}

adapter/strategy/web/utils_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/stretchr/testify/assert"
77

88
"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/client"
9-
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
9+
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
1010
"github.com/ibm-cloud-security/app-identity-and-access-adapter/tests/fake"
1111
)
1212

0 commit comments

Comments
 (0)