Skip to content

Commit

Permalink
oauth2: Enable client specific CORS settings (#1009)
Browse files Browse the repository at this point in the history
Field `allowed_cors_origins` was added to OAuth 2.0 Clients. It enables
CORS for the whitelisted URLS for paths which clients interact with,
such as /oauth2/token.

Closes #975

Signed-off-by: arekkas <aeneas@ory.am>
  • Loading branch information
aeneasr authored Aug 26, 2018
1 parent 4f0e061 commit a36d0af
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 29 deletions.
8 changes: 7 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ type Client struct {
// retains, and discloses personal data.
PolicyURI string `json:"policy_uri"`

// AllowedCORSOrigins are one or more URLs (scheme://host[:port]) which are allowed to make CORS requests
// to the /oauth/token endpoint. If this array is empty, the sever's CORS origin configuration (`CORS_ALLOWED_ORIGINS`)
// will be used instead. If this array is set, the allowed origins are appended to the server's CORS origin configuration.
// Be aware that environment variable `CORS_ENABLED` MUST be set to `true` for this to work.
AllowedCORSOrigins []string `json:"allowed_cors_origins"`

// TermsOfServiceURI is a URL string that points to a human-readable terms of service
// document for the client that describes a contractual relationship
// between the end-user and the client that the end-user accepts when
Expand Down
26 changes: 26 additions & 0 deletions client/manager_0_sql_migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ var createClientMigrations = []*migrate.Migration{
`DELETE FROM hydra_client WHERE id='6-data'`,
},
},
{
Id: "7-data",
Up: []string{
`INSERT INTO hydra_client (id, allowed_cors_origins, client_name, client_secret, redirect_uris, grant_types, response_types, scope, owner, policy_uri, tos_uri, client_uri, logo_uri, contacts, client_secret_expires_at, sector_identifier_uri, jwks, jwks_uri, token_endpoint_auth_method, request_uris, request_object_signing_alg, userinfo_signed_response_alg, subject_type) VALUES ('7-data', 'http://localhost|http://google', 'some-client', 'abcdef', 'http://localhost|http://google', 'authorize_code|implicit', 'token|id_token', 'foo|bar', 'aeneas', 'http://policy', 'http://tos', 'http://client', 'http://logo', 'aeneas|foo', 0, 'http://sector', '{"keys": []}', 'http://jwks', 'none', 'http://uri1|http://uri2', 'rs256', 'rs526', 'public')`,
},
Down: []string{
`DELETE FROM hydra_client WHERE id='7-data'`,
},
},
{
Id: "8-data",
Up: []string{
`INSERT INTO hydra_client (id, allowed_cors_origins, client_name, client_secret, redirect_uris, grant_types, response_types, scope, owner, policy_uri, tos_uri, client_uri, logo_uri, contacts, client_secret_expires_at, sector_identifier_uri, jwks, jwks_uri, token_endpoint_auth_method, request_uris, request_object_signing_alg, userinfo_signed_response_alg, subject_type) VALUES ('8-data', 'http://localhost|http://google', 'some-client', 'abcdef', 'http://localhost|http://google', 'authorize_code|implicit', 'token|id_token', 'foo|bar', 'aeneas', 'http://policy', 'http://tos', 'http://client', 'http://logo', 'aeneas|foo', 0, 'http://sector', '{"keys": []}', 'http://jwks', 'none', 'http://uri1|http://uri2', 'rs256', 'rs526', 'public')`,
},
Down: []string{
`DELETE FROM hydra_client WHERE id='8-data'`,
},
},
}

var migrations = map[string]*migrate.MemoryMigrationSource{
Expand All @@ -108,6 +126,10 @@ var migrations = map[string]*migrate.MemoryMigrationSource{
createClientMigrations[4],
client.Migrations["mysql"].Migrations[5],
createClientMigrations[5],
client.Migrations["mysql"].Migrations[6],
createClientMigrations[6],
client.Migrations["mysql"].Migrations[7],
createClientMigrations[7],
},
},
"postgres": {
Expand All @@ -125,6 +147,10 @@ var migrations = map[string]*migrate.MemoryMigrationSource{
createClientMigrations[4],
client.Migrations["postgres"].Migrations[5],
createClientMigrations[5],
client.Migrations["postgres"].Migrations[6],
createClientMigrations[6],
client.Migrations["postgres"].Migrations[7],
createClientMigrations[7],
},
},
}
Expand Down
35 changes: 35 additions & 0 deletions client/manager_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ var sharedMigrations = []*migrate.Migration{
`ALTER TABLE hydra_client DROP COLUMN subject_type`,
},
},
{
Id: "7",
Up: []string{
`ALTER TABLE hydra_client ADD allowed_cors_origins TEXT`,
},
Down: []string{
`ALTER TABLE hydra_client DROP COLUMN allowed_cors_origins`,
},
},
}

var Migrations = map[string]*migrate.MemoryMigrationSource{
Expand All @@ -133,6 +142,17 @@ var Migrations = map[string]*migrate.MemoryMigrationSource{
},
sharedMigrations[3],
sharedMigrations[4],
sharedMigrations[5],
{
Id: "8",
Up: []string{
`UPDATE hydra_client SET allowed_cors_origins=''`,
`ALTER TABLE hydra_client MODIFY allowed_cors_origins TEXT NOT NULL`,
},
Down: []string{
`ALTER TABLE hydra_client MODIFY allowed_cors_origins TEXT`,
},
},
}},
"postgres": {Migrations: []*migrate.Migration{
sharedMigrations[0],
Expand All @@ -156,6 +176,17 @@ var Migrations = map[string]*migrate.MemoryMigrationSource{
},
sharedMigrations[3],
sharedMigrations[4],
sharedMigrations[5],
{
Id: "8",
Up: []string{
`UPDATE hydra_client SET allowed_cors_origins=''`,
`ALTER TABLE hydra_client ALTER COLUMN allowed_cors_origins SET NOT NULL`,
},
Down: []string{
`ALTER TABLE hydra_client ALTER COLUMN allowed_cors_origins DROP NOT NULL`,
},
},
}},
}

Expand Down Expand Up @@ -187,6 +218,7 @@ type sqlData struct {
SubjectType string `db:"subject_type"`
RequestObjectSigningAlgorithm string `db:"request_object_signing_alg"`
UserinfoSignedResponseAlg string `db:"userinfo_signed_response_alg"`
AllowedCORSOrigins string `db:"allowed_cors_origins"`
}

var sqlParams = []string{
Expand All @@ -212,6 +244,7 @@ var sqlParams = []string{
"request_uris",
"request_object_signing_alg",
"userinfo_signed_response_alg",
"allowed_cors_origins",
}

func sqlDataFromClient(d *Client) (*sqlData, error) {
Expand Down Expand Up @@ -248,6 +281,7 @@ func sqlDataFromClient(d *Client) (*sqlData, error) {
RequestURIs: strings.Join(d.RequestURIs, "|"),
UserinfoSignedResponseAlg: d.UserinfoSignedResponseAlg,
SubjectType: d.SubjectType,
AllowedCORSOrigins: strings.Join(d.AllowedCORSOrigins, "|"),
}, nil
}

Expand All @@ -274,6 +308,7 @@ func (d *sqlData) ToClient() (*Client, error) {
RequestURIs: stringsx.Splitx(d.RequestURIs, "|"),
UserinfoSignedResponseAlg: d.UserinfoSignedResponseAlg,
SubjectType: d.SubjectType,
AllowedCORSOrigins: stringsx.Splitx(d.AllowedCORSOrigins, "|"),
}

if d.JSONWebKeys != "" {
Expand Down
1 change: 1 addition & 0 deletions client/manager_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func TestHelperCreateGetDeleteClient(k string, m Storage) func(t *testing.T) {
JSONWebKeysURI: "https://...",
TokenEndpointAuthMethod: "none",
RequestURIs: []string{"foo", "bar"},
AllowedCORSOrigins: []string{"foo", "bar"},
RequestObjectSigningAlgorithm: "rs256",
UserinfoSignedResponseAlg: "RS256",
}
Expand Down
22 changes: 22 additions & 0 deletions client/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ func (v *Validator) Validate(c *Client) error {
c.Scope = strings.Join(v.DefaultClientScopes, " ")
}

for k, origin := range c.AllowedCORSOrigins {
u, err := url.Parse(origin)
if err != nil {
return errors.WithStack(fosite.ErrInvalidRequest.WithHint(fmt.Sprintf("Origin URL %s from allowed_cors_origins could not be parsed: %s", origin, err)))
}

if u.Scheme != "https" && u.Scheme != "http" {
return errors.WithStack(fosite.ErrInvalidRequest.WithHint(fmt.Sprintf("Origin URL %s must use https:// or http:// as HTTP scheme.", origin)))
}

if u.User != nil && len(u.User.String()) > 0 {
return errors.WithStack(fosite.ErrInvalidRequest.WithHint(fmt.Sprintf("Origin URL %s has HTTP user and/or password set which is not allowed.", origin)))
}

u.Path = strings.TrimRight(u.Path, "/")
if len(u.Path)+len(u.RawQuery)+len(u.Fragment) > 0 {
return errors.WithStack(fosite.ErrInvalidRequest.WithHint(fmt.Sprintf("Origin URL %s must have an empty path, query, and fragment but one of the parts is not empty.", origin)))
}

c.AllowedCORSOrigins[k] = u.String()
}

// has to be 0 because it is not supposed to be set
c.SecretExpiresAt = 0

Expand Down
17 changes: 9 additions & 8 deletions cmd/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ import (

var _ = &consent.Handler{}

func enhanceRouter(c *config.Config, cmd *cobra.Command, serverHandler *Handler, router *httprouter.Router, middlewares []negroni.Handler) http.Handler {
func enhanceRouter(c *config.Config, cmd *cobra.Command, serverHandler *Handler, router *httprouter.Router, middlewares []negroni.Handler, enableCors bool) http.Handler {
n := negroni.New()
for _, m := range middlewares {
n.Use(m)
}
n.UseFunc(serverHandler.rejectInsecureRequests)
n.UseHandler(router)
if viper.GetString("CORS_ENABLED") == "true" {
if enableCors {
c.GetLogger().Info("Enabled CORS")
return context.ClearHandler(cors.New(corsx.ParseOptions()).Handler(n))
options := corsx.ParseOptions()
return context.ClearHandler(cors.New(options).Handler(n))
} else {
return context.ClearHandler(n)
}
Expand All @@ -77,7 +78,7 @@ func RunServeAdmin(c *config.Config) func(cmd *cobra.Command, args []string) {

cert := getOrCreateTLSCertificate(cmd, c)
// go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, frontend), c.GetFrontendAddress(), &wg)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, backend, mws), c.GetBackendAddress(), &wg, cert)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, backend, mws, viper.GetString("CORS_ENABLED") == "true"), c.GetBackendAddress(), &wg, cert)

wg.Wait()
}
Expand All @@ -93,7 +94,7 @@ func RunServePublic(c *config.Config) func(cmd *cobra.Command, args []string) {
wg.Add(2)

cert := getOrCreateTLSCertificate(cmd, c)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, frontend, mws), c.GetFrontendAddress(), &wg, cert)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, frontend, mws, false), c.GetFrontendAddress(), &wg, cert)
// go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, backend), c.GetBackendAddress(), &wg)

wg.Wait()
Expand All @@ -109,8 +110,8 @@ func RunServeAll(c *config.Config) func(cmd *cobra.Command, args []string) {
wg.Add(2)

cert := getOrCreateTLSCertificate(cmd, c)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, frontend, mws), c.GetFrontendAddress(), &wg, cert)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, backend, mws), c.GetBackendAddress(), &wg, cert)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, frontend, mws, false), c.GetFrontendAddress(), &wg, cert)
go serve(c, cmd, enhanceRouter(c, cmd, serverHandler, backend, mws, viper.GetString("CORS_ENABLED") == "true"), c.GetBackendAddress(), &wg, cert)

wg.Wait()
}
Expand Down Expand Up @@ -257,7 +258,7 @@ func (h *Handler) registerRoutes(frontend, backend *httprouter.Router) {
h.Clients = newClientHandler(c, backend, clientsManager)
h.Keys = newJWKHandler(c, frontend, backend)
h.Consent = newConsentHandler(c, frontend, backend)
h.OAuth2 = newOAuth2Handler(c, frontend, backend, ctx.ConsentManager, oauth2Provider)
h.OAuth2 = newOAuth2Handler(c, frontend, backend, ctx.ConsentManager, oauth2Provider, clientsManager)
_ = newHealthHandler(c, backend)
}

Expand Down
8 changes: 5 additions & 3 deletions cmd/server/handler_oauth2_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/ory/hydra/oauth2"
"github.com/ory/hydra/pkg"
"github.com/pborman/uuid"
"github.com/spf13/viper"
)

func injectFositeStore(c *config.Config, clients client.Manager) {
Expand Down Expand Up @@ -151,8 +152,8 @@ func setDefaultConsentURL(s string, c *config.Config, path string) string {
}

//func newOAuth2Handler(c *config.Config, router *httprouter.Router, cm oauth2.ConsentRequestManager, o fosite.OAuth2Provider, idTokenKeyID string) *oauth2.Handler {
func newOAuth2Handler(c *config.Config, frontend, backend *httprouter.Router, cm consent.Manager, o fosite.OAuth2Provider) *oauth2.Handler {
expectDependency(c.GetLogger(), c.Context().FositeStore)
func newOAuth2Handler(c *config.Config, frontend, backend *httprouter.Router, cm consent.Manager, o fosite.OAuth2Provider, clm client.Manager) *oauth2.Handler {
expectDependency(c.GetLogger(), c.Context().FositeStore, clm)

c.ConsentURL = setDefaultConsentURL(c.ConsentURL, c, "oauth2/fallbacks/consent")
c.LoginURL = setDefaultConsentURL(c.LoginURL, c, "oauth2/fallbacks/consent")
Expand Down Expand Up @@ -214,6 +215,7 @@ func newOAuth2Handler(c *config.Config, frontend, backend *httprouter.Router, cm
ShareOAuth2Debug: c.SendOAuth2DebugMessagesToClients,
}

handler.SetRoutes(frontend, backend)
corsMiddleware := newCORSMiddleware(viper.GetString("CORS_ENABLED") == "true", c, o.IntrospectToken, clm.GetConcreteClient)
handler.SetRoutes(frontend, backend, corsMiddleware)
return handler
}
92 changes: 92 additions & 0 deletions cmd/server/helper_cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright © 2015-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Aeneas Rekkas <aeneas+oss@aeneas.io>
* @Copyright 2017-2018 Aeneas Rekkas <aeneas+oss@aeneas.io>
* @license Apache-2.0
*/

package server

import (
"context"
"net/http"

"github.com/aeneasr/cors"
"github.com/ory/fosite"
"github.com/ory/go-convenience/corsx"
"github.com/ory/go-convenience/stringslice"
"github.com/ory/hydra/client"
"github.com/ory/hydra/config"
"github.com/ory/hydra/oauth2"
)

func newCORSMiddleware(
enable bool, c *config.Config,
o func(ctx context.Context, token string, tokenType fosite.TokenType, session fosite.Session, scope ...string) (fosite.TokenType, fosite.AccessRequester, error),
clm func(id string) (*client.Client, error),
) func(h http.Handler) http.Handler {
if !enable {
return func(h http.Handler) http.Handler {
return h
}
}

c.GetLogger().Info("Enabled CORS")
po := corsx.ParseOptions()
options := cors.Options{
AllowedOrigins: po.AllowedOrigins,
AllowedMethods: po.AllowedMethods,
AllowedHeaders: po.AllowedHeaders,
ExposedHeaders: po.ExposedHeaders,
MaxAge: po.MaxAge,
AllowCredentials: po.AllowCredentials,
OptionsPassthrough: po.OptionsPassthrough,
Debug: po.Debug,
AllowOriginRequestFunc: func(r *http.Request, origin string) bool {
if stringslice.Has(po.AllowedOrigins, origin) {
return true
}

username, _, ok := r.BasicAuth()
if !ok || username == "" {
token := fosite.AccessTokenFromRequest(r)
if token == "" {
return false
}

session := oauth2.NewSession("")
_, ar, err := o(context.Background(), token, fosite.AccessToken, session)
if err != nil {
return false
}

username = ar.GetClient().GetID()
}

cl, err := clm(username)
if err != nil {
return false
}

if stringslice.Has(cl.AllowedCORSOrigins, origin) {
return true
}

return false
},
}
return cors.New(options).Handler
}
Loading

0 comments on commit a36d0af

Please sign in to comment.