diff --git a/driver/config/config.go b/driver/config/config.go index 4cdbff51aad2..6d5aa68d8900 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -106,6 +106,7 @@ const ( ViperKeySessionName = "session.cookie.name" ViperKeySessionPath = "session.cookie.path" ViperKeySessionPersistentCookie = "session.cookie.persistent" + ViperKeySessionTokenizerTemplates = "session.whoami.tokenizer.templates" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" ViperKeySessionWhoAmICaching = "feature_flags.cacheable_sessions" ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" @@ -1051,7 +1052,7 @@ func (p *Config) CourierTemplatesHelper(ctx context.Context, key string) *Courie config, err := json.Marshal(p.GetProvider(ctx).Get(key)) if err != nil { - p.l.WithError(err).Fatalf("Unable to dencode values from %s.", key) + p.l.WithError(err).Fatalf("Unable to decode values from %s.", key) return courierTemplate } @@ -1467,3 +1468,23 @@ func (p *Config) getTLSCertificates(ctx context.Context, daemon, certBase64, key func (p *Config) GetProvider(ctx context.Context) *configx.Provider { return p.c.Config(ctx, p.p) } + +type SessionTokenizeFormat struct { + TTL time.Duration `koanf:"ttl" json:"ttl"` + ClaimsMapperURL string `koanf:"claims_mapper_url" json:"claims_mapper_url"` + JWKSURL string `koanf:"jwks_url" json:"jwks_url"` +} + +func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTokenizeFormat, err error) { + var result SessionTokenizeFormat + path := ViperKeySessionTokenizerTemplates + "." + key + if !p.GetProvider(ctx).Exists(path) { + return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to find tokenizer template \"%s\".", key)) + } + + if err := p.GetProvider(ctx).Unmarshal(path, &result); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode tokenizer template \"%s\": %s", key, err)) + } + + return &result, nil +} diff --git a/driver/registry.go b/driver/registry.go index 38c87baf5c9b..e0e0d2562393 100644 --- a/driver/registry.go +++ b/driver/registry.go @@ -113,6 +113,7 @@ type Registry interface { session.HandlerProvider session.ManagementProvider session.PersistenceProvider + session.TokenizerProvider settings.HandlerProvider settings.ErrorHandlerProvider diff --git a/driver/registry_default.go b/driver/registry_default.go index 514409f5bd1e..b4733fc6dccf 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -12,6 +12,10 @@ import ( "testing" "time" + "github.com/dgraph-io/ristretto" + + "github.com/ory/x/jwksx" + "github.com/ory/x/contextx" "github.com/ory/x/jsonnetsecure" @@ -114,8 +118,9 @@ type RegistryDefault struct { schemaHandler *schema.Handler - sessionHandler *session.Handler - sessionManager session.Manager + sessionHandler *session.Handler + sessionManager session.Manager + sessionTokenizer *session.Tokenizer passwordHasher hash.Hasher passwordValidator password2.Validator @@ -163,6 +168,7 @@ type RegistryDefault struct { csrfTokenGenerator x.CSRFToken jsonnetVMProvider jsonnetsecure.VMProvider + jwkFetcher *jwksx.FetcherNext } func (m *RegistryDefault) JsonnetVM(ctx context.Context) (jsonnetsecure.VM, error) { @@ -839,3 +845,29 @@ func (m *RegistryDefault) Contextualizer() contextx.Contextualizer { } return m.ctxer } + +func (m *RegistryDefault) Fetcher() *jwksx.FetcherNext { + if m.jwkFetcher == nil { + maxItems := int64(10000000) + cache, _ := ristretto.NewCache(&ristretto.Config{ + NumCounters: maxItems * 10, + MaxCost: maxItems, + BufferItems: 64, + Metrics: true, + IgnoreInternalCost: true, + Cost: func(value interface{}) int64 { + return 1 + }, + }) + + m.jwkFetcher = jwksx.NewFetcherNext(cache) + } + return m.jwkFetcher +} + +func (m *RegistryDefault) SessionTokenizer() *session.Tokenizer { + if m.sessionTokenizer == nil { + m.sessionTokenizer = session.NewTokenizer(m) + } + return m.sessionTokenizer +} diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 662cd3403800..e7f5ba3ddf95 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2597,6 +2597,44 @@ "properties": { "required_aal": { "$ref": "#/definitions/featureRequiredAal" + }, + "tokenizer": { + "title": "Tokenizer configuration", + "description": "Configure the tokenizer, responsible for converting a session into a token format such as JWT.", + "type": "object", + "properties": { + "templates": { + "title": "Tokenizer templates", + "description": "A list of different templates that govern how a session is converted to a token format.", + "type": "object", + "patternProperties": { + "[a-zA-Z0-9-_.]+": { + "type": "object", + "required": [ + "jwks_url" + ], + "properties": { + "ttl": { + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", + "default": "1m", + "title": "Token time to live" + }, + "claims_mapper_url": { + "type": "string", + "format": "uri", + "title": "JsonNet mapper URL" + }, + "jwks_url": { + "type": "string", + "format": "uri", + "title": "JSON Web Key Set URL" + } + } + } + } + } + } } }, "additionalProperties": false diff --git a/go.mod b/go.mod index 3b86d4e92abd..a35d9afc7289 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0 github.com/gofrs/uuid v4.3.1+incompatible github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 github.com/golang/mock v1.6.0 github.com/google/go-github/v27 v27.0.1 @@ -60,6 +61,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/knadh/koanf/parsers/json v0.1.0 github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 + github.com/lestrrat-go/jwx v1.2.26 github.com/luna-duclos/instrumentedsql v1.1.3 github.com/mailhog/MailHog v1.0.1 github.com/mattn/goveralls v0.0.7 @@ -130,6 +132,7 @@ require ( github.com/containerd/continuity v0.3.0 // indirect github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/docker/cli v20.10.21+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v20.10.24+incompatible // indirect @@ -145,6 +148,7 @@ require ( github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-bindata/go-bindata v3.1.2+incompatible // indirect github.com/go-crypt/x v0.2.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect @@ -170,6 +174,7 @@ require ( github.com/gobuffalo/tags/v3 v3.1.4 // indirect github.com/gobuffalo/validate/v3 v3.3.3 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.9.6 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -224,6 +229,11 @@ require ( github.com/kr/pretty v0.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.7 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailhog/MailHog-Server v1.0.1 // indirect diff --git a/go.sum b/go.sum index a54b199a2b27..b8f3117cbc72 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 h1:VzPvKOw28XJ77PYwOq5gAqvFB4gk6gst0HxxiW8kfZQ= github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+6FzxsSbK4oEuvdN06Jco8zKB2mQqIB6UduZdd0Zesk= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= @@ -220,6 +223,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -347,6 +352,8 @@ github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.9.6 h1:KhAu1zf9JXnm3vbG49aDE0E5uEBUsM4uwD31/58ZWyI= github.com/goccy/go-yaml v1.9.6/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -362,6 +369,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -678,6 +687,19 @@ github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 h1:PDeBswTUsSIT4QS github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.26 h1:4iFo8FPRZGDYe1t19mQP0zTRqA7n8HnJ5lkIiDvJcB0= +github.com/lestrrat-go/jwx v1.2.26/go.mod h1:MaiCdGbn3/cckbOFSCluJlJMmp9dmZm5hDuIkx8ftpQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -1113,6 +1135,7 @@ golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaE golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1130,6 +1153,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1170,6 +1194,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1223,6 +1248,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1343,6 +1370,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1353,6 +1382,8 @@ golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1365,6 +1396,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1436,6 +1469,7 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools/cmd/cover v0.1.0-deprecated h1:Rwy+mWYz6loAF+LnG1jHG/JWMHRMMC2/1XX3Ejkx9lA= diff --git a/internal/client-go/api_frontend.go b/internal/client-go/api_frontend.go index e3dc0fcf14f1..657a2c6c7721 100644 --- a/internal/client-go/api_frontend.go +++ b/internal/client-go/api_frontend.go @@ -665,6 +665,16 @@ type FrontendApi interface { console.log(session) ``` + When using a token template, the token is included in the `tokenized` field of the session. + + ```js + pseudo-code example + ... + const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) + + console.log(session.tokenized) // The JWT + ``` + Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user @@ -4477,6 +4487,7 @@ type FrontendApiApiToSessionRequest struct { ApiService FrontendApi xSessionToken *string cookie *string + tokenizeAs *string } func (r FrontendApiApiToSessionRequest) XSessionToken(xSessionToken string) FrontendApiApiToSessionRequest { @@ -4487,6 +4498,10 @@ func (r FrontendApiApiToSessionRequest) Cookie(cookie string) FrontendApiApiToSe r.cookie = &cookie return r } +func (r FrontendApiApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendApiApiToSessionRequest { + r.tokenizeAs = &tokenizeAs + return r +} func (r FrontendApiApiToSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ToSessionExecute(r) @@ -4521,6 +4536,16 @@ const session = await client.toSession("the-session-token") console.log(session) ``` +When using a token template, the token is included in the `tokenized` field of the session. + +```js +pseudo-code example +... +const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) + +console.log(session.tokenized) // The JWT +``` + Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user @@ -4579,6 +4604,9 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.tokenizeAs != nil { + localVarQueryParams.Add("tokenize_as", parameterToString(*r.tokenizeAs, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/client-go/model_session.go b/internal/client-go/model_session.go index 0ded40302a1c..aa10a1dac55c 100644 --- a/internal/client-go/model_session.go +++ b/internal/client-go/model_session.go @@ -34,6 +34,8 @@ type Session struct { Identity *Identity `json:"identity,omitempty"` // The Session Issuance Timestamp When this session was issued at. Usually equal or close to `authenticated_at`. IssuedAt *time.Time `json:"issued_at,omitempty"` + // Tokenized is the tokenized (e.g. JWT) version of the session. It is only set when the `tokenize` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized *string `json:"tokenized,omitempty"` } // NewSession instantiates a new Session object @@ -334,6 +336,38 @@ func (o *Session) SetIssuedAt(v time.Time) { o.IssuedAt = &v } +// GetTokenized returns the Tokenized field value if set, zero value otherwise. +func (o *Session) GetTokenized() string { + if o == nil || o.Tokenized == nil { + var ret string + return ret + } + return *o.Tokenized +} + +// GetTokenizedOk returns a tuple with the Tokenized field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetTokenizedOk() (*string, bool) { + if o == nil || o.Tokenized == nil { + return nil, false + } + return o.Tokenized, true +} + +// HasTokenized returns a boolean if a field has been set. +func (o *Session) HasTokenized() bool { + if o != nil && o.Tokenized != nil { + return true + } + + return false +} + +// SetTokenized gets a reference to the given string and assigns it to the Tokenized field. +func (o *Session) SetTokenized(v string) { + o.Tokenized = &v +} + func (o Session) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Active != nil { @@ -363,6 +397,9 @@ func (o Session) MarshalJSON() ([]byte, error) { if o.IssuedAt != nil { toSerialize["issued_at"] = o.IssuedAt } + if o.Tokenized != nil { + toSerialize["tokenized"] = o.Tokenized + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go index e3dc0fcf14f1..657a2c6c7721 100644 --- a/internal/httpclient/api_frontend.go +++ b/internal/httpclient/api_frontend.go @@ -665,6 +665,16 @@ type FrontendApi interface { console.log(session) ``` + When using a token template, the token is included in the `tokenized` field of the session. + + ```js + pseudo-code example + ... + const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) + + console.log(session.tokenized) // The JWT + ``` + Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user @@ -4477,6 +4487,7 @@ type FrontendApiApiToSessionRequest struct { ApiService FrontendApi xSessionToken *string cookie *string + tokenizeAs *string } func (r FrontendApiApiToSessionRequest) XSessionToken(xSessionToken string) FrontendApiApiToSessionRequest { @@ -4487,6 +4498,10 @@ func (r FrontendApiApiToSessionRequest) Cookie(cookie string) FrontendApiApiToSe r.cookie = &cookie return r } +func (r FrontendApiApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendApiApiToSessionRequest { + r.tokenizeAs = &tokenizeAs + return r +} func (r FrontendApiApiToSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ToSessionExecute(r) @@ -4521,6 +4536,16 @@ const session = await client.toSession("the-session-token") console.log(session) ``` +When using a token template, the token is included in the `tokenized` field of the session. + +```js +pseudo-code example +... +const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) + +console.log(session.tokenized) // The JWT +``` + Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user @@ -4579,6 +4604,9 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.tokenizeAs != nil { + localVarQueryParams.Add("tokenize_as", parameterToString(*r.tokenizeAs, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/model_session.go b/internal/httpclient/model_session.go index 0ded40302a1c..aa10a1dac55c 100644 --- a/internal/httpclient/model_session.go +++ b/internal/httpclient/model_session.go @@ -34,6 +34,8 @@ type Session struct { Identity *Identity `json:"identity,omitempty"` // The Session Issuance Timestamp When this session was issued at. Usually equal or close to `authenticated_at`. IssuedAt *time.Time `json:"issued_at,omitempty"` + // Tokenized is the tokenized (e.g. JWT) version of the session. It is only set when the `tokenize` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized *string `json:"tokenized,omitempty"` } // NewSession instantiates a new Session object @@ -334,6 +336,38 @@ func (o *Session) SetIssuedAt(v time.Time) { o.IssuedAt = &v } +// GetTokenized returns the Tokenized field value if set, zero value otherwise. +func (o *Session) GetTokenized() string { + if o == nil || o.Tokenized == nil { + var ret string + return ret + } + return *o.Tokenized +} + +// GetTokenizedOk returns a tuple with the Tokenized field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Session) GetTokenizedOk() (*string, bool) { + if o == nil || o.Tokenized == nil { + return nil, false + } + return o.Tokenized, true +} + +// HasTokenized returns a boolean if a field has been set. +func (o *Session) HasTokenized() bool { + if o != nil && o.Tokenized != nil { + return true + } + + return false +} + +// SetTokenized gets a reference to the given string and assigns it to the Tokenized field. +func (o *Session) SetTokenized(v string) { + o.Tokenized = &v +} + func (o Session) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Active != nil { @@ -363,6 +397,9 @@ func (o Session) MarshalJSON() ([]byte, error) { if o.IssuedAt != nil { toSerialize["issued_at"] = o.IssuedAt } + if o.Tokenized != nil { + toSerialize["tokenized"] = o.Tokenized + } return json.Marshal(toSerialize) } diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index c7c98188dc78..f14abd7a557d 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -55,12 +55,22 @@ type ( ) func NewPersister(ctx context.Context, r persisterDependencies, c *pop.Connection, extraMigrations ...fs.FS) (*Persister, error) { - m, err := popx.NewMigrationBox(mergefs.Merge(append([]fs.FS{migrations, networkx.Migrations}, extraMigrations...)...), popx.NewMigrator(c, r.Logger(), r.Tracer(ctx), 0)) + m, err := popx.NewMigrationBox( + mergefs.Merge( + append( + []fs.FS{ + migrations, networkx.Migrations, + }, + extraMigrations..., + )..., + ), + popx.NewMigrator(c, r.Logger(), r.Tracer(ctx), 0), + ) if err != nil { return nil, err } - m.DumpMigrations = false + m.DumpMigrations = false return &Persister{ c: c, mb: m, diff --git a/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json b/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json new file mode 100644 index 000000000000..19ce08b76775 --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=es256-without-jsonnet.json @@ -0,0 +1,8 @@ +{ + "exp": 1675209660, + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json b/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json new file mode 100644 index 000000000000..19ce08b76775 --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=es512-without-jsonnet.json @@ -0,0 +1,8 @@ +{ + "exp": 1675209660, + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json b/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json new file mode 100644 index 000000000000..84816eca114d --- /dev/null +++ b/session/.snapshots/TestTokenizer-case=rs512-with-jsonnet.json @@ -0,0 +1,12 @@ +{ + "aal": "aal1", + "exp": 1675209660, + "foo": "bar", + "iat": 1675209600, + "iss": "http://localhost/", + "nbf": 1675209600, + "schema_id": "default", + "second_claim": 1675209660, + "sid": "432caf86-c1d8-401c-978a-8da89133f78b", + "sub": "7458af86-c1d8-401c-978a-8da89133f78b" +} diff --git a/session/handler.go b/session/handler.go index fb0ae50d8c81..0fcfd9ad2103 100644 --- a/session/handler.go +++ b/session/handler.go @@ -34,10 +34,12 @@ type ( ManagementProvider PersistenceProvider x.WriterProvider + x.TracingProvider x.LoggingProvider x.CSRFProvider config.Provider sessiontokenexchange.PersistenceProvider + TokenizerProvider } HandlerProvider interface { SessionHandler() *Handler @@ -124,6 +126,13 @@ type toSession struct { // // in: header Cookie string `json:"Cookie"` + + // Returns the session additionally as a token (such as a JWT) + // + // The value of this parameter has to be a valid, configured Ory Session token template. For more information head over to [the documentation](http://ory.sh/docs/identities/session-to-jwt-cors). + // + // in: query + TokenizeAs string `json:"tokenize_as"` } // swagger:route GET /sessions/whoami frontend toSession @@ -156,6 +165,16 @@ type toSession struct { // // console.log(session) // ``` // +// When using a token template, the token is included in the `tokenized` field of the session. +// +// ```js +// // pseudo-code example +// // ... +// const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" }) +// +// console.log(session.tokenized) // The JWT +// ``` +// // Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator // Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn // credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user @@ -191,6 +210,9 @@ type toSession struct { // 403: errorGeneric // default: errorGeneric func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.Handler.whoami") + defer span.End() + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) c := h.r.Config() if err != nil { @@ -221,11 +243,19 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Pa // s.Devices = nil s.Identity = s.Identity.CopyWithoutCredentials() + tokenizeTemplate := r.URL.Query().Get("tokenize_as") + if tokenizeTemplate != "" { + if err := h.r.SessionTokenizer().TokenizeSession(ctx, tokenizeTemplate, s); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + // Set userId as the X-Kratos-Authenticated-Identity-Id header. w.Header().Set("X-Kratos-Authenticated-Identity-Id", s.Identity.ID.String()) - // Set Cache header only when configured - if c.SessionWhoAmICaching(r.Context()) { + // Set Cache header only when configured, and when no tokenization is requested. + if c.SessionWhoAmICaching(r.Context()) && len(tokenizeTemplate) == 0 { w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%d", int64(time.Until(s.ExpiresAt).Seconds()))) } diff --git a/session/handler_test.go b/session/handler_test.go index 352301a682ab..b565381a986c 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -5,6 +5,7 @@ package session_test import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -211,6 +212,32 @@ func TestSessionWhoAmI(t *testing.T) { }) }) + t.Run("tokenize", func(t *testing.T) { + setTokenizeConfig(conf, "es256", "jwk.es256.json", "") + conf.MustSet(ctx, config.ViperKeySessionWhoAmICaching, true) + + h3, _ := testhelpers.MockSessionCreateHandlerWithIdentityAndAMR(t, reg, createAAL1Identity(t, reg), []identity.CredentialsType{identity.CredentialsTypePassword}) + r.GET("/set/tokenize", h3) + + client := testhelpers.NewClientWithCookies(t) + testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set/"+"tokenize") + + res, err := client.Get(ts.URL + RouteWhoami + "?tokenize_as=es256") + require.NoError(t, err) + body := x.MustReadAll(res.Body) + assert.EqualValues(t, http.StatusOK, res.StatusCode, string(body)) + + token := gjson.GetBytes(body, "tokenized").String() + require.NotEmpty(t, token) + segments := strings.Split(token, ".") + require.Len(t, segments, 3, token) + decoded, err := base64.RawURLEncoding.DecodeString(segments[1]) + require.NoError(t, err) + + assert.NotEmpty(t, gjson.GetBytes(decoded, "sub").Str, decoded) + assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) + }) + /* t.Run("case=respects AAL config", func(t *testing.T) { conf.MustSet(ctx, config.ViperKeySessionLifespan, "1m") diff --git a/session/session.go b/session/session.go index 41b811b146b9..ceb792f66b2e 100644 --- a/session/session.go +++ b/session/session.go @@ -140,6 +140,11 @@ type Session struct { // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + // Tokenized is the tokenized (e.g. JWT) version of the session. + // + // It is only set when the `tokenize` query parameter was set to a valid tokenize template during calls to `/session/whoami`. + Tokenized string `json:"tokenized,omitempty" faker:"-" db:"-"` + // The Session Token // // The token of this session. diff --git a/session/stub/jwk.es256.json b/session/stub/jwk.es256.json new file mode 100644 index 000000000000..860ff1038bac --- /dev/null +++ b/session/stub/jwk.es256.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "247f1420-e581-4023-88e0-07ee662f80da", + "crv": "P-256", + "alg": "ES256", + "x": "1odGSu9bvVq_9QqqNny8TvvUElscLYoTExxhnomYOgQ", + "y": "pa4d4Ql1lO86PBnQ8efYzSzW9nUrsfLlomn3RIpH2Ic", + "d": "kPoEy2OcUeHobxp9jK00YKTs0CBoRTMWZJoPOe9K5hQ" + } + ] +} diff --git a/session/stub/jwk.es512.json b/session/stub/jwk.es512.json new file mode 100644 index 000000000000..d8ab0abcd746 --- /dev/null +++ b/session/stub/jwk.es512.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "bc7f7afc-6742-427c-bb9e-164fe0f8b6a7", + "crv": "P-521", + "alg": "ES512", + "x": "ASj36HQOpsWiaGyzK1F0GkxXRt37R01M-OCWFk8rFqH8UnFBk0qnCmVYWv3pwVPPsN0CfFiaXTrV1gUSapkkDgWY", + "y": "ALf5bqXExUq6FzQNQg01hDhR2lOKzkrC02Bc6Alld8Zji3-echbimNZltoOi4MhXbSJeWHpU8wzb3v9XAAW4eovn", + "d": "ALP0Sf7cmcELc9CQ2bWd6Qs-YxMu0N9EYZhDmR6qbYdGnvv-lcGy_ySoEJD0vPMKagA8PHDvFhC7ORwP-sBIJ4O_" + } + ] +} diff --git a/session/stub/jwk.rs512.json b/session/stub/jwk.rs512.json new file mode 100644 index 000000000000..1d70bb1595e9 --- /dev/null +++ b/session/stub/jwk.rs512.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "95311ff8-ff91-486b-9ad9-21df8bdc95d7", + "alg": "RS512", + "n": "6_ygtx-8qvTeN7ts_qFJCuOIEyxOnUpggbx04cG3vqjtyfzZMfi0wlidgMH3zhglij2MwC5lPLbze5n4lGQk26s8bl0uhdWlFHO_44hN3l2NVbPcocVZDWwqOdct2qRx1sEdRAt-P1a-2gxYN4HaemER9lgZSgbikJhmL3EEKhcr0QklUZyMcUnbaHAopzdiMKpnykR28-SXEizBi7JTI0hRDgCVmjuCRsciI5GAFy-nQ5n3Ofm0x8wGflKN1RAeWvolpakb7YJgQAXKQhOY3huoHlr-sh3ZO9vQjBgVQ1AM3k-z4OiQjJwvgogfLa1lSLmh9_Ax3LJQ5iax-aN9yQ", + "e": "AQAB", + "d": "DjZhy4WazEUBGSQtlUxLZN99M4Jonap8E3QxKeOL2Gy-HXsf7ZWH9Wh22-lSrlPf9upsDqr8p-Jw2ZHVWcKKQbyXYCI2ihLq5UdvWBm-btT9jRrO_-Mt0NQh2uftuAxNWty4kX-Ls-7agbFaosUsTlCIT2jQ5RdzD7hN9y98S7iJ6oUiBKdc7cRkNj2lGqowwM_IR4IoU2rY2pC7OtHZg_BKU8UnSYcPopn9tnobpr4E_AA1x49iUOkEny3wSXEfP2gEqb2Frqsf342b0pOtiq4WuiAopG2vBfMCKGddDBG_fFsxp858i03G9S4IuMDYk1uSeXd0zc_nb0GxFjXpXQ", + "p": "-_rHgqAtRJpeUbdTWhWQxlUFp-g0Yt1WCnKhCYXzgwM1kG-m9mzHW9mEJxTc0olygeo2A0vJ7htZJpd9NtJkD6g5yDZ7GIm5Emr9fNk4glz8nkVV9udU2gv6hVe83XU86cI4B4XWXhjdNvjQd7ohO7kBwB3uBmekVRm6ZaUCD-c", + "q": "78CGezUjroDlUA5CiF75_apyBgrEI41eNiOPwN9X7Jy-3zpvecY9ZfIuyf5O1yNkC_zjanGEKom6L3iqcAV_1-wzGMOMvtIwhmkTPT9g0f1Z1utRQVljyXFBml7__xb9cCfq62YnH7irsM5nbe9Txziq2zvBjWCkkjJwTBPWDs8", + "dp": "lXjhuJ8Du1pG8Ppqu1lnk_8DZ-LakHrzeyccV-XZ2bGhqJhS1oMYj2esePJrO4jFIEOq3rGqi1A1xiq-4DJVoOQNwrJuutOXsVE-JT1FxC8cu1Yt9FSthNruNQMiycut4oyPaAcAbrkZIG7gWuVSqXbJjwkyFSKN3N1yMLF9U6k", + "dq": "MLapnHcbnOVLsoxzMEo7-TKcoGWnnKGots9a8hFvSABBOBIjfFavOvuOTjSByGzEczsa6hHOjOYXEnYuCzzS0QiJCUsSWeNTQLww0I0EGyajDmwZwnFrOQ7uCXOsCCSfsh4qOVI0ONnI6M_HbCrolt4IuSrXFObCCYJ-FrchEzk", + "qi": "fBkpKoiyeXnwBYezKcxkeu6E2F2tWiRgRikM-s50kRu2fGgmAZ-nIIHU5glPXCPbn4IolURxeLm8ZZyCCo2tNPWiAQV2C7UF04Kh8QANULoUdpObjplLEWQuCDgz5m9EkR4Sx6FGGA1TvUpYCjks8YxvdSIa56o3l72I49aL1t8" + } + ] +} diff --git a/session/stub/rs512-template.jsonnet b/session/stub/rs512-template.jsonnet new file mode 100644 index 000000000000..fa67d936b54a --- /dev/null +++ b/session/stub/rs512-template.jsonnet @@ -0,0 +1,12 @@ +local claims = std.extVar('claims'); +local session = std.extVar('session'); + +{ + claims: { + foo: "bar", + sub: "can not be overwritten", + schema_id: session.identity.schema_id, + aal: session.authenticator_assurance_level, + second_claim: claims.exp, + } +} diff --git a/session/tokenizer.go b/session/tokenizer.go new file mode 100644 index 000000000000..f4317f067ac1 --- /dev/null +++ b/session/tokenizer.go @@ -0,0 +1,152 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session + +import ( + "context" + "encoding/json" + "time" + + "go.opentelemetry.io/otel/trace" + + "github.com/ory/kratos/x/events" + + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" + "github.com/ory/x/fetcher" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/jwksx" + "github.com/ory/x/otelx" +) + +type ( + tokenizerDependencies interface { + jsonnetsecure.VMProvider + x.TracingProvider + x.HTTPClientProvider + config.Provider + x.JWKFetchProvider + } + Tokenizer struct { + r tokenizerDependencies + nowFunc func() time.Time + } + TokenizerProvider interface { + SessionTokenizer() *Tokenizer + } +) + +func NewTokenizer(r tokenizerDependencies) *Tokenizer { + return &Tokenizer{r: r, nowFunc: time.Now} +} + +func (s *Tokenizer) SetNowFunc(t func() time.Time) { + s.nowFunc = t +} + +func (s *Tokenizer) TokenizeSession(ctx context.Context, template string, session *Session) (err error) { + ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.TokenizeSession") + defer otelx.End(span, &err) + + tpl, err := s.r.Config().TokenizeTemplate(ctx, template) + if err != nil { + return err + } + + httpClient := s.r.HTTPClient(ctx) + key, err := s.r.Fetcher().ResolveKey( + ctx, + tpl.JWKSURL, + jwksx.WithCacheEnabled(), + jwksx.WithCacheTTL(time.Hour), + jwksx.WithHTTPClient(httpClient)) + if err != nil { + if errors.Is(err, jwksx.ErrUnableToFindKeyID) { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Could not find key a suitable key for tokenization in the JWKS url.")) + } + return err + } + + alg := jwt.GetSigningMethod(key.Algorithm()) + if alg == nil { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("The JSON Web Key must include a valid \"alg\" parameter but \"%s\" was given.", key.Algorithm())) + } + + vm, err := s.r.JsonnetVM(ctx) + if err != nil { + return err + } + + fetch := fetcher.NewFetcher(fetcher.WithClient(httpClient)) + + now := s.nowFunc() + token := jwt.New(alg) + token.Header["kid"] = key.KeyID() + claims := jwt.MapClaims{ + "jti": uuid.Must(uuid.NewV4()).String(), + "iss": s.r.Config().SelfPublicURL(ctx).String(), + "exp": now.Add(tpl.TTL).Unix(), + "sub": session.IdentityID.String(), + "sid": session.ID.String(), + "nbf": now.Unix(), + "iat": now.Unix(), + } + + if mapper := tpl.ClaimsMapperURL; len(mapper) > 0 { + jn, err := fetch.FetchContext(ctx, mapper) + if err != nil { + return err + } + + sessionRaw, err := json.Marshal(session) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to encode session to JSON.")) + } + + claimsRaw, err := json.Marshal(&claims) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf("Unable to encode claims to JSON.")) + } + + vm.ExtCode("session", string(sessionRaw)) + vm.ExtCode("claims", string(claimsRaw)) + + evaluated, err := vm.EvaluateAnonymousSnippet(tpl.ClaimsMapperURL, jn.String()) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithWrap(err).WithDebug(err.Error()).WithReasonf("Unable to execute tokenizer JsonNet.")) + } + + evaluatedClaims := gjson.Get(evaluated, "claims") + if !evaluatedClaims.IsObject() { + return errors.WithStack(herodot.ErrBadRequest.WithWrap(err).WithReasonf("Expected tokenizer JsonNet to return a claims object but it did not.")) + } + + if err := json.Unmarshal([]byte(evaluatedClaims.Raw), &claims); err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithWrap(err).WithReasonf("Unable to encode tokenized claims.")) + } + + claims["sub"] = session.IdentityID.String() + } + + var privateKey interface{} + if err := key.Raw(&privateKey); err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithWrap(err).WithReasonf("Unable to decode the given private key.")) + } + + token.Claims = claims + result, err := token.SignedString(privateKey) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithWrap(err).WithReasonf("Unable to sign JSON Web Token.")) + } + + trace.SpanFromContext(ctx).AddEvent(events.NewSessionJWTIssued(ctx, session.ID, session.IdentityID, tpl.TTL)) + session.Tokenized = result + return nil +} diff --git a/session/tokenizer_test.go b/session/tokenizer_test.go new file mode 100644 index 000000000000..bb69b222b83e --- /dev/null +++ b/session/tokenizer_test.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package session_test + +import ( + "context" + _ "embed" + "net/http/httptest" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/jwk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/session" + "github.com/ory/x/snapshotx" +) + +//go:embed stub/jwk.es256.json +var es256Key []byte + +//go:embed stub/jwk.es512.json +var es512Key []byte + +func validateTokenized(t *testing.T, raw string, key []byte) *jwt.Token { + token, err := jwt.Parse( + raw, + func(token *jwt.Token) (target interface{}, _ error) { + set, err := jwk.Parse(key) + if err != nil { + return nil, err + } + key, _ := set.Get(0) + if pk, err := key.PublicKey(); err != nil { + return nil, err + } else if err := pk.Raw(&target); err != nil { + return nil, err + } + return target, nil + }, + // We use a fixed time function for snapshot testing, and thus can not validate claims. + jwt.WithoutClaimsValidation(), + ) + require.NoError(t, err) + return token +} + +func setTokenizeConfig(conf *config.Config, templateID string, keyFile string, mapper string) { + conf.MustSet(context.Background(), config.ViperKeySessionTokenizerTemplates+"."+templateID, &config.SessionTokenizeFormat{ + TTL: time.Minute, + JWKSURL: "file://stub/" + keyFile, + ClaimsMapperURL: mapper, + }) +} + +func TestTokenizer(t *testing.T) { + ctx := context.Background() + now := time.Now() + + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/") + tkn := session.NewTokenizer(reg) + nowDate := time.Date(2023, 02, 01, 00, 00, 00, 0, time.UTC) + tkn.SetNowFunc(func() time.Time { + return nowDate + }) + + r := httptest.NewRequest("GET", "/sessions/whoami", nil) + i := identity.NewIdentity("default") + i.ID = uuid.FromStringOrNil("7458af86-c1d8-401c-978a-8da89133f78b") + + s, err := session.NewActiveSession(r, i, conf, now, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) + s.ID = uuid.FromStringOrNil("432caf86-c1d8-401c-978a-8da89133f78b") + + t.Run("case=es256-without-jsonnet", func(t *testing.T) { + tid := "es256-no-template" + setTokenizeConfig(conf, tid, "jwk.es256.json", "") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es256Key) + + resultClaims := token.Claims.(jwt.MapClaims) + assert.Equal(t, i.ID.String(), resultClaims["sub"]) + assert.Equal(t, s.ID.String(), resultClaims["sid"]) + assert.NotEmpty(t, resultClaims["jti"]) + assert.EqualValues(t, resultClaims["exp"], nowDate.Add(time.Minute).Unix()) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=es512-without-jsonnet", func(t *testing.T) { + tid := "es512-no-template" + setTokenizeConfig(conf, tid, "jwk.es512.json", "") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es512Key) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) + + t.Run("case=rs512-with-jsonnet", func(t *testing.T) { + tid := "rs512-template" + setTokenizeConfig(conf, tid, "jwk.es512.json", "file://stub/rs512-template.jsonnet") + + require.NoError(t, tkn.TokenizeSession(ctx, tid, s)) + token := validateTokenized(t, s.Tokenized, es512Key) + + snapshotx.SnapshotT(t, token.Claims, snapshotx.ExceptPaths("jti")) + }) +} diff --git a/spec/api.json b/spec/api.json index 6c12cdbf26f4..06cd193d9edc 100644 --- a/spec/api.json +++ b/spec/api.json @@ -1736,6 +1736,10 @@ "description": "The Session Issuance Timestamp\n\nWhen this session was issued at. Usually equal or close to `authenticated_at`.", "format": "date-time", "type": "string" + }, + "tokenized": { + "description": "Tokenized is the tokenized (e.g. JWT) version of the session.\n\nIt is only set when the `tokenize` query parameter was set to a valid tokenize template during calls to `/session/whoami`.", + "type": "string" } }, "required": [ @@ -6846,7 +6850,7 @@ }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nWhen the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header\nin the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking:\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nWhen the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header\nin the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nWhen using a token template, the token is included in the `tokenized` field of the session.\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\", { tokenize_as: \"example-jwt-template\" })\n\nconsole.log(session.tokenized) // The JWT\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking:\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "operationId": "toSession", "parameters": [ { @@ -6866,6 +6870,14 @@ "schema": { "type": "string" } + }, + { + "description": "Returns the session additionally as a token (such as a JWT)\n\nThe value of this parameter has to be a valid, configured Ory Session token template. For more information head over to [the documentation](http://ory.sh/docs/identities/session-to-jwt-cors).", + "in": "query", + "name": "tokenize_as", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 3bfd4226af23..7d4321cf0b78 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -3010,7 +3010,7 @@ }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nWhen the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header\nin the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking:\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nWhen the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header\nin the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nWhen using a token template, the token is included in the `tokenized` field of the session.\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\", { tokenize_as: \"example-jwt-template\" })\n\nconsole.log(session.tokenized) // The JWT\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking:\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "produces": [ "application/json" ], @@ -3035,6 +3035,12 @@ "description": "Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that\nscenario you must include the HTTP Cookie Header which originally was included in the request to your server.\nAn example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`.\n\nIt is ok if more than one cookie are included here as all other cookies will be ignored.", "name": "Cookie", "in": "header" + }, + { + "type": "string", + "description": "Returns the session additionally as a token (such as a JWT)\n\nThe value of this parameter has to be a valid, configured Ory Session token template. For more information head over to [the documentation](http://ory.sh/docs/identities/session-to-jwt-cors).", + "name": "tokenize_as", + "in": "query" } ], "responses": { @@ -4730,6 +4736,10 @@ "description": "The Session Issuance Timestamp\n\nWhen this session was issued at. Usually equal or close to `authenticated_at`.", "type": "string", "format": "date-time" + }, + "tokenized": { + "description": "Tokenized is the tokenized (e.g. JWT) version of the session.\n\nIt is only set when the `tokenize` query parameter was set to a valid tokenize template during calls to `/session/whoami`.", + "type": "string" } } }, diff --git a/x/events/events.go b/x/events/events.go index b8f26bab2ffc..83da68e98558 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -5,6 +5,7 @@ package events import ( "context" + "time" "github.com/gofrs/uuid" otelattr "go.opentelemetry.io/otel/attribute" @@ -17,6 +18,7 @@ const ( SessionIssued semconv.Event = "SessionIssued" SessionChanged semconv.Event = "SessionChanged" SessionRevoked semconv.Event = "SessionRevoked" + SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" RegistrationFailed semconv.Event = "RegistrationFailed" RegistrationSucceeded semconv.Event = "RegistrationSucceeded" LoginFailed semconv.Event = "LoginFailed" @@ -39,12 +41,17 @@ const ( attributeKeySelfServiceSSOProviderUsed semconv.AttributeKey = "SelfServiceSSOProviderUsed" attributeKeyLoginRequestedAAL semconv.AttributeKey = "LoginRequestedAAL" attributeKeyLoginRequestedPrivilegedSession semconv.AttributeKey = "LoginRequestedPrivilegedSession" + attributeKeyTokenizedSessionTTL semconv.AttributeKey = "TokenizedSessionTTL" ) func attrSessionID(val uuid.UUID) otelattr.KeyValue { return otelattr.String(attributeKeySessionID.String(), val.String()) } +func attrTokenizedSessionTTL(ttl time.Duration) otelattr.KeyValue { + return otelattr.String(attributeKeyTokenizedSessionTTL.String(), ttl.String()) +} + func attrSessionAAL(val string) otelattr.KeyValue { return otelattr.String(attributeKeySessionAAL.String(), val) } @@ -232,3 +239,15 @@ func NewSessionRevoked(ctx context.Context, sessionID, identityID uuid.UUID) (st )..., ) } + +func NewSessionJWTIssued(ctx context.Context, sessionID, identityID uuid.UUID, ttl time.Duration) (string, trace.EventOption) { + return SessionTokenizedAsJWT.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attrTokenizedSessionTTL(ttl), + )..., + ) +} diff --git a/x/fetcher.go b/x/fetcher.go new file mode 100644 index 000000000000..77f3dbf31099 --- /dev/null +++ b/x/fetcher.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "github.com/ory/x/jwksx" + +type JWKFetchProvider interface { + Fetcher() *jwksx.FetcherNext +}