Skip to content

Commit

Permalink
oauth2: Adds JWT Access Token strategy
Browse files Browse the repository at this point in the history
This patch adds the (experimental) ability to issue JSON Web Tokens instead of ORY Hydra's opaque access tokens. Please be aware that this feature has had little real-world and unit testing and may not be suitable for production.

Simple integration tests using the JWT strategy have been added to ensure functionality.

To use the new JWT strategy, set environment variable `OAUTH2_ACCESS_TOKEN_STRATEGY` to `jwt`. For example: `export OAUTH2_ACCESS_TOKEN_STRATEGY=jwt`.

Please be aware that we (ORY) do not recommend using the JWT strategy for various reasons. If you can, use the default and recommended "opaque" strategy instead.

Closes #248

Signed-off-by: arekkas <aeneas@ory.am>
  • Loading branch information
arekkas committed Jul 22, 2018
1 parent 16115f2 commit c6b13a0
Show file tree
Hide file tree
Showing 21 changed files with 317 additions and 90 deletions.
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ jobs:
- run: go install github.com/ory/hydra/test/mock-lcp
- run: go-acc -o coverage.txt ./...
- run: go test -race -short $(go list ./... | grep -v cmd)
- run: ./scripts/test-e2e.sh
- run: ./scripts/test-e2e-jwt.sh
- run: ./scripts/test-e2e-opaque.sh
- run: test -z "$CIRCLE_PR_NUMBER" && goveralls -service=circle-ci -coverprofile=coverage.txt -repotoken=$COVERALLS_REPO_TOKEN || echo "forks are not allowed to push to coveralls"

swagger:
Expand Down
44 changes: 20 additions & 24 deletions cmd/cli/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,31 @@
package cli

import (
"flag"
"log"
"testing"

"github.com/jmoiron/sqlx"
"github.com/ory/hydra/config"
"github.com/ory/sqlcon/dockertest"
)

var db *sqlx.DB

func TestMain(m *testing.M) {
runner := dockertest.Register()

flag.Parse()
if !testing.Short() {
dockertest.Parallel([]func(){
func() {
var err error
db, err = dockertest.ConnectToTestPostgreSQL()
if err != nil {
log.Fatalf("Unable to connect to database: %s", err)
}
},
})
}

runner.Exit(m.Run())
}
//var db *sqlx.DB
//
//func TestMain(m *testing.M) {
// runner := dockertest.Register()
//
// flag.Parse()
// if !testing.Short() {
// dockertest.Parallel([]func(){
// func() {
// var err error
// db, err = dockertest.ConnectToTestPostgreSQL()
// if err != nil {
// log.Fatalf("Unable to connect to database: %s", err)
// }
// },
// })
// }
//
// runner.Exit(m.Run())
//}

func TestNewHandler(t *testing.T) {
_ = NewHandler(&config.Config{})
Expand Down
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ func initConfig() {
viper.BindEnv("CLUSTER_URL")
viper.SetDefault("CLUSTER_URL", "")

viper.BindEnv("OAUTH2_ACCESS_TOKEN_STRATEGY")
viper.SetDefault("OAUTH2_ACCESS_TOKEN_STRATEGY", "opaque")

viper.BindEnv("PORT")
viper.SetDefault("PORT", 4444)

Expand Down
6 changes: 6 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ OAUTH2 CONTROLS
codes and similar errors.
Defaults to OAUTH2_SHARE_ERROR_DEBUG=false
- OAUTH2_ACCESS_TOKEN_STRATEGY: Sets the Access Token Strategy. Defaults to "opaque" which is the recommended strategy
for usage with ORY Hydra. If set to "jwt", then Access Tokens will be a signed JSON Web Token. The public key
for verifying the token can be obtained from "./well-known/jwks.json". Please note that the "jwt" strategy is currently
in BETA and not recommended for production just yet.
Defaults to OAUTH2_ACCESS_TOKEN_STRATEGY="opaque"
OPENID CONNECT CONTROLS
===============
Expand Down
16 changes: 12 additions & 4 deletions cmd/server/handler_jwk_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/ory/herodot"
"github.com/ory/hydra/config"
"github.com/ory/hydra/jwk"
"github.com/ory/hydra/oauth2"
"github.com/ory/sqlcon"
)

Expand Down Expand Up @@ -60,12 +61,19 @@ func newJWKHandler(c *config.Config, router *httprouter.Router) *jwk.Handler {
ctx := c.Context()
w := herodot.NewJSONWriter(c.GetLogger())
w.ErrorEnhancer = writerErrorEnhancer
var wellKnown []string

expectDependency(c.GetLogger(), ctx.KeyManager)
h := &jwk.Handler{
H: w,
Manager: ctx.KeyManager,
if c.OAuth2AccessTokenStrategy == "jwt" {
wellKnown = append(wellKnown, oauth2.OAuth2JWTKeyName)
}

expectDependency(c.GetLogger(), ctx.KeyManager)
h := jwk.NewHandler(
ctx.KeyManager,
nil,
w,
wellKnown,
)
h.SetRoutes(router)
return h
}
63 changes: 50 additions & 13 deletions cmd/server/handler_oauth2_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
foauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/openid"
"github.com/ory/herodot"
"github.com/ory/hydra/client"
Expand All @@ -52,7 +53,7 @@ func injectFositeStore(c *config.Config, clients client.Manager) {
break
case *sqlcon.SQLConnection:
expectDependency(c.GetLogger(), con.GetDatabase())
store = oauth2.NewFositeSQLStore(clients, con.GetDatabase(), c.GetLogger(), c.GetAccessTokenLifespan())
store = oauth2.NewFositeSQLStore(clients, con.GetDatabase(), c.GetLogger(), c.GetAccessTokenLifespan(), c.OAuth2AccessTokenStrategy == "jwt")
break
case *config.PluginConnection:
var err error
Expand Down Expand Up @@ -99,11 +100,38 @@ func newOAuth2Provider(c *config.Config) fosite.OAuth2Provider {
}
oidcStrategy := &openid.DefaultStrategy{JWTStrategy: jwtStrategy}

var coreStrategy foauth2.CoreStrategy
hmacStrategy := compose.NewOAuth2HMACStrategy(fc, c.GetSystemSecret())
if c.OAuth2AccessTokenStrategy == "jwt" {
kid := uuid.New()
if _, err := createOrGetJWK(c, oauth2.OAuth2JWTKeyName, kid, "private"); err != nil {
c.GetLogger().WithError(err).Fatalf(`Could not fetch private signing key for OAuth 2.0 Access Tokens - did you forget to run "hydra migrate sql" or forget to set the SYSTEM_SECRET?`)
}

if _, err := createOrGetJWK(c, oauth2.OAuth2JWTKeyName, kid, "public"); err != nil {
c.GetLogger().WithError(err).Fatalf(`Could not fetch public signing key for OAuth 2.0 Access Tokens - did you forget to run "hydra migrate sql" or forget to set the SYSTEM_SECRET?`)
}

jwtStrategy, err := jwk.NewRS256JWTStrategy(c.Context().KeyManager, oauth2.OAuth2JWTKeyName)
if err != nil {
c.GetLogger().WithError(err).Fatalf("Unable to refresh Access Token signing keys.")
}

coreStrategy = &foauth2.DefaultJWTStrategy{
JWTStrategy: jwtStrategy,
HMACSHAStrategy: hmacStrategy,
}
} else if c.OAuth2AccessTokenStrategy == "opaque" {
coreStrategy = hmacStrategy
} else {
c.GetLogger().Fatalf(`Environment variable OAUTH2_ACCESS_TOKEN_STRATEGY is set to "%s" but only "opaque" and "jwt" are valid values.`, c.OAuth2AccessTokenStrategy)
}

return compose.Compose(
fc,
store,
&compose.CommonStrategy{
CoreStrategy: compose.NewOAuth2HMACStrategy(fc, c.GetSystemSecret()),
CoreStrategy: coreStrategy,
OpenIDConnectTokenStrategy: oidcStrategy,
JWTStrategy: jwtStrategy,
},
Expand Down Expand Up @@ -148,12 +176,20 @@ func newOAuth2Handler(c *config.Config, router *httprouter.Router, cm consent.Ma
errorURL, err := url.Parse(c.ErrorURL)
pkg.Must(err, "Could not parse error url %s.", errorURL)

jwtStrategy, err := jwk.NewRS256JWTStrategy(c.Context().KeyManager, oauth2.OpenIDConnectKeyName)
openIDJWTStrategy, err := jwk.NewRS256JWTStrategy(c.Context().KeyManager, oauth2.OpenIDConnectKeyName)
pkg.Must(err, "Could not fetch private signing key for OpenID Connect - did you forget to run \"hydra migrate sql\" or forget to set the SYSTEM_SECRET?")
oidcStrategy := &openid.DefaultStrategy{JWTStrategy: jwtStrategy}
oidcStrategy := &openid.DefaultStrategy{JWTStrategy: openIDJWTStrategy}

w := herodot.NewJSONWriter(c.GetLogger())
w.ErrorEnhancer = writerErrorEnhancer
var accessTokenJWTStrategy *jwk.RS256JWTStrategy

if c.OAuth2AccessTokenStrategy == "jwt" {
accessTokenJWTStrategy, err = jwk.NewRS256JWTStrategy(c.Context().KeyManager, oauth2.OAuth2JWTKeyName)
if err != nil {
c.GetLogger().WithError(err).Fatalf("Unable to refresh Access Token signing keys.")
}
}

handler := &oauth2.Handler{
ScopesSupported: c.OpenIDDiscoveryScopesSupported,
Expand All @@ -170,15 +206,16 @@ func newOAuth2Handler(c *config.Config, router *httprouter.Router, cm consent.Ma
oidcStrategy,
openid.NewOpenIDConnectRequestValidator(nil, oidcStrategy),
),
Storage: c.Context().FositeStore,
ErrorURL: *errorURL,
H: w,
AccessTokenLifespan: c.GetAccessTokenLifespan(),
CookieStore: sessions.NewCookieStore(c.GetCookieSecret()),
IssuerURL: c.Issuer,
L: c.GetLogger(),
JWTStrategy: jwtStrategy,
IDTokenLifespan: c.GetIDTokenLifespan(),
Storage: c.Context().FositeStore,
ErrorURL: *errorURL,
H: w,
AccessTokenLifespan: c.GetAccessTokenLifespan(),
CookieStore: sessions.NewCookieStore(c.GetCookieSecret()),
IssuerURL: c.Issuer,
L: c.GetLogger(),
OpenIDJWTStrategy: openIDJWTStrategy,
AccessTokenJWTStrategy: accessTokenJWTStrategy,
IDTokenLifespan: c.GetIDTokenLifespan(),
}

handler.SetRoutes(router)
Expand Down
3 changes: 2 additions & 1 deletion cmd/server/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func TestStart(t *testing.T) {
router := httprouter.New()
h := &Handler{
Config: &config.Config{
DatabaseURL: "memory",
DatabaseURL: "memory",
OAuth2AccessTokenStrategy: "opaque",
},
}
h.registerRoutes(router)
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type Config struct {
OpenIDDiscoveryScopesSupported string `mapstructure:"OIDC_DISCOVERY_SCOPES_SUPPORTED" yaml:"-"`
OpenIDDiscoveryUserinfoEndpoint string `mapstructure:"OIDC_DISCOVERY_USERINFO_ENDPOINT" yaml:"-"`
SendOAuth2DebugMessagesToClients bool `mapstructure:"OAUTH2_SHARE_ERROR_DEBUG" yaml:"-"`
OAuth2AccessTokenStrategy string `mapstructure:"OAUTH2_ACCESS_TOKEN_STRATEGY" yaml:"-"`
ForceHTTP bool `yaml:"-"`

BuildVersion string `yaml:"-"`
Expand Down
47 changes: 34 additions & 13 deletions jwk/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,24 @@ const (
)

type Handler struct {
Manager Manager
Generators map[string]KeyGenerator
H herodot.Writer
Manager Manager
Generators map[string]KeyGenerator
H herodot.Writer
WellKnownKeys []string
}

func NewHandler(
manager Manager,
generators map[string]KeyGenerator,
h herodot.Writer,
wellKnownKeys []string,
) *Handler {
return &Handler{
Manager: manager,
Generators: generators,
H: h,
WellKnownKeys: append(wellKnownKeys, IDTokenKeyName),
}
}

func (h *Handler) GetGenerators() map[string]KeyGenerator {
Expand Down Expand Up @@ -92,19 +107,25 @@ func (h *Handler) SetRoutes(r *httprouter.Router) {
// 403: genericError
// 500: genericError
func (h *Handler) WellKnown(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
keys, err := h.Manager.GetKeySet(IDTokenKeyName)
if err != nil {
h.H.WriteError(w, r, err)
return
}
var jwks jose.JSONWebKeySet

keys, err = FindKeysByPrefix(keys, "public")
if err != nil {
h.H.WriteError(w, r, err)
return
for _, set := range h.WellKnownKeys {
keys, err := h.Manager.GetKeySet(set)
if err != nil {
h.H.WriteError(w, r, err)
return
}

keys, err = FindKeysByPrefix(keys, "public")
if err != nil {
h.H.WriteError(w, r, err)
return
}

jwks.Keys = append(jwks.Keys, keys.Keys...)
}

h.H.Write(w, r, keys)
h.H.Write(w, r, &jwks)
}

// swagger:route GET /keys/{set}/{kid} jsonWebKey getJsonWebKey
Expand Down
10 changes: 6 additions & 4 deletions jwk/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ func init() {
router := httprouter.New()
IDKS, _ = testGenerator.Generate("test-id", "sig")

h := Handler{
Manager: &MemoryManager{},
H: herodot.NewJSONWriter(nil),
}
h := NewHandler(
&MemoryManager{},
nil,
herodot.NewJSONWriter(nil),
[]string{},
)
h.Manager.AddKeySet(IDTokenKeyName, IDKS)
h.SetRoutes(router)
testServer = httptest.NewServer(router)
Expand Down
18 changes: 18 additions & 0 deletions oauth2/fosite_store_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package oauth2

import (
"context"
"crypto/sha512"
"database/sql"
"encoding/json"
"fmt"
Expand All @@ -43,18 +44,21 @@ type FositeSQLStore struct {
DB *sqlx.DB
L logrus.FieldLogger
AccessTokenLifespan time.Duration
HashSignature bool
}

func NewFositeSQLStore(m client.Manager,
db *sqlx.DB,
l logrus.FieldLogger,
accessTokenLifespan time.Duration,
hashSignature bool,
) *FositeSQLStore {
return &FositeSQLStore{
Manager: m,
L: l,
DB: db,
AccessTokenLifespan: accessTokenLifespan,
HashSignature: hashSignature,
}
}

Expand Down Expand Up @@ -253,7 +257,17 @@ func (s *sqlData) toRequest(session fosite.Session, cm client.Manager, logger lo
return r, nil
}

// hashSignature prevents errors where the signature is longer than 128 characters (and thus doesn't fit into the pk).
func (s *FositeSQLStore) hashSignature(signature, table string) string {
if table == sqlTableAccess && s.HashSignature {
return fmt.Sprintf("%x", sha512.Sum384([]byte(signature)))
}
return signature
}

func (s *FositeSQLStore) createSession(signature string, requester fosite.Requester, table string) error {
signature = s.hashSignature(signature, table)

data, err := sqlSchemaFromRequest(signature, requester, s.L)
if err != nil {
return err
Expand All @@ -272,6 +286,8 @@ func (s *FositeSQLStore) createSession(signature string, requester fosite.Reques
}

func (s *FositeSQLStore) findSessionBySignature(signature string, session fosite.Session, table string) (fosite.Requester, error) {
signature = s.hashSignature(signature, table)

var d sqlData
if err := s.DB.Get(&d, s.DB.Rebind(fmt.Sprintf("SELECT * FROM hydra_oauth2_%s WHERE signature=?", table)), signature); err == sql.ErrNoRows {
return nil, errors.Wrap(fosite.ErrNotFound, "")
Expand All @@ -291,6 +307,8 @@ func (s *FositeSQLStore) findSessionBySignature(signature string, session fosite
}

func (s *FositeSQLStore) deleteSession(signature string, table string) error {
signature = s.hashSignature(signature, table)

if _, err := s.DB.Exec(s.DB.Rebind(fmt.Sprintf("DELETE FROM hydra_oauth2_%s WHERE signature=?", table)), signature); err != nil {
return sqlcon.HandleError(err)
}
Expand Down
Loading

0 comments on commit c6b13a0

Please sign in to comment.