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

Use state param for original visited URL and support fixed OIDC callback URL #55

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Istio uses an Envoy proxy sidecar to mediate all inbound and outbound traffic fo

### Protecting frontend apps

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.
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.

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

Expand Down Expand Up @@ -137,7 +137,11 @@ Depending on whether you're protecting frontend or backend applications, create
| `clientSecretRef` | object | no | A reference secret that is used to authenticate the client. This can be used in place of the `clientSecret`. |
| `clientSecretRef.name` | string |yes | The name of the Kubernetes Secret that contains the `clientSecret`. |
| `clientSecretRef.key` | string | yes | The field within the Kubernetes Secret that contains the `clientSecret`. |
| `callback` | string | no | Callback URL or URI to return to from the IODC provider (default is relative `oidc/callback`) |

**Callback**

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.

* 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.

Expand Down
80 changes: 80 additions & 0 deletions adapter/client/callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package client

import (
"path"
"regexp"
"strings"
)

var regexpURL = regexp.MustCompile(`(https?)://([^/]+)(/?.*)`)

func callbackDefinitionForClient(c Client) string {
callbackDef := ""

// also support empty clients (for tests without a valid client, using default callbackEndpoint)
if c != nil {
callbackDef = c.Callback()
}

if callbackDef == "" {
// if the callback is empty for the client, use the original behavior:
// /oidc/callback is simply appended to the original path, resulting in many different callback URLs
callbackDef = "oidc/callback"
}
return callbackDef
}

k3a marked this conversation as resolved.
Show resolved Hide resolved
// CallbackURLForTarget returns the absolute URL to which IdP should redirect after authenticating the user.
// This is the speckial URL which is expected to be handled by the adapter under the provided Client configuration.
func CallbackURLForTarget(c Client, requestScheme, requestHost, requestPath string) string {
callbackDef := callbackDefinitionForClient(c)

if requestPath == "" {
requestPath = "/"
}

if callbackDef[0] == '/' {
// callback path absolute on the request host
return requestScheme + "://" + requestHost + callbackDef
}

m := regexpURL.FindStringSubmatch(callbackDef)
if len(m) > 1 {
// callback is full URL
return callbackDef
}

// callback path relative to the target path
if strings.HasSuffix(requestPath, callbackDef) {
// relative callback path already appended, do not append twice
// this can happen if this function is called in the actual callback handler
// (happens after /authorize OIDC provider URL redirects back and we call OIDC provider API again)
callbackDef = ""
}
return requestScheme + "://" + requestHost + path.Join(requestPath, callbackDef)
}

// IsCallbackRequest returns true if the provided request should be handled by the adapter as part of the auth flow
func IsCallbackRequest(c Client, requestScheme, requestHost, requestPath string) bool {
callbackDef := callbackDefinitionForClient(c)

if requestPath == "" {
requestPath = "/"
}

if callbackDef[0] == '/' {
// callback path absolute on the request host
return strings.HasPrefix(requestPath, callbackDef)
}

m := regexpURL.FindStringSubmatch(callbackDef)
if len(m) == 4 {
// callback is full URL, compare parts (case-insensitive just in case)
return strings.EqualFold(m[1], requestScheme) &&
strings.EqualFold(m[2], requestHost) &&
strings.EqualFold(m[3], requestPath)
}

// callback path relative to the target path, thus ending with callbackDef
return strings.HasSuffix(requestPath, callbackDef)
}
73 changes: 73 additions & 0 deletions adapter/client/callback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package client

import (
"net/url"
"testing"

"github.com/ibm-cloud-security/app-identity-and-access-adapter/tests/fake"
)

func TestCallbackURLForTarget(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for chacking this. Let's see what @ishangulhane or @TalAviel will say. I will soon start working on extending session store. Currently it stores all sessions in sync.Map in the adapter without any expiration (you can see it by searching for w.tokenCache). This needs to be extended in some plug-gable manner, through interface into more implementations. One of them could be Redis, the other one securecookie or something like that. This will have separate PR and/or issue.

tests := []struct {
CallbackDef string
Scheme, Host, Path string
Result string
}{
{ // http scheme test
"",
"http", "test.com", "/admin",
"http://test.com/admin/oidc/callback",
},
{ // default relative callback
"",
"https", "test.com", "/admin",
"https://test.com/admin/oidc/callback",
},
{ // relative URI
"custom/relative/path",
"https", "test.com", "/admin",
"https://test.com/admin/custom/relative/path",
},
{ // relative URI already appended
"custom/relative/path",
"https", "test.com", "/admin/custom/relative/path",
"https://test.com/admin/custom/relative/path",
},
{ // absolute URI
"/absolute/uri/callback",
"https", "test.com", "/admin",
"https://test.com/absolute/uri/callback",
},
{ // full URL (http)
"http://test.com/my/callback",
"http", "test.com", "/admin",
"http://test.com/my/callback",
},
{ // full URL (https)
"https://test.com/my/callback",
"https", "test.com", "/admin",
"https://test.com/my/callback",
},
}

for n, tst := range tests {
cli := fake.NewClientWithCallback(nil, tst.CallbackDef)

res := CallbackURLForTarget(cli, tst.Scheme, tst.Host, tst.Path)
if res != tst.Result {
t.Fatalf("TestCallbackURLForTarget#%d CallbackURLForTarget failed.\n"+
" Expected: %s\n Got: %s", n, tst.Result, res)
}

resURL, err := url.Parse(res)
if err != nil {
t.Fatalf("TestCallbackURLForTarget#%d failed to parse URL returned from CallbackURLForTarget %s: %s",
n, res, err.Error())
}

if !IsCallbackRequest(cli, resURL.Scheme, resURL.Host, resURL.Path) {
t.Fatalf("TestCallbackURLForTarget#%d IsCallbackRequest check not returning true\n"+
" scheme: %s, host: %s, path: %s", n, resURL.Scheme, resURL.Host, resURL.Path)
}
}
}
7 changes: 6 additions & 1 deletion adapter/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import (
"go.uber.org/zap"

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

// Client encapsulates an authn/z client object
type Client interface {
Name() string
ID() string
Callback() string
Secret() string
AuthorizationServer() authserver.AuthorizationServerService
ExchangeGrantCode(code string, redirectURI string) (*authserver.TokenResponse, error)
Expand All @@ -32,6 +33,10 @@ func (c *remoteClient) ID() string {
return c.ClientID
}

func (c *remoteClient) Callback() string {
return c.ClientCallback
}

func (c *remoteClient) Secret() string {
return c.ClientSecret
}
Expand Down
6 changes: 5 additions & 1 deletion adapter/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ type Config struct {
// The blockKey is used to encrypt the cookie value
// Valid lengths are 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
BlockKeySize IntOptions
// Use Secure attribute for session cookies.
// That ensures they are sent over HTTPS and should be enabled for production!
SecureCookies bool
}

// defaultArgs returns the default configuration size
// NewConfig returns the default configuration
func NewConfig() *Config {
return &Config{
AdapterPort: uint16(47304),
Expand All @@ -37,5 +40,6 @@ func NewConfig() *Config {
},
Value: 16,
},
SecureCookies: false,
}
}
15 changes: 8 additions & 7 deletions adapter/pkg/apis/policies/v1/oidc_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ type OidcConfig struct {

// OidcConfigSpec is the spec for a OidcConfig resource
type OidcConfigSpec struct {
ClientName string
AuthMethod string `json:"authMethod"`
ClientID string `json:"clientId"`
DiscoveryURL string `json:"discoveryUrl"`
ClientSecret string `json:"clientSecret"`
ClientName string
AuthMethod string `json:"authMethod"`
ClientID string `json:"clientId"`
ClientCallback string `json:"callback"`
DiscoveryURL string `json:"discoveryUrl"`
ClientSecret string `json:"clientSecret"`
ClientSecretRef ClientSecretRef `json:"clientSecretRef"`
}

type ClientSecretRef struct {
Name string `json:"name"`
Key string `json:"key"`
Name string `json:"name"`
Key string `json:"key"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
11 changes: 7 additions & 4 deletions adapter/policy/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import (
"errors"
"strings"

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

"go.uber.org/zap"

"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/policy"
policy2 "github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/policy/store/policy"
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
)

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

// Strip custom path components
if strings.HasSuffix(target.Path, callbackEndpoint) {
// Attempt to strip default /oidc/callback for backward compatibility.
// Now also a custom callbak can be configured in OidcConfig. Custom callbacks
// has to be explicitly supported in the routing so stripping them is not required.
target.Path = strings.Split(target.Path, callbackEndpoint)[0]
} else if strings.HasSuffix(target.Path, logoutEndpoint) {
target.Path = strings.Split(target.Path, logoutEndpoint)[0]
Expand Down Expand Up @@ -150,8 +153,8 @@ func createDefaultRules(action Action) []v1.Rule {
case policy.OIDC:
return []v1.Rule{
{
Claim: aud,
Match: "ANY",
Claim: aud,
Match: "ANY",
Values: []string{action.Client.ID()},
},
}
Expand Down
2 changes: 1 addition & 1 deletion adapter/strategy/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
policy "istio.io/api/policy/v1beta1"

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

// Strategy defines the entry point to an authentication handler
Expand Down
14 changes: 9 additions & 5 deletions adapter/strategy/web/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"go.uber.org/zap"

"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/client"
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
)

const (
Expand Down Expand Up @@ -82,7 +82,7 @@ func buildTokenCookieName(base string, c client.Client) string {

// generateSessionIDCookie creates a new sessionId cookie
// if the provided value is empty and new id is randomly generated
func generateSessionIDCookie(c client.Client, value *string) *http.Cookie {
func generateSessionIDCookie(c client.Client, value *string, secure bool) *http.Cookie {
var v = randString(15)
if value != nil {
v = *value
Expand All @@ -91,8 +91,12 @@ func generateSessionIDCookie(c client.Client, value *string) *http.Cookie {
Name: buildTokenCookieName(sessionCookie, c),
Value: v,
Path: "/",
Secure: false, // TODO: replace on release
HttpOnly: false,
Expires: time.Now().Add(time.Hour * time.Duration(2160)), // 90 days
Secure: secure,
SameSite: http.SameSiteLaxMode,
HttpOnly: true, // Cookie available to HTTP protocol only (no JS access)
//TODO: possible to use Expires instead of Max-Age once Istio supports it,
// see https://github.com/istio/istio/pull/21270
//Expires: time.Now().Add(time.Hour * time.Duration(2160)), // 90 days
MaxAge: 90 * 24 * 60 * 60, // 90 days
}
}
2 changes: 1 addition & 1 deletion adapter/strategy/web/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/ibm-cloud-security/app-identity-and-access-adapter/adapter/client"
"github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
authnz "github.com/ibm-cloud-security/app-identity-and-access-adapter/config/template"
"github.com/ibm-cloud-security/app-identity-and-access-adapter/tests/fake"
)

Expand Down
Loading