diff --git a/config.go b/config.go index 5e4ab2e8f6f2ad6086169e98eea4e4b8354184a4..a5f1f784c11dbba0e50f1ea2b0d456976b28d0f5 100644 --- a/config.go +++ b/config.go @@ -40,7 +40,7 @@ func newDefaultConfig() *Config { TagData: make(map[string]string, 0), ClaimsMatch: make(map[string]string, 0), Header: make(map[string]string, 0), - CORSConfig: &CORS{ + CORS: &CORS{ Origins: []string{}, Methods: []string{}, Headers: []string{}, @@ -203,6 +203,24 @@ func readOptions(cx *cli.Context, config *Config) (err error) { if cx.IsSet("hostname") { config.Hostnames = cx.StringSlice("hostname") } + if cx.IsSet("cors-origins") { + config.CORS.Origins = cx.StringSlice("cors-origins") + } + if cx.IsSet("cors-methods") { + config.CORS.Methods = cx.StringSlice("cors-methods") + } + if cx.IsSet("cors-headers") { + config.CORS.Headers = cx.StringSlice("cors-headers") + } + if cx.IsSet("cors-exposed-headers") { + config.CORS.ExposedHeaders = cx.StringSlice("cors-exposed-headers") + } + if cx.IsSet("cors-max-age") { + config.CORS.MaxAge = cx.Duration("cors-max-age") + } + if cx.IsSet("cors-credentials") { + config.CORS.Credentials = cx.BoolT("cors-credentials") + } if cx.IsSet("tag") { config.TagData, err = decodeKeyPairs(cx.StringSlice("tag")) if err != nil { @@ -221,18 +239,7 @@ func readOptions(cx *cli.Context, config *Config) (err error) { return err } } - if cx.IsSet("cors-origins") { - config.CORSConfig.Origins = cx.StringSlice("cors-origins") - } - if cx.IsSet("cors-methods") { - config.CORSConfig.Methods = cx.StringSlice("cors-methods") - } - if cx.IsSet("cors-headers") { - config.CORSConfig.Headers = cx.StringSlice("cors-headers") - } - if cx.IsSet("cors-max-age") { - config.CORSConfig.MaxAge = cx.Duration("cors-max-age") - } + if cx.IsSet("resource") { for _, x := range cx.StringSlice("resource") { resource, err := decodeResource(x) @@ -355,18 +362,26 @@ func getOptions() []cli.Flag { Name: "cors-origins", Usage: "a set of origins to add to the CORS access control (Access-Control-Allow-Origin)", }, + cli.StringSliceFlag{ + Name: "cors-methods", + Usage: "the method permitted in the access control (Access-Control-Allow-Methods)", + }, cli.StringSliceFlag{ Name: "cors-headers", Usage: "a set of headers to add to the CORS access control (Access-Control-Allow-Headers)", }, cli.StringSliceFlag{ - Name: "cors-methods", - Usage: "the method permitted in the access control (Access-Control-Allow-Methods)", + Name: "cors-exposes-headers", + Usage: "set the expose cors headers access control (Access-Control-Expose-Headers)", }, cli.DurationFlag{ Name: "cors-max-age", Usage: "the max age applied to cors headers (Access-Control-Max-Age)", }, + cli.BoolFlag{ + Name: "cors-credentials", + Usage: "the credentials access control header (Access-Control-Allow-Credentials)", + }, cli.BoolFlag{ Name: "skip-token-verification", Usage: "testing purposes ONLY, the option allows you to bypass the token verification, expiration and roles are still enforced", diff --git a/config_sample.yml b/config_sample.yml index 100c69d534c125f4ab7a0d8cb5e1ae8cbb74f901..e367b03f2347db6139d173ac9eb033dafb71ec45 100644 --- a/config_sample.yml +++ b/config_sample.yml @@ -53,3 +53,19 @@ resources: roles: - openvpn:vpn-user - openvpn:prod-vpn + +# set the cross origin resource sharing headers +cors: + # an array of origins (Access-Control-Allow-Origin) + origins: [] + # an array of headers to apply (Access-Control-Allow-Headers) + headers: [] + # an array of expose headers (Access-Control-Expose-Headers) + exposed-headers: [] + # an array of methods (Access-Control-Allow-Methods) + methods: [] + # the credentials flag (Access-Control-Allow-Credentials) + credentials: true|false + # the max age (Access-Control-Max-Age) + max-age: 1h + diff --git a/doc.go b/doc.go index fe5b679b359f3aec45755fdf55ce4ed43b28ebf7..6b82d64ed598e07b2e0afce5806f212ef807c7e9 100644 --- a/doc.go +++ b/doc.go @@ -22,7 +22,7 @@ import ( const ( prog = "keycloak-proxy" - version = "v1.0.0-rc1" + version = "v1.0.0-rc2" author = "Rohith" email = "gambol99@gmail.com" description = "is a proxy using the keycloak service for auth and authorization" @@ -67,7 +67,7 @@ type Resource struct { Roles []string `json:"roles" yaml:"roles"` } -// CORS controls +// CORS access controls type CORS struct { // Origins is a list of origins permitted Origins []string `json:"origins" yaml:"origins"` @@ -75,6 +75,10 @@ type CORS struct { Methods []string `json:"methods" yaml:"methods"` // Headers is a set of cors headers Headers []string `json:"headers" yaml:"headers"` + // ExposedHeaders are the exposed header fields + ExposedHeaders []string `json:"exposed-headers" yaml:"exposed-headers"` + // Credentials set the creds flag + Credentials bool `json:"credentials" yaml:"credentials"` // MaxAge is the age for CORS MaxAge time.Duration `json:"max-age" yaml:"max-age"` } @@ -116,7 +120,7 @@ type Config struct { // TagData is passed to the templates TagData map[string]string `json:"TagData" yaml:"TagData"` // CORS permits adding headers to the /oauth handlers - CORSConfig *CORS `json:"cors" yaml:"cors"` + CORS *CORS `json:"cors" yaml:"cors"` // Header permits adding customs headers across the board Header map[string]string `json:"headers" yaml:"headers"` // Scopes is a list of scope we should request diff --git a/handlers.go b/handlers.go index e83d214fc29da498b1d3bb4c0df4458a4ecc066f..0ac8cc353f628103e2f52a6fe3bba5212cc4bf78 100644 --- a/handlers.go +++ b/handlers.go @@ -16,7 +16,6 @@ limitations under the License. package main import ( - "fmt" "net/http" "path" "regexp" @@ -452,9 +451,6 @@ func (r *KeycloakProxy) oauthAuthorizationHandler(cx *gin.Context) { return } - // step: add the cors headers - r.corsAccessHeaders(cx) - // step: get the redirection url r.redirectToURL(redirectionURL, cx) } @@ -564,9 +560,6 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { } } - // step: add the cors headers - r.corsAccessHeaders(cx) - r.redirectToURL(state, cx) } @@ -576,21 +569,3 @@ func (r *KeycloakProxy) oauthCallbackHandler(cx *gin.Context) { func (r *KeycloakProxy) healthHandler(cx *gin.Context) { cx.String(http.StatusOK, "OK") } - -// corsAccessHeaders adds the cors access controls to the oauth responses -func (r *KeycloakProxy) corsAccessHeaders(cx *gin.Context) { - cors := r.config.CORSConfig - if len(cors.Origins) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Origin", strings.Join(cors.Origins, ",")) - } - if len(cors.Methods) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Methods", strings.Join(cors.Methods, ",")) - } - if len(cors.Headers) > 0 { - cx.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(cors.Headers, ",")) - } - if cors.MaxAge > 0 { - cx.Writer.Header().Set("Access-Control-Max-Age", - fmt.Sprintf("%d", int(cors.MaxAge.Seconds()))) - } -} diff --git a/server.go b/server.go index 118b64a88d663e2ba61a43769feb3524eb9da156..0d9523fca0b3d68b6eb75e234fa6f9cf3551dc5d 100644 --- a/server.go +++ b/server.go @@ -29,6 +29,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" + "strings" ) // KeycloakProxy is the server component @@ -193,7 +194,7 @@ func (r *KeycloakProxy) Run() error { // redirectToURL redirects the user and aborts the context func (r KeycloakProxy) redirectToURL(url string, cx *gin.Context) { // step: add the cors headers - r.corsAccessHeaders(cx) + r.injectCORSHeaders(cx) cx.Redirect(http.StatusTemporaryRedirect, url) cx.Abort() @@ -227,6 +228,29 @@ func (r KeycloakProxy) redirectToAuthorization(cx *gin.Context) { r.redirectToURL(authorizationURL+authQuery, cx) } +// injectCORSHeaders adds the cors access controls to the oauth responses +func (r *KeycloakProxy) injectCORSHeaders(cx *gin.Context) { + c := r.config.CORS + if len(c.Origins) > 0 { + cx.Writer.Header().Set("Access-Control-Allow-Origin", strings.Join(c.Origins, ",")) + } + if len(c.Methods) > 0 { + cx.Writer.Header().Set("Access-Control-Allow-Methods", strings.Join(c.Methods, ",")) + } + if len(c.Headers) > 0 { + cx.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(c.Headers, ",")) + } + if len(c.ExposedHeaders) > 0 { + cx.Writer.Header().Set("Access-Control-Expose-Headers", strings.Join(c.ExposedHeaders, ",")) + } + if c.Credentials { + cx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + } + if c.MaxAge > 0 { + cx.Writer.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", int(c.MaxAge.Seconds()))) + } +} + // tryUpdateConnection attempt to upgrade the connection to a http pdy stream func (r *KeycloakProxy) tryUpdateConnection(cx *gin.Context) error { // step: dial the endpoint diff --git a/server_test.go b/server_test.go index 80a1aabfa5f850e92de6ecc573c54be24b67e108..2a0303f55b430e03cbd778da2a37401c0b8a8ffa 100644 --- a/server_test.go +++ b/server_test.go @@ -94,7 +94,7 @@ func newFakeKeycloakProxy(t *testing.T) *KeycloakProxy { Roles: []string{}, }, }, - CORSConfig: &CORS{}, + CORS: &CORS{}, }, proxy: new(fakeReverseProxy), }